From a504dfee27318300fe54c0f5760ab1458a3c534f Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 18 Sep 2026 02:16:47 +0000 Subject: [PATCH 01/21] ci: Update pyrefly dependency version to 1.3.0 (#42445) --- api/commands/storage.py | 2 +- api/controllers/console/app/conversation.py | 8 +++-- .../console/datasets/data_source.py | 2 +- api/core/rag/extractor/notion_extractor.py | 2 +- .../rag/index_processor/index_processor.py | 3 +- api/pyproject.toml | 2 +- api/services/legacy_model_type_migration.py | 10 +++--- api/tasks/deal_dataset_index_update_task.py | 12 +++---- api/tasks/deal_dataset_vector_index_task.py | 12 +++---- api/tasks/enable_segments_to_index_task.py | 3 +- .../controllers/files/test_appdeploy_files.py | 4 +-- .../unit_tests/core/tools/test_signature.py | 17 ++++++++-- .../test_workflow_node_execution_tasks.py | 11 ++++++- api/uv.lock | 32 +++++++++---------- 14 files changed, 72 insertions(+), 48 deletions(-) diff --git a/api/commands/storage.py b/api/commands/storage.py index f23b17680a0..a21b66aa4e8 100644 --- a/api/commands/storage.py +++ b/api/commands/storage.py @@ -751,7 +751,7 @@ def migrate_oss( UploadFile.storage_type == source_storage_type, UploadFile.key.in_(copied_upload_file_keys), ) - .values(storage_type=dify_config.STORAGE_TYPE) + .values(storage_type=StorageType(dify_config.STORAGE_TYPE)) ), ).rowcount db.session.commit() diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 2f0977b39f8..6b136bacb2d 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -406,9 +406,11 @@ def _get_conversation(session: Session, current_user: Account, app_model, conver .where(Conversation.id == conversation_id, Conversation.read_at.is_(None)) # Keep updated_at unchanged when only marking a conversation as read. .values( - read_at=naive_utc_now(), - read_account_id=current_user.id, - updated_at=Conversation.updated_at, + { + Conversation.read_at: naive_utc_now(), + Conversation.read_account_id: current_user.id, + Conversation.updated_at: Conversation.updated_at, + } ) ) session.flush() diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index f5506e36850..39b1d454f13 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -157,7 +157,7 @@ class DataSourceApi(Resource): data_source_oauth_base_path = "/console/api/oauth/data-source" providers = ["notion"] - integrate_data = [] + integrate_data: list[dict[str, bool | datetime | dict[str, Any] | str | None]] = [] for provider in providers: # existing_integrate = next((ai for ai in data_source_integrates if ai.provider == provider), None) existing_integrates = filter(lambda item: item.provider == provider, data_source_integrates) diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index f0c30b36d7e..f8b849255c1 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -358,7 +358,7 @@ class NotionExtractor(BaseExtractor): db.session.execute( update(DocumentModel) .where(DocumentModel.id == document_model.id) - .values(data_source_info=json.dumps(data_source_info)) + .values({DocumentModel.data_source_info: json.dumps(data_source_info)}) ) db.session.commit() diff --git a/api/core/rag/index_processor/index_processor.py b/api/core/rag/index_processor/index_processor.py index dc951c554fe..2ab23ae3a82 100644 --- a/api/core/rag/index_processor/index_processor.py +++ b/api/core/rag/index_processor/index_processor.py @@ -15,6 +15,7 @@ from core.rag.index_processor.index_processor_base import SummaryIndexSettingDic from core.workflow.nodes.knowledge_index.exc import KnowledgeIndexNodeError from core.workflow.nodes.knowledge_index.protocols import IndexingResultDict, Preview, PreviewItem, QaPreview from models.dataset import Dataset, Document, DocumentSegment +from models.enums import SegmentStatus from services.vector_space_admission_service import VectorSpaceAdmissionService from .index_processor_factory import IndexProcessorFactory @@ -157,7 +158,7 @@ class IndexProcessor: DocumentSegment.tenant_id == dataset.tenant_id, ) .values( - status="completed", + status=SegmentStatus.COMPLETED, enabled=True, completed_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), ) diff --git a/api/pyproject.toml b/api/pyproject.toml index 28ccf3c3d8a..cfbf28c95b3 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -186,7 +186,7 @@ dev = [ # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "pyrefly>=1.2.0", + "pyrefly>=1.3.1", "xinference-client>=2.7.0", "types-pytz>=2026.3.1.20260727", "types-bleach>=6.4.0.20260728", diff --git a/api/services/legacy_model_type_migration.py b/api/services/legacy_model_type_migration.py index 2034246991f..7fcbb95c421 100644 --- a/api/services/legacy_model_type_migration.py +++ b/api/services/legacy_model_type_migration.py @@ -876,7 +876,7 @@ class Migration: session.execute( sa.update(ProviderModel) .where(ProviderModel.id == str(plan.winner.row.id)) - .values(model_type=plan.winner.canonical_model_type.value) + .values(model_type=plan.winner.canonical_model_type) ) self._log_row_updated( ProviderModel.__tablename__, @@ -1099,7 +1099,7 @@ class Migration: session.execute( sa.update(TenantDefaultModel) .where(TenantDefaultModel.id == str(plan.winner.row.id)) - .values(model_type=plan.winner.canonical_model_type.value) + .values(model_type=plan.winner.canonical_model_type) ) self._log_row_updated( TenantDefaultModel.__tablename__, @@ -1314,7 +1314,7 @@ class Migration: session.execute( sa.update(ProviderModelSetting) .where(ProviderModelSetting.id == str(plan.winner.row.id)) - .values(model_type=plan.winner.canonical_model_type.value) + .values(model_type=plan.winner.canonical_model_type) ) self._log_row_updated( ProviderModelSetting.__tablename__, @@ -1751,7 +1751,7 @@ class Migration: session.execute( sa.update(LoadBalancingModelConfig) .where(LoadBalancingModelConfig.id == processed_row_id) - .values(model_type=current_row.canonical_model_type.value) + .values(model_type=current_row.canonical_model_type) ) self._log_row_updated( LoadBalancingModelConfig.__tablename__, @@ -2096,7 +2096,7 @@ class Migration: session.execute( sa.update(ProviderModelCredential) .where(ProviderModelCredential.id == winner_credential_id) - .values(model_type=plan.winner.canonical_model_type.value) + .values(model_type=plan.winner.canonical_model_type) ) self._log_row_updated( ProviderModelCredential.__tablename__, diff --git a/api/tasks/deal_dataset_index_update_task.py b/api/tasks/deal_dataset_index_update_task.py index 63279348455..dfbbe3de065 100644 --- a/api/tasks/deal_dataset_index_update_task.py +++ b/api/tasks/deal_dataset_index_update_task.py @@ -48,7 +48,7 @@ def deal_dataset_index_update_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id.in_(dataset_documents_ids)) - .values(indexing_status="indexing") + .values({DatasetDocument.indexing_status: "indexing"}) ) session.commit() @@ -90,14 +90,14 @@ def deal_dataset_index_update_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="completed") + .values({DatasetDocument.indexing_status: "completed"}) ) session.commit() except Exception as e: session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="error", error=str(e)) + .values({DatasetDocument.indexing_status: "error", DatasetDocument.error: str(e)}) ) session.commit() elif action == "update": @@ -116,7 +116,7 @@ def deal_dataset_index_update_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id.in_(dataset_documents_ids)) - .values(indexing_status="indexing") + .values({DatasetDocument.indexing_status: "indexing"}) ) session.commit() @@ -191,14 +191,14 @@ def deal_dataset_index_update_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="completed") + .values({DatasetDocument.indexing_status: "completed"}) ) session.commit() except Exception as e: session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="error", error=str(e)) + .values({DatasetDocument.indexing_status: "error", DatasetDocument.error: str(e)}) ) session.commit() else: diff --git a/api/tasks/deal_dataset_vector_index_task.py b/api/tasks/deal_dataset_vector_index_task.py index 001436a9af6..928134fb7b7 100644 --- a/api/tasks/deal_dataset_vector_index_task.py +++ b/api/tasks/deal_dataset_vector_index_task.py @@ -52,7 +52,7 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id.in_(dataset_documents_ids)) - .values(indexing_status="indexing") + .values({DatasetDocument.indexing_status: "indexing"}) ) session.commit() @@ -86,14 +86,14 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="completed") + .values({DatasetDocument.indexing_status: "completed"}) ) session.commit() except Exception as e: session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="error", error=str(e)) + .values({DatasetDocument.indexing_status: "error", DatasetDocument.error: str(e)}) ) session.commit() elif action == "update": @@ -112,7 +112,7 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id.in_(dataset_documents_ids)) - .values(indexing_status="indexing") + .values({DatasetDocument.indexing_status: "indexing"}) ) session.commit() @@ -187,14 +187,14 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str): session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="completed") + .values({DatasetDocument.indexing_status: "completed"}) ) session.commit() except Exception as e: session.execute( update(DatasetDocument) .where(DatasetDocument.id == dataset_document.id) - .values(indexing_status="error", error=str(e)) + .values({DatasetDocument.indexing_status: "error", DatasetDocument.error: str(e)}) ) session.commit() else: diff --git a/api/tasks/enable_segments_to_index_task.py b/api/tasks/enable_segments_to_index_task.py index d4ab9a333df..0de5effcd47 100644 --- a/api/tasks/enable_segments_to_index_task.py +++ b/api/tasks/enable_segments_to_index_task.py @@ -14,6 +14,7 @@ from extensions.ext_redis import redis_client from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, DocumentSegment from models.dataset import Document as DatasetDocument +from models.enums import SegmentStatus logger = logging.getLogger(__name__) @@ -133,7 +134,7 @@ def enable_segments_to_index_task(segment_ids: list, dataset_id: str, document_i DocumentSegment.dataset_id == dataset_id, DocumentSegment.document_id == document_id, ) - .values(error=str(e), status="error", disabled_at=naive_utc_now(), enabled=False) + .values(error=str(e), status=SegmentStatus.ERROR, disabled_at=naive_utc_now(), enabled=False) ) session.commit() finally: diff --git a/api/tests/unit_tests/controllers/files/test_appdeploy_files.py b/api/tests/unit_tests/controllers/files/test_appdeploy_files.py index b9e7d6e289e..04623b8a2fc 100644 --- a/api/tests/unit_tests/controllers/files/test_appdeploy_files.py +++ b/api/tests/unit_tests/controllers/files/test_appdeploy_files.py @@ -12,7 +12,7 @@ from uuid import UUID import jwt import pytest from flask import Flask -from sqlalchemy import update +from sqlalchemy import null, update from sqlalchemy.engine import Engine from sqlalchemy.orm import Session, sessionmaker @@ -775,7 +775,7 @@ def test_content_disposition_follows_the_inline_whitelist( @pytest.mark.usefixtures("sqlite_db", "stored_bytes") def test_content_downloads_a_file_with_no_recorded_mime_type(app: Flask, sqlite_session: Session) -> None: upload_file = _persist_upload_file(sqlite_session, owner_id="anyone") - sqlite_session.execute(update(UploadFile).where(UploadFile.id == upload_file.id).values(mime_type=None)) + sqlite_session.execute(update(UploadFile).where(UploadFile.id == upload_file.id).values(mime_type=null())) sqlite_session.commit() token = _content_token(file_id=upload_file.id, kind=FileKind.UPLOAD) diff --git a/api/tests/unit_tests/core/tools/test_signature.py b/api/tests/unit_tests/core/tools/test_signature.py index ed1b043087a..1214d8a4caf 100644 --- a/api/tests/unit_tests/core/tools/test_signature.py +++ b/api/tests/unit_tests/core/tools/test_signature.py @@ -10,6 +10,7 @@ from typing import Literal from urllib.parse import parse_qs, urlparse import pytest +from typing_extensions import TypedDict from core.tools.signature import ( bind_file_uri, @@ -22,6 +23,16 @@ from core.tools.signature import ( ) +class _SignedPluginFile(TypedDict, closed=True): + filename: str + mimetype: str + tenant_id: str + user_id: str + timestamp: str + nonce: str + sign: str + + @pytest.fixture(autouse=True) def _signature_config(config_overrides: Callable[..., None]) -> None: config_overrides( @@ -213,7 +224,7 @@ def test_plugin_upload_signature_binds_max_size_without_legacy_payload_ambiguity max_size=1024, ) query = parse_qs(urlparse(uri).query) - signed = { + signed: _SignedPluginFile = { "filename": "report.pdf", "mimetype": "application/pdf", "tenant_id": "tenant-id", @@ -227,7 +238,7 @@ def test_plugin_upload_signature_binds_max_size_without_legacy_payload_ambiguity assert verify_plugin_file_signature(**signed, user_from=user_from, max_size=1024) is True assert verify_plugin_file_signature(**signed, user_from=user_from, max_size=2048) is False assert verify_plugin_file_signature(**signed, user_from=user_from) is False - forged = {**signed, "nonce": f"{signed['nonce']}{forged_nonce_suffix}"} + forged: _SignedPluginFile = {**signed, "nonce": f"{signed['nonce']}{forged_nonce_suffix}"} assert verify_plugin_file_signature(**forged) is False @@ -248,7 +259,7 @@ def test_plugin_upload_signature_binds_account_user_from( query = parse_qs(urlparse(uri).query) assert query["user_from"] == ["account"] - signed = { + signed: _SignedPluginFile = { "filename": "report.pdf", "mimetype": "application/pdf", "tenant_id": "tenant-id", diff --git a/api/tests/unit_tests/tasks/test_workflow_node_execution_tasks.py b/api/tests/unit_tests/tasks/test_workflow_node_execution_tasks.py index 0b3e314a3f4..2a87d73fd35 100644 --- a/api/tests/unit_tests/tasks/test_workflow_node_execution_tasks.py +++ b/api/tests/unit_tests/tasks/test_workflow_node_execution_tasks.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock import pytest from sqlalchemy import func, select from sqlalchemy.orm import Session, sessionmaker +from typing_extensions import TypedDict from graphon.entities import WorkflowNodeExecution from graphon.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey @@ -29,6 +30,14 @@ ACCOUNT_ID = "00000000-0000-0000-0000-000000000050" EXECUTION_ID = "00000000-0000-0000-0000-000000000060" +class _ExecutionTaskKwargs(TypedDict, closed=True): + tenant_id: str + app_id: str + triggered_from: str + creator_user_id: str + creator_user_role: str + + class _TaskWithRequestContext(Protocol): def push_request(self, *, retries: int) -> None: ... @@ -313,7 +322,7 @@ def test_task_repeated_delivery_updates_one_row( outputs={"delivery": 2}, finished_at=created_at + timedelta(seconds=5), ) - task_kwargs = { + task_kwargs: _ExecutionTaskKwargs = { "tenant_id": TENANT_ID, "app_id": APP_ID, "triggered_from": WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value, diff --git a/api/uv.lock b/api/uv.lock index 701309c91f3..5723a6825ed 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1741,7 +1741,7 @@ dev = [ { name = "lxml-stubs", specifier = ">=0.5.1" }, { name = "mypy", specifier = ">=2.3.1" }, { name = "pandas-stubs", specifier = ">=3.0.5.260730" }, - { name = "pyrefly", specifier = ">=1.2.0" }, + { name = "pyrefly", specifier = ">=1.3.1" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-benchmark", specifier = ">=5.3.0" }, { name = "pytest-cov", specifier = ">=7.1.0" }, @@ -3355,8 +3355,8 @@ name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11", marker = "sys_platform != 'emscripten'" }, - { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "h11" }, + { name = "truststore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ @@ -5606,21 +5606,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.2.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/7e/082cc1b392318bacfbc087732524f3e089e9e7fe45b7645c4649b6f1f1e9/pyrefly-1.3.1.tar.gz", hash = "sha256:b774f1dfac82867fc16fe2175d80a630e2a72a6835d5b7e492568d6bfb6b05e7", size = 6679145, upload-time = "2026-09-15T05:09:34.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, - { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, - { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, - { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, - { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, - { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, - { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bc/f09b5bd7eed796349bd93627aa2c3086df7ed0a114dafc3d2b2841599ee7/pyrefly-1.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a2a7dfed824fb066aa8a7c8d856c152b98e998fb84b954d5f892fb9834c0bd1", size = 15090117, upload-time = "2026-09-15T05:08:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/78/24/a31e7de8e0f310c9f0753826d0df8b6a8ff86793941a6bd88ba24b735633/pyrefly-1.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37a1a1fb41e7190fe3964a877875dd809d6ee2887809e2c3d6e5f25a69805bfd", size = 14430773, upload-time = "2026-09-15T05:09:01.67Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/022352beb186f36d6213a95a975c42676ee418ff5e730629378d25564c5e/pyrefly-1.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b7650bc651a5a784701f2fb4f579da9a421c4d1b5fd91f9048f4529e46093c", size = 14877762, upload-time = "2026-09-15T05:09:04.767Z" }, + { url = "https://files.pythonhosted.org/packages/19/2c/d62a1b445aad1fd46375b83393b8a67da53883aeeba73d269ce71b3e8fd1/pyrefly-1.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad57f38bc73679c14a5f62995a2bd3bc43649b7270c858487e2eab78a6f95362", size = 16118225, upload-time = "2026-09-15T05:09:07.828Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/3c3fa8a13e3ce8d8322ffb3627255a41a52261f17793e1a24e9214f10857/pyrefly-1.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3506b1252e9f5d755d0693c5a46d46b85579d36534f6e22a41eb8e098d0dde90", size = 16051197, upload-time = "2026-09-15T05:09:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/19/d1/84f81b9e32288b5d11c75e006f67e0f6b78b85da575673bd1a21524c8172/pyrefly-1.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d5a17c1ad655e5f93a6e8e0be90b8db28c6015fe07d6ed4e054891c9e14ef8", size = 15440062, upload-time = "2026-09-15T05:09:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/46/3c/2aa0c5ad6a2957a728bb38cfab70d1e9cafad21b0b1c1d92df74a3cf22bd/pyrefly-1.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c61e9dcf888beb81ca0e0c60ea4442640fdcf2f1f65bfe3d9f5d89b85516eb03", size = 14898534, upload-time = "2026-09-15T05:09:17.823Z" }, + { url = "https://files.pythonhosted.org/packages/03/5d/2974f249fb2e285f966485719d977c5a2248cc4866403af94d935775ff15/pyrefly-1.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a78eed1c53f37807f0e2997be8edbe833b91eb9915f2cb553a1bdcd599e7105c", size = 15477758, upload-time = "2026-09-15T05:09:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/d31f8f0abf5b0610ec24183286ee8a1ee5b76c9aaaa91091399d7e1aaf60/pyrefly-1.3.1-py3-none-win32.whl", hash = "sha256:ea0d0349f1bc80fe8e00a7b3640e40db7486398b685f87b9bd5d2207063668f0", size = 14133005, upload-time = "2026-09-15T05:09:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/3f/23/1859f47f536314755d80c4ec2a76d2698435ae8b73604bcd1dfa57ecb39b/pyrefly-1.3.1-py3-none-win_amd64.whl", hash = "sha256:4c24e7d99df9237916c33be23714219a7c8eaf22bad2ac0c3e1d45adcf7df8fb", size = 15066557, upload-time = "2026-09-15T05:09:28.265Z" }, + { url = "https://files.pythonhosted.org/packages/92/2a/73e3721921c2bb2c950a9f8c362ee5e2367556b5d4416eb4f8f88503e66f/pyrefly-1.3.1-py3-none-win_arm64.whl", hash = "sha256:1ed85e2b957a78e93353125d890ce3668ce450e49b74025a15c87b6e8ec7f3d1", size = 14348209, upload-time = "2026-09-15T05:09:31.422Z" }, ] [[package]] From bfa4a8b5e437ed4a89675e5ebc41a2eb726d0f8c Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:31:51 +0000 Subject: [PATCH 02/21] refactor(web): remove obsolete maintenance notice (#42464) --- oxlint-suppressions.json | 2 +- web/app/(commonLayout)/layout.tsx | 2 - web/app/account/(commonLayout)/layout.tsx | 2 - .../base/icons/src/vender/line/general/X.json | 39 ------ .../base/icons/src/vender/line/general/X.tsx | 18 --- .../icons/src/vender/line/general/index.ts | 2 - .../__tests__/maintenance-notice.spec.tsx | 129 ------------------ .../components/header/maintenance-notice.tsx | 67 --------- web/app/components/header/storage.ts | 6 - .../__tests__/console-bootstrap.spec.tsx | 4 - web/env.ts | 4 - web/i18n-config/language.ts | 66 --------- 12 files changed, 1 insertion(+), 340 deletions(-) delete mode 100644 web/app/components/base/icons/src/vender/line/general/X.json delete mode 100644 web/app/components/base/icons/src/vender/line/general/X.tsx delete mode 100644 web/app/components/header/__tests__/maintenance-notice.spec.tsx delete mode 100644 web/app/components/header/maintenance-notice.tsx delete mode 100644 web/app/components/header/storage.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index ad3e770766c..fad85761d11 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1287,7 +1287,7 @@ }, "web/app/components/base/icons/src/vender/line/general/index.ts": { "no-barrel-files/no-barrel-files": { - "count": 10 + "count": 9 } }, "web/app/components/base/icons/src/vender/line/images/index.ts": { diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index b011b432ef8..c2eed0a2818 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -1,6 +1,5 @@ import * as React from 'react' import Zendesk from '@/app/components/base/zendesk' -import MaintenanceNotice from '@/app/components/header/maintenance-notice' import MainNavLayout from '@/app/components/main-nav/server' import { CommonLayoutGlobalMounts } from './global-mounts' import { ConsoleContextProviders, ConsoleRuntimeProviders } from './providers' @@ -16,7 +15,6 @@ export default async function Layout({
- {children} diff --git a/web/app/account/(commonLayout)/layout.tsx b/web/app/account/(commonLayout)/layout.tsx index 06ec2ae4e5d..0e323e98798 100644 --- a/web/app/account/(commonLayout)/layout.tsx +++ b/web/app/account/(commonLayout)/layout.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { ConsoleContextProviders, ConsoleRuntimeProviders } from '@/app/(commonLayout)/providers' import HeaderWrapper from '@/app/components/header/header-wrapper' -import MaintenanceNotice from '@/app/components/header/maintenance-notice' import Header from './header' export default async function Layout({ children }: { children: React.ReactNode }) { @@ -9,7 +8,6 @@ export default async function Layout({ children }: { children: React.ReactNode }
-
diff --git a/web/app/components/base/icons/src/vender/line/general/X.json b/web/app/components/base/icons/src/vender/line/general/X.json deleted file mode 100644 index 5a9dbde4cb8..00000000000 --- a/web/app/components/base/icons/src/vender/line/general/X.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "16", - "height": "16", - "viewBox": "0 0 16 16", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "id": "x" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "id": "Icon", - "d": "M11.3334 4.66663L4.66675 11.3333M4.66675 4.66663L11.3334 11.3333", - "stroke": "currentColor", - "stroke-width": "1.5", - "stroke-linecap": "round", - "stroke-linejoin": "round" - }, - "children": [] - } - ] - } - ] - }, - "name": "X" -} diff --git a/web/app/components/base/icons/src/vender/line/general/X.tsx b/web/app/components/base/icons/src/vender/line/general/X.tsx deleted file mode 100644 index 3ad993190b8..00000000000 --- a/web/app/components/base/icons/src/vender/line/general/X.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import type { IconData } from '@/app/components/base/icons/IconBase' -import * as React from 'react' -import IconBase from '@/app/components/base/icons/IconBase' -import data from './X.json' - -const Icon = ({ - ref, - ...props -}: React.SVGProps & { - ref?: React.RefObject> -}) => - -Icon.displayName = 'X' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts index 54d3538dce6..8eadf255b3b 100644 --- a/web/app/components/base/icons/src/vender/line/general/index.ts +++ b/web/app/components/base/icons/src/vender/line/general/index.ts @@ -12,5 +12,3 @@ export { default as Plus02 } from './Plus02' export { default as SearchMenu } from './SearchMenu' export { default as Settings01 } from './Settings01' - -export { default as X } from './X' diff --git a/web/app/components/header/__tests__/maintenance-notice.spec.tsx b/web/app/components/header/__tests__/maintenance-notice.spec.tsx deleted file mode 100644 index 91310c133ec..00000000000 --- a/web/app/components/header/__tests__/maintenance-notice.spec.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import { vi } from 'vite-plus/test' -import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' -import { NOTICE_I18N } from '@/i18n-config/language' -import MaintenanceNotice from '../maintenance-notice' - -const mockEnv = vi.hoisted(() => ({ - NEXT_PUBLIC_MAINTENANCE_NOTICE: 'true', -})) - -vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({ - X: (props: React.SVGProps) => , -})) - -vi.mock('@/env', () => ({ - env: mockEnv, -})) - -vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ - useLanguage: vi.fn(), -})) - -vi.mock('@/i18n-config/language', async (importOriginal) => { - const actual = (await importOriginal()) as Record - return { - ...actual, - NOTICE_I18N: { - title: { - en_US: 'Notice Title', - zh_Hans: '提示标题', - }, - desc: { - en_US: 'Notice Description', - zh_Hans: '提示描述', - }, - href: '#', - }, - } -}) - -describe('MaintenanceNotice', () => { - const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null) - const setNoticeHref = (href: string) => { - NOTICE_I18N.href = href - } - - beforeEach(() => { - vi.clearAllMocks() - localStorage.clear() - mockEnv.NEXT_PUBLIC_MAINTENANCE_NOTICE = 'true' - vi.mocked(useLanguage).mockReturnValue('en_US') - setNoticeHref('#') - }) - - afterAll(() => { - windowOpenSpy.mockRestore() - }) - - describe('Rendering', () => { - it('should render localized content correctly (English)', () => { - render() - expect(screen.getByText('Notice Title')).toBeInTheDocument() - expect(screen.getByText('Notice Description')).toBeInTheDocument() - }) - - it('should render localized content correctly (Chinese)', () => { - vi.mocked(useLanguage).mockReturnValue('zh_Hans') - render() - expect(screen.getByText('提示标题')).toBeInTheDocument() - expect(screen.getByText('提示描述')).toBeInTheDocument() - }) - - it('should not render when hidden in localStorage', () => { - localStorage.setItem('hide-maintenance-notice', '1') - const { container } = render() - expect(container.firstChild).toBeNull() - }) - - it('should not render when the notice env flag is disabled', () => { - mockEnv.NEXT_PUBLIC_MAINTENANCE_NOTICE = '' - - const { container } = render() - - expect(container.firstChild).toBeNull() - }) - }) - - describe('User Interactions', () => { - it('should close the notice when X is clicked', () => { - render() - expect(screen.getByText('Notice Title')).toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' })) - - expect(screen.queryByText('Notice Title')).not.toBeInTheDocument() - expect(localStorage.getItem('hide-maintenance-notice')).toBe('1') - }) - - it('should jump to notice when description is clicked and href is valid', () => { - setNoticeHref('https://dify.ai/notice') - render() - - const desc = screen.getByRole('button', { name: 'Notice Description' }) - fireEvent.click(desc) - - expect(windowOpenSpy).toHaveBeenCalledWith('https://dify.ai/notice', '_blank') - }) - - it('should not jump when href is #', () => { - setNoticeHref('#') - render() - - const desc = screen.getByText('Notice Description') - fireEvent.click(desc) - - expect(windowOpenSpy).not.toHaveBeenCalled() - }) - - it('should not jump when href is empty', () => { - setNoticeHref('') - render() - - const desc = screen.getByText('Notice Description') - fireEvent.click(desc) - - expect(windowOpenSpy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx deleted file mode 100644 index 6042e267765..00000000000 --- a/web/app/components/header/maintenance-notice.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { X } from '@/app/components/base/icons/src/vender/line/general' -import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' -import { env } from '@/env' -import { NOTICE_I18N } from '@/i18n-config/language' -import { useHideMaintenanceNotice } from './storage' - -function MaintenanceNotice() { - if (!env.NEXT_PUBLIC_MAINTENANCE_NOTICE) return null - - return -} - -function MaintenanceNoticeContent() { - const { t } = useTranslation() - const locale = useLanguage() - - const [hiddenNoticeValue, setHiddenNoticeValue] = useHideMaintenanceNotice() - const hiddenNotice = hiddenNoticeValue === '1' - const [closedInSession, setClosedInSession] = useState(false) - const showNotice = !hiddenNotice && !closedInSession - const handleJumpNotice = () => { - window.open(NOTICE_I18N.href, '_blank') - } - - const handleCloseNotice = () => { - setHiddenNoticeValue('1') - setClosedInSession(true) - } - - const titleByLocale: { [key: string]: string } = NOTICE_I18N.title - const descByLocale: { [key: string]: string } = NOTICE_I18N.desc - - if (!showNotice) return null - - return ( -
-
- {titleByLocale[locale]} -
- {NOTICE_I18N.href && NOTICE_I18N.href !== '#' ? ( - - ) : ( -
{descByLocale[locale]}
- )} - -
- ) -} - -export default MaintenanceNotice diff --git a/web/app/components/header/storage.ts b/web/app/components/header/storage.ts deleted file mode 100644 index 8c1fd40058d..00000000000 --- a/web/app/components/header/storage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createLocalStorageState } from 'foxact/create-local-storage-state' - -const [useHideMaintenanceNotice, _useHideMaintenanceNoticeValue, _useSetHideMaintenanceNotice] = - createLocalStorageState('hide-maintenance-notice', '0', { raw: true }) - -export { useHideMaintenanceNotice } diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx index 87a9c2ae10b..5e0ee0deace 100644 --- a/web/context/__tests__/console-bootstrap.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -205,10 +205,6 @@ vi.mock('@/app/components/base/zendesk/runtime', () => ({ }, })) -vi.mock('@/app/components/header/maintenance-notice', () => ({ - default: () => null, -})) - function ConsoleBootstrapProbe() { const currentWorkspace = useAtomValue(currentWorkspaceAtom) const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) diff --git a/web/env.ts b/web/env.ts index 6d755eb70ae..da3c21809ed 100644 --- a/web/env.ts +++ b/web/env.ts @@ -101,7 +101,6 @@ const clientSchema = { * Maximum loop count in the workflow */ NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: coercedNumber.default(100), - NEXT_PUBLIC_MAINTENANCE_NOTICE: z.string().optional(), /** * Additional literal characters allowed in Markdown form field names. */ @@ -267,9 +266,6 @@ export const env = createEnv({ NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: isServer ? process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT : getRuntimeEnvFromBody('loopNodeMaxCount'), - NEXT_PUBLIC_MAINTENANCE_NOTICE: isServer - ? process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE - : getRuntimeEnvFromBody('maintenanceNotice'), NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS: isServer ? process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS : getRuntimeEnvFromBody('markdownFormFieldNameExtraChars'), diff --git a/web/i18n-config/language.ts b/web/i18n-config/language.ts index e6d39916712..4d183acb45e 100644 --- a/web/i18n-config/language.ts +++ b/web/i18n-config/language.ts @@ -71,69 +71,3 @@ export const getDocLanguage = (locale: string): DocLanguage => { export const getAccessControlTemplateLanguage = (locale: string): AccessControlTemplateLanguage => { return ACCESS_CONTROL_TEMPLATE_LANGUAGE[locale] || 'en' } - -export const NOTICE_I18N = { - title: { - en_US: 'Important Notice', - zh_Hans: '重要公告', - zh_Hant: '重要公告', - pt_BR: 'Aviso Importante', - es_ES: 'Aviso Importante', - fr_FR: 'Avis important', - de_DE: 'Wichtiger Hinweis', - ja_JP: '重要なお知らせ', - ko_KR: '중요 공지', - ru_RU: 'Важное Уведомление', - it_IT: 'Avviso Importante', - th_TH: 'ประกาศสำคัญ', - id_ID: 'Pengumuman Penting', - uk_UA: 'Важливе повідомлення', - vi_VN: 'Thông báo quan trọng', - ro_RO: 'Anunț Important', - pl_PL: 'Ważne ogłoszenie', - hi_IN: 'महत्वपूर्ण सूचना', - tr_TR: 'Önemli Duyuru', - fa_IR: 'هشدار مهم', - sl_SI: 'Pomembno obvestilo', - ar_TN: 'إشعار مهم', - }, - desc: { - en_US: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - zh_Hans: - '为了有效提升数据检索能力及稳定性,Dify 将于 2023 年 8 月 29 日 03:00 至 08:00 期间进行服务升级,届时 Dify 云端版及应用将无法访问。感谢您的耐心与支持。', - pt_BR: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - es_ES: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - fr_FR: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - de_DE: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - ja_JP: - 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', - ko_KR: - '시스템이 업그레이드를 위해 UTC 시간대로 8 월 28 일 19:00 ~ 24:00 에 사용 불가될 예정입니다. 질문이 있으시면 지원 팀에 연락주세요 (support@dify.ai). 최선을 다해 답변해드리겠습니다.', - pl_PL: - 'Nasz system będzie niedostępny od 19:00 do 24:00 UTC 28 sierpnia w celu aktualizacji. W przypadku pytań prosimy o kontakt z naszym zespołem wsparcia (support@dify.ai). Doceniamy Twoją cierpliwość.', - uk_UA: - 'Наша система буде недоступна з 19:00 до 24:00 UTC 28 серпня для оновлення. Якщо у вас виникнуть запитання, будь ласка, зв’яжіться з нашою службою підтримки (support@dify.ai). Дякуємо за терпіння.', - ru_RU: - 'Наша система будет недоступна с 19:00 до 24:00 UTC 28 августа для обновления. По вопросам, пожалуйста, обращайтесь в нашу службу поддержки (support@dify.ai). Спасибо за ваше терпение', - vi_VN: - 'Hệ thống của chúng tôi sẽ ngừng hoạt động từ 19:00 đến 24:00 UTC vào ngày 28 tháng 8 để nâng cấp. Nếu có thắc mắc, vui lòng liên hệ với nhóm hỗ trợ của chúng tôi (support@dify.ai). Chúng tôi đánh giá cao sự kiên nhẫn của bạn.', - id_ID: - 'Sistem kami tidak akan tersedia dari 19:00 hingga 24:00 UTC pada 28 Agustus untuk pemutakhiran. Untuk pertanyaan, silakan hubungi tim dukungan kami (support@dify.ai). Kami menghargai kesabaran Anda.', - tr_TR: - "Sistemimiz, 28 Ağustos'ta 19:00 ile 24:00 UTC saatleri arasında güncelleme nedeniyle kullanılamayacaktır. Sorularınız için lütfen destek ekibimizle iletişime geçin (support@dify.ai). Sabrınız için teşekkür ederiz.", - fa_IR: - 'سیستم ما از ساعت 19:00 تا 24:00 UTC در تاریخ 28 اوت برای ارتقاء در دسترس نخواهد بود. برای سؤالات، لطفاً با تیم پشتیبانی ما (support@dify.ai) تماس بگیرید. ما برای صبر شما ارزش قائلیم.', - sl_SI: - 'Naš sistem ne bo na voljo od 19:00 do 24:00 UTC 28. avgusta zaradi nadgradnje. Za vprašanja se obrnite na našo skupino za podporo (support@dify.ai). Cenimo vašo potrpežljivost.', - th_TH: - 'ระบบของเราจะไม่สามารถใช้งานได้ตั้งแต่เวลา 19:00 ถึง 24:00 UTC ในวันที่ 28 สิงหาคม เพื่อทำการอัปเกรด หากมีคำถามใดๆ กรุณาติดต่อทีมสนับสนุนของเรา (support@dify.ai) เราขอขอบคุณในความอดทนของท่าน', - ar_TN: - 'سيكون نظامنا غير متاح من الساعة 19:00 إلى 24:00 بالتوقيت العالمي المنسق في 28 أغسطس لإجراء ترقية. للأسئلة، يرجى الاتصال بفريق الدعم لدينا (support@dify.ai). نحن نقدر صبرك.', - }, - href: '#', -} From 7069c7127b77cc166a7f625c9f587d55c957b411 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:42:28 +0000 Subject: [PATCH 03/21] chore(deps): bump soupsieve from 2.8.4 to 2.9 in /api (#42455) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/uv.lock b/api/uv.lock index 5723a6825ed..9bf9ca07cff 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -6304,11 +6304,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.4" +version = "2.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/f1/93422647dd7e461f23d254e6b2bfa687a85b53aeb4903fcdbb74474d4584/soupsieve-2.9.tar.gz", hash = "sha256:acee8417325c5653e1377dc31eccad59eb82cbc65942afe6174c53b3aaad63fc", size = 122122, upload-time = "2026-07-19T01:35:18.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d6/3185ab5ad1280319b31986898f3206dd7227cd75e293d4dba2a5e6bf27a0/soupsieve-2.9-py3-none-any.whl", hash = "sha256:a2b2c76d67df2382d245409fd71e321a571717e58463efa32ace87dcadac2c12", size = 37387, upload-time = "2026-07-19T01:35:17.106Z" }, ] [[package]] From 6024141c2684aedca2008cc77bc1d0caf3644f7d Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:52:26 +0000 Subject: [PATCH 04/21] fix(i18n): normalize locale inputs before language selection (#42466) --- web/i18n-config/README.md | 11 ++++- .../__tests__/client-locale.spec.ts | 44 +++++++++++++++++++ .../__tests__/load-resource.spec.ts | 16 +++++++ web/i18n-config/__tests__/server.spec.ts | 38 ++++++++++++++++ web/i18n-config/client.ts | 5 ++- web/i18n-config/index.ts | 13 +++--- web/i18n-config/language.ts | 5 +-- web/i18n-config/load-resource.ts | 22 ++-------- web/i18n-config/locale.ts | 27 ++++++++++++ web/i18n-config/server.ts | 38 ++++++---------- web/i18n-config/settings.ts | 3 +- 11 files changed, 164 insertions(+), 58 deletions(-) create mode 100644 web/i18n-config/__tests__/client-locale.spec.ts create mode 100644 web/i18n-config/__tests__/load-resource.spec.ts create mode 100644 web/i18n-config/locale.ts diff --git a/web/i18n-config/README.md b/web/i18n-config/README.md index 1777800acc6..385a52523a5 100644 --- a/web/i18n-config/README.md +++ b/web/i18n-config/README.md @@ -5,7 +5,8 @@ English JSON files under `web/i18n/en-US/` are the source locale. Other locale d ## Owners - `languages.ts` is the source of truth for supported Web locales. -- `language.ts` owns locale normalization and product-specific locale mappings. +- `locale.ts` owns canonical UI locale tags, the default locale, and input normalization. +- `language.ts` owns product-specific locale mappings. - `resources.ts` owns the typed namespace registry. File names use kebab case while namespaces use camel case, for example `app-debug.json` and `appDebug`. - `locale-resources/.ts` owns lazy loading for one locale. - `settings.ts` owns shared i18next options. @@ -21,7 +22,13 @@ Do not copy the language registry into documentation. Read the current source fi 4. Keep the backend language and timezone registry in `api/constants/languages.py` aligned when the locale is accepted by backend APIs. 5. Run the complete i18n check before submitting the change. -`LanguagesSupported` is populated from the supported entries in `languages.ts`. `language.ts` also owns the accepted locale spellings and the `I18nText` contract; keep them aligned when adding a locale. +`supportedLocales` is populated from the supported entries in `languages.ts`. `language.ts` exposes the existing `LanguagesSupported` and `I18nText` contracts. + +## Locale selection + +A nonempty locale cookie takes priority over `Accept-Language`. An unusable cookie falls back to English without consulting the header. Header negotiation preserves valid preferences and their quality order while ignoring malformed tags and wildcards. + +`locale.ts` canonicalizes language tags and accepts the existing `en_US`, `zh_Hans`, and `ja_JP` spellings at input boundaries. Client language changes, saved preferences, and resource paths use supported UI tags or the English default. Share-app language overrides do not write the console locale cookie. Plugin and model metadata spellings remain separate product contracts. ## Add or change copy diff --git a/web/i18n-config/__tests__/client-locale.spec.ts b/web/i18n-config/__tests__/client-locale.spec.ts new file mode 100644 index 00000000000..1f78d7eabd1 --- /dev/null +++ b/web/i18n-config/__tests__/client-locale.spec.ts @@ -0,0 +1,44 @@ +import Cookies from 'js-cookie' +import { setLocaleOnClient } from '..' +import { changeLanguage } from '../client' + +const mocks = vi.hoisted(() => ({ changeLanguage: vi.fn() })) + +vi.mock('js-cookie', () => ({ default: { set: vi.fn() } })) +vi.mock('react-i18next', () => ({ getI18n: () => ({ changeLanguage: mocks.changeLanguage }) })) + +describe('client locale inputs', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.changeLanguage.mockResolvedValue(undefined) + }) + + it.each([ + ['zh_Hans', 'zh-Hans'], + ['ja_JP', 'ja-JP'], + ['en_US', 'en-US'], + ['ZH-hans', 'zh-Hans'], + ['zh-Hant', 'zh-Hant'], + ['en--US', 'en-US'], + ['unsupported', 'en-US'], + ])('changes and persists %s as %s', async (input, expected) => { + await setLocaleOnClient(input, false) + + expect(Cookies.set).toHaveBeenCalledWith('locale', expected, { expires: 365 }) + expect(mocks.changeLanguage).toHaveBeenCalledWith(expected) + }) + + it('normalizes a share-app override without persisting console preferences', async () => { + await changeLanguage('zh_Hans') + + expect(mocks.changeLanguage).toHaveBeenCalledWith('zh-Hans') + expect(Cookies.set).not.toHaveBeenCalled() + }) + + it('preserves the current language when the app has no override', async () => { + await changeLanguage(undefined) + + expect(mocks.changeLanguage).not.toHaveBeenCalled() + expect(Cookies.set).not.toHaveBeenCalled() + }) +}) diff --git a/web/i18n-config/__tests__/load-resource.spec.ts b/web/i18n-config/__tests__/load-resource.spec.ts new file mode 100644 index 00000000000..ca2ceb67843 --- /dev/null +++ b/web/i18n-config/__tests__/load-resource.spec.ts @@ -0,0 +1,16 @@ +import { loadI18nResource } from '../load-resource' + +describe('locale resource loading', () => { + it.each([ + ['en_US', 'Save'], + ['zh_Hans', '保存'], + ['ja_JP', '保存'], + ['ZH-hans', '保存'], + ['en--US', 'Save'], + ['unsupported', 'Save'], + ])('loads usable translations for %s', async (locale, expected) => { + const resource = await loadI18nResource(locale, 'common') + + expect(resource.default).toMatchObject({ 'operation.save': expected }) + }) +}) diff --git a/web/i18n-config/__tests__/server.spec.ts b/web/i18n-config/__tests__/server.spec.ts index 8f19d2b4a94..df1ccce8525 100644 --- a/web/i18n-config/__tests__/server.spec.ts +++ b/web/i18n-config/__tests__/server.spec.ts @@ -117,4 +117,42 @@ describe('server locale', () => { await expect(getLocaleOnServer()).resolves.toBe('en-US') }, ) + + it.each([ + ['en_US', 'en-US'], + ['zh_Hans', 'zh-Hans'], + ['ja_JP', 'ja-JP'], + ['ZH-hans', 'zh-Hans'], + ])('normalizes the saved locale %s to %s', async (saved, expected) => { + mocks.getCookie.mockReturnValue({ value: saved }) + const { getLocaleOnServer } = await import('../server') + + await expect(getLocaleOnServer()).resolves.toBe(expected) + expect(mocks.headers).not.toHaveBeenCalled() + }) + + it.each(['en--US', 'en_US_invalid', '*', 'zz-ZZ'])( + 'falls back for an unusable cookie without consulting the browser: %s', + async (saved) => { + mocks.getCookie.mockReturnValue({ value: saved }) + mocks.headers.mockResolvedValue(new Headers({ 'accept-language': 'ja-JP' })) + const { getLocaleOnServer } = await import('../server') + + await expect(getLocaleOnServer()).resolves.toBe('en-US') + expect(mocks.headers).not.toHaveBeenCalled() + }, + ) + + it.each([ + ['en--US,ja-JP;q=0.9,en-US;q=0.8', 'ja-JP'], + ['ja-JP,en-US;q=0.8,*;q=0.5', 'ja-JP'], + ['en-US;q=0.5,zh-CN;q=0.9', 'zh-Hans'], + ['*,en--US;q=0.5', 'en-US'], + ['ja-JP;q=0,en-US;q=0.5', 'en-US'], + ])('negotiates valid header preferences from %s', async (header, expected) => { + mocks.headers.mockResolvedValue(new Headers({ 'accept-language': header })) + const { getLocaleOnServer } = await import('../server') + + await expect(getLocaleOnServer()).resolves.toBe(expected) + }) }) diff --git a/web/i18n-config/client.ts b/web/i18n-config/client.ts index a1b949d4997..dbc371c19dd 100644 --- a/web/i18n-config/client.ts +++ b/web/i18n-config/client.ts @@ -6,6 +6,7 @@ import { createInstance } from 'i18next' import resourcesToBackend from 'i18next-resources-to-backend' import { getI18n, initReactI18next } from 'react-i18next' import { loadI18nResource } from './load-resource' +import { normalizeLocale } from './locale' import { getInitOptions } from './settings' export function createI18nextInstance(lng: Locale, resources: Resource) { @@ -25,8 +26,8 @@ export function createI18nextInstance(lng: Locale, resources: Resource) { return instance } -export const changeLanguage = async (lng?: Locale) => { +export const changeLanguage = async (lng?: string) => { if (!lng) return const i18n = getI18n() - await i18n.changeLanguage(lng) + await i18n.changeLanguage(normalizeLocale(lng)) } diff --git a/web/i18n-config/index.ts b/web/i18n-config/index.ts index 2268cf2d540..3e67e9484d7 100644 --- a/web/i18n-config/index.ts +++ b/web/i18n-config/index.ts @@ -2,18 +2,19 @@ import type { Locale } from '@/i18n-config/language' import Cookies from 'js-cookie' import { LOCALE_COOKIE_NAME } from '@/config' import { changeLanguage } from '@/i18n-config/client' -import { LanguagesSupported } from '@/i18n-config/language' +import { defaultLocale, normalizeLocale, supportedLocales } from './locale' export const i18n = { - defaultLocale: 'en-US', - locales: LanguagesSupported, + defaultLocale, + locales: supportedLocales, } as const export type { Locale } -export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => { - Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 }) - await changeLanguage(locale) +export const setLocaleOnClient = async (locale: string, reloadPage = true) => { + const normalized = normalizeLocale(locale) + Cookies.set(LOCALE_COOKIE_NAME, normalized, { expires: 365 }) + await changeLanguage(normalized) if (reloadPage) location.reload() } diff --git a/web/i18n-config/language.ts b/web/i18n-config/language.ts index 4d183acb45e..dec4f378939 100644 --- a/web/i18n-config/language.ts +++ b/web/i18n-config/language.ts @@ -1,5 +1,6 @@ import type { DocLanguage } from '@/types/doc-paths' import data from './languages' +import { supportedLocales } from './locale' export type I18nText = Record<(typeof LanguagesSupported)[number], string> @@ -8,9 +9,7 @@ export const languages = data.languages // for compatibility export type Locale = 'ja_JP' | 'zh_Hans' | 'en_US' | (typeof languages)[number]['value'] -export const LanguagesSupported: Locale[] = languages - .filter((item) => item.supported) - .map((item) => item.value) +export const LanguagesSupported: Locale[] = supportedLocales export const getLanguage = (locale: Locale): Locale => { // Plugin metadata supports only en_US, zh_Hans, ja_JP, and pt_BR; otherwise use en_US. diff --git a/web/i18n-config/load-resource.ts b/web/i18n-config/load-resource.ts index 214a351a31f..b767f8093b9 100644 --- a/web/i18n-config/load-resource.ts +++ b/web/i18n-config/load-resource.ts @@ -1,35 +1,19 @@ import type { ResourceKey } from 'i18next' -import type { Locale } from './language' import type { Namespace, NamespaceInFileName } from './resources' import { kebabCase } from 'es-toolkit/string' -import { LanguagesSupported } from './language' +import { normalizeLocale } from './locale' type LocaleResourceModule = { loadResource: (fileNamespace: string) => Promise<{ default: ResourceKey }> } -const legacyLocaleMap: Partial> = { - en_US: 'en-US', - ja_JP: 'ja-JP', - zh_Hans: 'zh-Hans', -} - -const defaultLocale = 'en-US' satisfies Locale - -const normalizeLocale = (locale: Locale): Locale => { - const normalized = legacyLocaleMap[locale] ?? locale - if (LanguagesSupported.includes(normalized)) return normalized - - return defaultLocale -} - -const loadLocaleResources = (locale: Locale): Promise => { +const loadLocaleResources = (locale: string): Promise => { const normalized = normalizeLocale(locale) return import(`./locale-resources/${normalized}.ts`) } export const loadI18nResource = async ( - locale: Locale, + locale: string, namespace: Namespace | NamespaceInFileName, ) => { const { loadResource } = await loadLocaleResources(locale) diff --git a/web/i18n-config/locale.ts b/web/i18n-config/locale.ts new file mode 100644 index 00000000000..0b16699b753 --- /dev/null +++ b/web/i18n-config/locale.ts @@ -0,0 +1,27 @@ +import data from './languages' + +export type SupportedLocale = (typeof data.languages)[number]['value'] + +export const defaultLocale = 'en-US' satisfies SupportedLocale +export const supportedLocales = data.languages + .filter((language) => language.supported) + .map((language) => language.value) + +const legacyLocaleMap = new Map([ + ['en_US', 'en-US'], + ['ja_JP', 'ja-JP'], + ['zh_Hans', 'zh-Hans'], +]) + +export const canonicalizeLanguageTag = (language: string): string | undefined => { + try { + return Intl.getCanonicalLocales(legacyLocaleMap.get(language) ?? language)[0] + } catch { + return undefined + } +} + +export const normalizeLocale = (language: string): SupportedLocale => { + const canonical = canonicalizeLanguageTag(language) + return supportedLocales.find((locale) => locale === canonical) ?? defaultLocale +} diff --git a/web/i18n-config/server.ts b/web/i18n-config/server.ts index fe59a232774..aa6fab82c08 100644 --- a/web/i18n-config/server.ts +++ b/web/i18n-config/server.ts @@ -8,9 +8,10 @@ import resourcesToBackend from 'i18next-resources-to-backend' import Negotiator from 'negotiator' import { cache } from 'react' import { initReactI18next } from 'react-i18next/initReactI18next' +import { LOCALE_COOKIE_NAME } from '@/config' import { cookies, headers } from '@/next/headers' -import { i18n } from '.' import { loadI18nResource } from './load-resource' +import { canonicalizeLanguageTag, defaultLocale, supportedLocales } from './locale' import { namespacesInFileName } from './resources' import { getInitOptions } from './settings' @@ -42,31 +43,18 @@ export async function getTranslation(lng: Locale, ns?: T) { } export const getLocaleOnServer = cache(async (): Promise => { - const locales: string[] = i18n.locales + const localeCookie = (await cookies()).get(LOCALE_COOKIE_NAME) + const requestedLanguages = localeCookie?.value + ? [localeCookie.value] + : new Negotiator({ + headers: { 'accept-language': (await headers()).get('accept-language') ?? '' }, + }).languages() + const languages = requestedLanguages.flatMap((language) => { + const canonical = canonicalizeLanguageTag(language) + return canonical ? [canonical] : [] + }) - let languages: string[] | undefined - // get locale from cookie - const localeCookie = (await cookies()).get('locale') - languages = localeCookie?.value ? [localeCookie.value] : [] - - if (!languages.length) { - // Negotiator expects plain object so we need to transform headers - const negotiatorHeaders: Record = {} - ;(await headers()).forEach((value, key) => (negotiatorHeaders[key] = value)) - // Use negotiator and intl-localematcher to get best locale - languages = new Negotiator({ headers: negotiatorHeaders }).languages() - } - - // Validate languages - if ( - !Array.isArray(languages) || - languages.length === 0 || - !languages.every((lang) => typeof lang === 'string' && /^[\w-]+$/.test(lang)) - ) - languages = [i18n.defaultLocale] - - // match locale - return match(languages, locales, i18n.defaultLocale) as Locale + return match(languages, supportedLocales, defaultLocale) as Locale }) export const getResources = cache(async (lng: Locale): Promise => { diff --git a/web/i18n-config/settings.ts b/web/i18n-config/settings.ts index 04761052ceb..a6b7400ab6f 100644 --- a/web/i18n-config/settings.ts +++ b/web/i18n-config/settings.ts @@ -1,11 +1,12 @@ import type { InitOptions } from 'i18next' +import { defaultLocale } from './locale' import { defaultNS, namespaces } from './resources' export function getInitOptions(): InitOptions { return { // We do not have en for fallback load: 'currentOnly', - fallbackLng: 'en-US', + fallbackLng: defaultLocale, partialBundledLanguages: true, defaultNS, enableSelector: 'optimize', From 6d88df5d7c68518bd4d3936e843db6a49c5b9879 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:58:11 +0000 Subject: [PATCH 05/21] docs: clarify Dify Enterprise offering (#42474) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0f1dcb5654..410ade141eb 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ All of Dify's offerings come with corresponding APIs, so you could effortlessly Use our [documentation](https://docs.dify.ai) for further references and more in-depth instructions. - **[Dify Enterprise](https://dify.ai/dify-enterprise)
** - We provide additional enterprise-centric features. Fill out our [Enterprise form](https://share-na2.hsforms.com/14-09ff5HS92Sh4m3f4yrcw40s9fk) to discuss your enterprise needs.
+ For organizations requiring self-hosting, SSO, RBAC Security and Enterprise Support SLAs, please fill out [this form](https://share-na2.hsforms.com/14-09ff5HS92Sh4m3f4yrcw40s9fk) to speak to a solution representative.
## Staying ahead From 228541c15d047c70e1d202495515673cb5b4792b Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Fri, 18 Sep 2026 03:04:57 +0000 Subject: [PATCH 06/21] refactor(web): replace emoji-mart with self-hosted Frimousse picker (#42469) --- pnpm-lock.yaml | 46 +- pnpm-workspace.yaml | 3 +- vite.config.ts | 2 + .../card-item/__tests__/index.spec.tsx | 2 +- .../external-data-tool-modal.spec.tsx | 7 +- .../create-app-modal/__tests__/index.spec.tsx | 13 +- .../duplicate-modal/__tests__/index.spec.tsx | 4 +- .../app/overview/apikey-info-panel/index.tsx | 2 +- .../switch-app-modal/__tests__/index.spec.tsx | 13 +- .../apps/__tests__/app-card.spec.tsx | 4 +- ...t-from-marketplace-template-modal.spec.tsx | 2 +- web/app/components/base/answer-icon/index.tsx | 9 +- .../app-icon-picker/__tests__/index.spec.tsx | 13 +- web/app/components/base/app-icon/index.tsx | 20 +- .../components/base/emoji-picker/Inner.tsx | 173 +- .../__tests__/Inner.browser.spec.tsx | 83 + .../emoji-picker/__tests__/Inner.spec.tsx | 214 +- .../emoji-picker/__tests__/index.spec.tsx | 30 +- .../__tests__/edit-pipeline-info.spec.tsx | 21 +- .../rename-modal/__tests__/index.spec.tsx | 21 +- .../create-app-modal/__tests__/index.spec.tsx | 54 +- ...blish-as-knowledge-pipeline-modal.spec.tsx | 13 +- .../tools/mcp/__tests__/modal.spec.tsx | 13 +- .../__tests__/navigation.spec.tsx | 6 +- web/global.d.ts | 1 - web/hooks/use-app-favicon.spec.ts | 6 +- web/hooks/use-app-favicon.ts | 6 +- web/package.json | 3 +- web/public/emoji/EMOJI-MART-LICENSE | 21 + web/public/emoji/README.md | 16 + web/public/emoji/emojibase-17.0.0/LICENSE | 21 + .../emoji/emojibase-17.0.0/en/data.json | 1 + .../emoji/emojibase-17.0.0/en/messages.json | 1 + web/test/emoji-picker.ts | 22 + web/types/jsx.d.ts | 12 - web/utils/__tests__/emoji.spec.ts | 17 + web/utils/emoji-legacy.json | 1933 +++++++++++++++++ web/utils/emoji.ts | 16 +- web/vite.config.ts | 4 - 39 files changed, 2376 insertions(+), 472 deletions(-) create mode 100644 web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx create mode 100644 web/public/emoji/EMOJI-MART-LICENSE create mode 100644 web/public/emoji/README.md create mode 100644 web/public/emoji/emojibase-17.0.0/LICENSE create mode 100644 web/public/emoji/emojibase-17.0.0/en/data.json create mode 100644 web/public/emoji/emojibase-17.0.0/en/messages.json create mode 100644 web/test/emoji-picker.ts delete mode 100644 web/types/jsx.d.ts create mode 100644 web/utils/__tests__/emoji.spec.ts create mode 100644 web/utils/emoji-legacy.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d762e4b8a9..9653c66478b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,9 +185,6 @@ catalogs: '@egoist/tailwindcss-icons': specifier: 1.9.2 version: 1.9.2 - '@emoji-mart/data': - specifier: 1.2.1 - version: 1.2.1 '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.8.1 version: 4.8.1 @@ -464,9 +461,6 @@ catalogs: embla-carousel-react: specifier: 8.6.0 version: 8.6.0 - emoji-mart: - specifier: 5.6.0 - version: 5.6.0 es-toolkit: specifier: 1.52.0 version: 1.52.0 @@ -536,6 +530,9 @@ catalogs: foxact: specifier: 0.3.11 version: 0.3.11 + frimousse: + specifier: 0.3.0 + version: 0.3.0 fuse.js: specifier: 7.5.0 version: 7.5.0 @@ -1360,9 +1357,6 @@ importers: '@base-ui/react': specifier: 'catalog:' version: 1.8.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@emoji-mart/data': - specifier: 'catalog:' - version: 1.2.1 '@floating-ui/react': specifier: 'catalog:' version: 0.27.20(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -1486,9 +1480,6 @@ importers: embla-carousel-react: specifier: 'catalog:' version: 8.6.0(react@19.3.0) - emoji-mart: - specifier: 'catalog:' - version: 5.6.0 es-toolkit: specifier: 'catalog:' version: 1.52.0 @@ -1498,6 +1489,9 @@ importers: foxact: specifier: 'catalog:' version: 0.3.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + frimousse: + specifier: 'catalog:' + version: 0.3.0(@typescript/typescript6@6.0.2)(react@19.3.0) fuse.js: specifier: 'catalog:' version: 7.5.0 @@ -2177,9 +2171,6 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emoji-mart/data@1.2.1': - resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - '@es-joy/jsdoccomment@0.92.0': resolution: {integrity: sha512-WivZqV5jPTYDQJgIMp0hTsyQESKYNY6yCJr4b0l84tYtQntkAMkc6zARh2vlRQyeg4aYHPUkCUDpvr2IWAiGpQ==} engines: {node: ^22.22.2 || >=24.15.0} @@ -6346,9 +6337,6 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - emoji-mart@5.6.0: - resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} - emoji-regex-xs@2.0.1: resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} engines: {node: '>=10.0.0'} @@ -6810,6 +6798,15 @@ packages: react-dom: optional: true + frimousse@0.3.0: + resolution: {integrity: sha512-kO6LMoKY/cLAYEhXXtqLRaLIE6L/DagpFPrUZaLv3LsUa1/8Iza3HhwZcgN8eZ+weXnhv69eoclNUPohcCa/IQ==} + peerDependencies: + react: ^18 || ^19 + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -10193,8 +10190,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emoji-mart/data@1.2.1': {} - '@es-joy/jsdoccomment@0.92.0': dependencies: '@types/estree': 1.0.9 @@ -13962,8 +13957,6 @@ snapshots: embla-carousel@8.6.0: {} - emoji-mart@5.6.0: {} - emoji-regex-xs@2.0.1: {} empathic@2.0.1: {} @@ -14584,6 +14577,12 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) + frimousse@0.3.0(@typescript/typescript6@6.0.2)(react@19.3.0): + dependencies: + react: 19.3.0 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' + fs-constants@1.0.0: optional: true @@ -18102,7 +18101,6 @@ time: '@chromatic-com/storybook@5.3.1': '2026-09-02T13:43:27.595Z' '@cucumber/cucumber@13.2.1': '2026-08-04T07:20:28.681Z' '@egoist/tailwindcss-icons@1.9.2': '2026-01-31T10:48:44.594Z' - '@emoji-mart/data@1.2.1': '2024-04-25T15:36:14.556Z' '@eslint-community/eslint-plugin-eslint-comments@4.8.1': '2026-09-12T22:48:47.148Z' '@eslint-react/eslint-plugin@5.19.1': '2026-09-14T17:56:33.726Z' '@eslint/markdown@8.0.3': '2026-07-01T13:28:44.339Z' @@ -18196,7 +18194,6 @@ time: embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z' embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z' embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z' - emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z' es-toolkit@1.52.0: '2026-08-28T07:22:17.687Z' eslint-config-flat-gitignore@2.4.0: '2026-08-27T07:24:43.063Z' eslint-markdown@0.17.0: '2026-09-16T06:20:36.564Z' @@ -18220,6 +18217,7 @@ time: eventsource-parser@4.1.1: '2026-09-15T18:30:29.195Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' foxact@0.3.11: '2026-09-05T12:25:54.102Z' + frimousse@0.3.0: '2025-07-15T10:44:32.958Z' fuse.js@7.5.0: '2026-07-13T17:23:36.705Z' happy-dom@20.14.5: '2026-09-12T00:07:47.688Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6160a92379..04ff09c4ba0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -68,7 +68,6 @@ catalog: '@chromatic-com/storybook': 5.3.1 '@cucumber/cucumber': 13.2.1 '@egoist/tailwindcss-icons': 1.9.2 - '@emoji-mart/data': 1.2.1 '@eslint-community/eslint-plugin-eslint-comments': 4.8.1 '@eslint-react/eslint-plugin': 5.19.1 '@eslint/markdown': 8.0.3 @@ -162,7 +161,6 @@ catalog: embla-carousel-autoplay: 8.6.0 embla-carousel-fade: 8.6.0 embla-carousel-react: 8.6.0 - emoji-mart: 5.6.0 es-toolkit: 1.52.0 eslint: 10.10.0 eslint-config-flat-gitignore: 2.4.0 @@ -186,6 +184,7 @@ catalog: eventsource-parser: 4.1.1 fast-deep-equal: 3.1.3 foxact: 0.3.11 + frimousse: 0.3.0 fuse.js: 7.5.0 happy-dom: 20.14.5 hast-util-to-jsx-runtime: 2.3.6 diff --git a/vite.config.ts b/vite.config.ts index 8d47f7528bb..dee3d7eccca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,6 +40,8 @@ const generatedIgnores = [ 'web/public/embed.min.js', 'web/public/pdf.worker.min.mjs', 'web/public/vs/**', + // Vendored Emojibase JSON is served verbatim. + 'web/public/emoji/emojibase-*/**', ] const formatterUnstableInputs = ['web/app/components/develop/template/*.mdx'] diff --git a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx index acb21be3f6a..48424a0322c 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx @@ -218,7 +218,7 @@ describe('dataset-config/card-item', () => { const iconElement = nameElement.parentElement?.firstElementChild as HTMLElement expect(iconElement)!.toHaveStyle({ background: '#FFF4ED' }) - expect(iconElement.querySelector('em-emoji'))!.toHaveAttribute('id', '📙') + expect(iconElement).toHaveTextContent('📙') }) it('should apply mask overlay on mobile when drawer is open', async () => { diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx index 4b5ca839428..5169868b53a 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import ExternalDataToolModal from '../external-data-tool-modal' const mockToastError = vi.fn() @@ -131,9 +132,9 @@ describe('ExternalDataToolModal', () => { fireEvent.click(screen.getByText('pick-extension')) fireEvent.click(screen.getByText('open-emoji-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -228,3 +229,5 @@ describe('ExternalDataToolModal', () => { expect(mockOnCancel).toHaveBeenCalled() }) }) + +mockEmojiData() diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index 3bbe0d7c8a1..68af8670fe4 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -4,6 +4,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useRouter } from '@/next/navigation' import { renderWithConsoleQuery } from '@/test/console/query-data' +import { mockEmojiData } from '@/test/emoji-picker' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { trackCreateApp } from '@/utils/create-app-tracking' @@ -265,12 +266,12 @@ describe('CreateAppModal', () => { fireEvent.click(screen.getByText('app.types.chatbot')) fireEvent.click(screen.getByText('open-icon-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), { target: { value: 'Keyboard App' }, @@ -309,14 +310,14 @@ describe('CreateAppModal', () => { fireEvent.click(screen.getByText('open-icon-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() ahooksMocks.keyPressHandlers.at(-1)?.() @@ -379,3 +380,5 @@ describe('CreateAppModal', () => { }) }) }) + +mockEmojiData() diff --git a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx index 1f5adc98622..f36ee285ac1 100644 --- a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx @@ -41,7 +41,7 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({
+ ), +} + function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPickerInnerProps) { const { t } = useTranslation() - const { categories } = data as EmojiMartData - const [selectedEmoji, setSelectedEmoji] = useState(emoji || '') + const [selectedEmoji, setSelectedEmoji] = useState(() => (emoji ? resolveEmoji(emoji) : '')) const [selectedBackground, setSelectedBackground] = useState(background || defaultEmojiBackground) const [showStyleColors, setShowStyleColors] = useState(!!emoji) - const [searchedEmojis, setSearchedEmojis] = useState([]) - const [isSearching, setIsSearching] = useState(false) const styleColorsLabelId = React.useId() const handleEmojiSelect = (emoji: string) => { @@ -44,88 +62,43 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick } return ( -
-
- - $['operation.search'], { ns: 'common' })} - placeholder="Search emojis..." - onValueChange={async (value) => { - if (value === '') { - setIsSearching(false) - } else { - setIsSearching(true) - const emojis = await searchEmoji(value) - setSearchedEmojis(emojis) - } - }} - /> - - - -
- - -
- {isSearching && ( - <> -
-

Search

-
- {searchedEmojis.map((emoji: string, index: number) => { - return ( - - ) - })} -
-
- - )} - - {categories.map((category, index: number) => { - return ( -
-

{category.id}

-
- {category.emojis.map((emoji, index: number) => { - return ( - - ) - })} -
-
- ) - })} -
+
+ handleEmojiSelect(emoji)} + > +
+ + $['operation.search'], { ns: 'common' })} + placeholder={t(($) => $['operation.search'], { ns: 'common' })} + className="h-8 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-hover" + /> + + + +
+ + + + {t(($) => $.loading, { ns: 'common' })} + + + {t(($) => $.noData, { ns: 'common' })} + + + +
{/* Color Select */} -
+

Choose Style

@@ -134,7 +107,7 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick type="button" aria-labelledby={styleColorsLabelId} aria-expanded="true" - className="i-heroicons-chevron-down size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary" + className="i-heroicons-chevron-down size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary focus-visible:outline-2 focus-visible:outline-offset-2" onClick={() => setShowStyleColors(!showStyleColors)} /> ) : ( @@ -142,13 +115,13 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick type="button" aria-labelledby={styleColorsLabelId} aria-expanded="false" - className="i-heroicons-chevron-up size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary" + className="i-heroicons-chevron-up size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary focus-visible:outline-2 focus-visible:outline-offset-2" onClick={() => setShowStyleColors(!showStyleColors)} /> )}
{showStyleColors && ( -
+
{backgroundColors.map((color) => { return ( ) diff --git a/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx b/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx new file mode 100644 index 00000000000..500ec454afe --- /dev/null +++ b/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx @@ -0,0 +1,83 @@ +import { page, userEvent } from 'vite-plus/test/browser' +import { render } from 'vitest-browser-react' +import AppIconPicker from '../../app-icon-picker' +import { backgroundColors } from '../constants' +import EmojiPickerInner from '../Inner' + +// Real layout is needed to verify virtualized rows remain reachable using the keyboard. +it('selects a searched emoji with the keyboard and keeps style controls usable', async () => { + const onSelect = vi.fn() + await render( +
+ +
, + ) + const search = page.getByRole('searchbox') + await search.fill('rabbit face') + await expect.element(page.getByRole('gridcell', { name: 'Rabbit face' })).toBeVisible() + await userEvent.keyboard('{Enter}') + await expect.poll(() => onSelect.mock.lastCall).toEqual(['🐰', '#FFEAD5']) + await page.getByRole('button', { name: '#E4FBCC' }).click() + expect(onSelect).toHaveBeenLastCalledWith('🐰', '#E4FBCC') + await search.fill('') + await expect + .element(page.getByRole('gridcell', { name: 'Grinning face', exact: true })) + .toBeVisible() + await search.click() + await userEvent.keyboard( + '{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{Enter}', + ) + expect(onSelect).toHaveBeenCalledTimes(3) +}) + +// The production dialog is narrower than the old fixed-width eight-column grid. +// Verify every edge option is visible and clickable, rather than hiding overflow. +it('fits emoji and style options inside the app icon dialog', async () => { + const originalFontSize = document.documentElement.style.fontSize + document.documentElement.style.fontSize = '16px' + onTestFinished(() => { + document.documentElement.style.fontSize = originalFontSize + }) + const onSelect = vi.fn() + await render( + {}} + onSelect={onSelect} + initialEmoji={{ icon: 'robot_face' }} + />, + ) + const dialog = page.getByRole('dialog') + const firstEmoji = page.getByRole('gridcell', { name: 'Grinning face', exact: true }) + await expect.element(firstEmoji).toBeVisible() + const bounds = dialog.element().getBoundingClientRect() + const options = [ + ...dialog.element().querySelectorAll('[role="row"] [role="gridcell"]'), + ...backgroundColors.map((color) => + page.getByRole('button', { name: color, exact: true }).element(), + ), + ] + for (const option of options) { + const rect = option.getBoundingClientRect() + expect(rect.left).toBeGreaterThanOrEqual(bounds.left) + expect(rect.right).toBeLessThanOrEqual(bounds.right) + } + const footerTop = page + .getByRole('button', { name: 'app.iconPicker.ok', exact: true }) + .element() + .getBoundingClientRect().top + for (const color of backgroundColors) { + expect( + page.getByRole('button', { name: color, exact: true }).element().getBoundingClientRect() + .bottom, + ).toBeLessThanOrEqual(footerTop) + } + await page.getByRole('gridcell', { name: 'Face with tears of joy', exact: true }).click() + await page.getByRole('button', { name: backgroundColors.at(-1)!, exact: true }).click() + await page.getByRole('button', { name: 'app.iconPicker.ok', exact: true }).click() + expect(onSelect).toHaveBeenCalledWith({ + type: 'emoji', + icon: '😂', + background: backgroundColors.at(-1), + }) +}) diff --git a/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx b/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx index c5321def681..74b67300aca 100644 --- a/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx +++ b/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx @@ -1,181 +1,55 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import EmojiPickerInner from '../Inner' -vi.mock('@emoji-mart/data', () => ({ - default: { - categories: [ - { - id: 'nature', - emojis: ['rabbit', 'bear'], - }, - { - id: 'food', - emojis: ['apple', 'orange'], - }, - ], - }, -})) +vi.mock('@/utils/var', () => ({ basePath: '/console' })) -vi.mock('emoji-mart', () => ({ - init: vi.fn(), -})) - -vi.mock('@/utils/emoji', () => ({ - searchEmoji: vi.fn().mockResolvedValue(['dog', 'cat']), -})) +mockEmojiData() describe('EmojiPickerInner', () => { - const mockOnSelect = vi.fn() - - beforeEach(() => { - vi.clearAllMocks() - // Define the custom element to avoid "Unknown custom element" warnings - if (!customElements.get('em-emoji')) { - customElements.define( - 'em-emoji', - class extends HTMLElement { - static get observedAttributes() { - return ['id'] - } - }, - ) - } + it('selects Unicode from self-hosted data and preserves the chosen background', async () => { + const onSelect = vi.fn() + render() + fireEvent.click(await screen.findByRole('gridcell', { name: 'Grinning face' })) + expect(onSelect).toHaveBeenLastCalledWith('😀', '#FFEAD5') + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + expect(onSelect).toHaveBeenLastCalledWith('😀', '#E4FBCC') + expect(fetch).toHaveBeenCalledTimes(2) + expect(fetch).toHaveBeenCalledWith( + '/console/emoji/emojibase-17.0.0/en/data.json', + expect.any(Object), + ) + expect(fetch).toHaveBeenCalledWith( + '/console/emoji/emojibase-17.0.0/en/messages.json', + expect.any(Object), + ) }) - describe('Rendering', () => { - it('renders initial categories and emojis correctly', () => { - render() - - expect(screen.getByText('nature'))!.toBeInTheDocument() - expect(screen.getByText('food'))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() - }) - - it('initializes selected emoji and background when provided', () => { - render() - - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - expect(mockOnSelect).not.toHaveBeenCalled() - }) + it('filters search, selects a result, shows empty state and restores categories on clear', async () => { + const onSelect = vi.fn() + render() + await screen.findByRole('gridcell', { name: 'Grinning face' }) + const search = screen.getByRole('searchbox') + fireEvent.change(search, { target: { value: 'rabbit' } }) + fireEvent.click(await screen.findByRole('gridcell', { name: 'Rabbit face' })) + expect(onSelect).toHaveBeenLastCalledWith('🐰', '#FFEAD5') + fireEvent.change(search, { target: { value: 'zzzz-no-emoji' } }) + await screen.findByText('common.noData') + fireEvent.change(search, { target: { value: '' } }) + await screen.findByRole('gridcell', { name: 'Grinning face' }) }) - describe('User Interactions', () => { - it('calls searchEmoji and displays results when typing in search input', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await waitFor(() => { - expect(screen.getByText('Search'))!.toBeInTheDocument() - }) - - const searchSection = screen.getByText('Search').parentElement - expect(searchSection?.querySelectorAll('em-emoji').length).toBe(2) - }) - - it('updates selected emoji and calls onSelect when an emoji is clicked', async () => { - render() - const emojiButton = screen.getByRole('button', { name: 'rabbit' }) - - await act(async () => { - fireEvent.click(emojiButton) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('rabbit', expect.any(String)) - }) - - it('toggles style colors display when clicking the chevron', async () => { - render() - - expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - expect(toggleButton)!.toBeInTheDocument() - - await act(async () => { - fireEvent.click(toggleButton!) - }) - - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - const colorOptions = document.querySelectorAll('[style^="background:"]') - expect(colorOptions.length).toBeGreaterThan(0) - }) - - it('updates background color and calls onSelect when a color is clicked', async () => { - render() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - await act(async () => { - fireEvent.click(toggleButton!) - }) - - const emojiButton = screen.getByRole('button', { name: 'rabbit' }) - await act(async () => { - fireEvent.click(emojiButton) - }) - - mockOnSelect.mockClear() - - const colorOptions = screen.getAllByRole('button', { name: /^#/ }) - await act(async () => { - fireEvent.click(colorOptions[1]!) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC') - }) - - it('updates selected emoji when clicking a search result', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await screen.findByText('Search') - - const searchEmoji = screen.getByRole('button', { name: 'dog' }) - await act(async () => { - fireEvent.click(searchEmoji) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('dog', expect.any(String)) - }) - - it('toggles style colors display back and forth', async () => { - render() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - - await act(async () => { - fireEvent.click(toggleButton!) - }) - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Choose Style' })) - }) - expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument() - }) - - it('clears search results when input is cleared', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await screen.findByText('Search') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: '' } }) - }) - - expect(screen.queryByText('Search')).not.toBeInTheDocument() - }) + it('resolves a saved ID in style previews and only emits after a user action', async () => { + const onSelect = vi.fn() + render() + const color = screen.getByRole('button', { name: '#E4FBCC' }) + expect(color).toHaveTextContent('🐰') + expect(onSelect).not.toHaveBeenCalled() + fireEvent.click(color) + expect(onSelect).toHaveBeenCalledWith('🐰', '#E4FBCC') + fireEvent.click(screen.getByRole('button', { name: 'Choose Style' })) + await waitFor(() => + expect(screen.queryByRole('button', { name: '#E4FBCC' })).not.toBeInTheDocument(), + ) }) }) diff --git a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx index 7a36b5a779f..b0d8d41a7c7 100644 --- a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx @@ -1,28 +1,8 @@ import { act, fireEvent, render, screen } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import EmojiPicker from '../index' -vi.mock('@emoji-mart/data', () => ({ - default: { - categories: [ - { - id: 'category1', - name: 'Category 1', - emojis: ['emoji1', 'emoji2'], - }, - ], - }, -})) - -vi.mock('emoji-mart', () => ({ - init: vi.fn(), - SearchIndex: { - search: vi.fn().mockResolvedValue([{ skins: [{ native: '🔍' }] }]), - }, -})) - -vi.mock('@/utils/emoji', () => ({ - searchEmoji: vi.fn().mockResolvedValue(['🔍']), -})) +mockEmojiData() describe('EmojiPicker', () => { const mockOnSelect = vi.fn() @@ -43,7 +23,7 @@ describe('EmojiPicker', () => { render() }) expect(screen.getByRole('dialog', { name: /Emoji/i }))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search'))!.toBeInTheDocument() expect(screen.getByText(/Cancel/i))!.toBeInTheDocument() expect(screen.getByText(/OK/i))!.toBeInTheDocument() }) @@ -63,9 +43,7 @@ describe('EmojiPicker', () => { render() }) - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'emoji1' })) - }) + fireEvent.click(await screen.findByRole('gridcell', { name: 'Grinning face' })) const okButton = screen.getByText(/OK/i) expect(okButton.closest('button')).not.toBeDisabled() diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index de26e64e577..24c28a0071b 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -5,6 +5,7 @@ import { fireEvent, screen, render as testingLibraryRender, waitFor } from '@tes import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { ChunkingMode } from '@/models/datasets' +import { mockEmojiData } from '@/test/emoji-picker' import EditPipelineInfo from '../edit-pipeline-info' const render = (ui: ReactElement) => @@ -282,7 +283,7 @@ describe('EditPipelineInfo', () => { // Open icon picker fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) it('should save correct icon_info when starting with image icon type', async () => { @@ -346,13 +347,13 @@ describe('EditPipelineInfo', () => { // Open picker fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() // Close without selection - should revert to original image icon fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -370,7 +371,7 @@ describe('EditPipelineInfo', () => { // Open picker and select emoji fireEvent.click(getIconButton()) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -407,14 +408,14 @@ describe('EditPipelineInfo', () => { describe('AppIconPicker', () => { it('should not show picker initially', () => { render() - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) it('should open picker when icon is clicked', () => { render() fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) it('should close picker and update icon when emoji style is selected', async () => { @@ -425,7 +426,7 @@ describe('EditPipelineInfo', () => { fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -445,7 +446,7 @@ describe('EditPipelineInfo', () => { fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -488,7 +489,7 @@ describe('EditPipelineInfo', () => { render() fireEvent.click(getIconButton()) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -577,3 +578,5 @@ describe('EditPipelineInfo', () => { }) }) }) + +mockEmojiData() diff --git a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx index 4257199d6f3..52ed523e6b8 100644 --- a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { mockEmojiData } from '@/test/emoji-picker' import RenameDatasetModal from '../index' const { mockToast } = vi.hoisted(() => { @@ -42,7 +43,7 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({ let selectedBackground = '#FFEAD5' return (
- + , +})) + +it('keeps desktop keyboard focus visible across sidebar states and conversation actions', async () => { + // Real Chromium is needed to prove native inert Tab skipping, focus restoration + // during collapse, and focus-visible opacity on a non-hovered conversation row. + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const screen = await render( + +
+ + +
+
, + ) + const before = screen.getByRole('button', { name: 'Before chat' }) + const collapse = screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' }) + const chat = screen.getByRole('button', { name: 'Chat input boundary' }) + await before.click() + await userEvent.tab() + await expect.element(collapse).toHaveFocus() + await userEvent.tab() + await expect.element(screen.getByRole('button', { name: 'share.chat.newChat' })).toHaveFocus() + await userEvent.tab() + await expect.element(screen.getByRole('button', { name: 'Saved conversation' })).toHaveFocus() + await userEvent.tab() + const more = screen.getByRole('button', { name: 'common.operation.more' }).first() + await expect.element(more).toHaveFocus() + expect(getComputedStyle(more.element()).opacity).toBe('1') + await userEvent.keyboard('{Enter}') + await expect + .element(screen.getByRole('menuitem', { name: 'explore.sidebar.action.pin' })) + .toBeVisible() + await userEvent.keyboard('{Escape}') + await userEvent.tab() + await userEvent.tab() + await expect.element(chat).toHaveFocus() + + // Collapsing must put focus on the visible replacement toggle, and the next + // native Tab must skip both hidden copies of the sidebar's actions. + await collapse.click() + const expand = screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' }).last() + await expect.element(expand).toHaveFocus() + await userEvent.tab() + await expect.element(chat).toHaveFocus() + await userEvent.tab({ shift: true }) + await expect.element(expand).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect.element(collapse).toHaveFocus() + expect(collapse.element().checkVisibility({ checkOpacity: true })).toBe(true) +}) + +it('keeps the floating sidebar available through its portalled menu and closes after focus leaves', async () => { + // Pointer leave and Escape cross the real portal boundary; focus must return + // to a visible, interactive trigger instead of a newly inert offscreen panel. + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const screen = await render( + +
+ + +
+
, + ) + await screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' }).click() + await screen.getByRole('region', { name: 'Chat preview' }).hover({ position: { x: 2, y: 150 } }) + const conversation = screen.getByRole('button', { name: 'Saved conversation' }).last() + await expect.element(conversation).toBeVisible() + await conversation.click() + await userEvent.tab() + const more = screen.getByRole('button', { name: 'common.operation.more' }).nth(2) + await expect.element(more).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect + .element(screen.getByRole('menuitem', { name: 'explore.sidebar.action.pin' })) + .toBeVisible() + await screen.getByRole('button', { name: 'Outside sidebar' }).hover() + await userEvent.keyboard('{Escape}') + await expect.element(more).toHaveFocus() + expect(more.element().checkVisibility({ checkOpacity: true })).toBe(true) + expect(more.element().closest('[inert]')).toBeNull() + await userEvent.keyboard('{Enter}') + await expect + .element(screen.getByRole('menuitem', { name: 'explore.sidebar.action.pin' })) + .toBeVisible() + await userEvent.keyboard('{Escape}') + await expect.element(more).toHaveFocus() + await screen.getByRole('button', { name: 'Outside sidebar' }).click() + await expect.poll(() => conversation.element().closest('[inert]')).not.toBeNull() +}) + +it('closes the floating sidebar when both the pointer and keyboard focus leave it', async () => { + // Native pointer leave precedes blur here. Tab between sidebar controls must + // keep the panel usable, but moving focus outside must make its actions inert. + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const screen = await render( + +
+ + +
+
, + ) + await screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' }).click() + await screen.getByRole('region', { name: 'Chat preview' }).hover({ position: { x: 2, y: 150 } }) + const conversation = screen.getByRole('button', { name: 'Saved conversation' }).last() + await conversation.click() + await screen.getByRole('button', { name: 'Outside sidebar' }).hover() + await expect.element(conversation).toHaveFocus() + + await userEvent.tab() + const more = screen.getByRole('button', { name: 'common.operation.more' }).nth(2) + await expect.element(more).toHaveFocus() + await userEvent.tab({ shift: true }) + await expect.element(conversation).toHaveFocus() + + const outside = screen.getByRole('button', { name: 'Outside sidebar' }) + await outside.click() + await expect.element(outside).toHaveFocus() + await expect.poll(() => conversation.element().closest('[inert]')).not.toBeNull() +}) diff --git a/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx b/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx index 11c0f04a649..39bff5e5008 100644 --- a/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx +++ b/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx @@ -8,6 +8,16 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { + Dialog, + DialogBackdrop, + DialogClose, + DialogPopup, + DialogPortal, + DialogTitle, + DialogTrigger, + DialogViewport, +} from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -77,86 +87,105 @@ const HeaderInMobile = () => { return ( <> -
- $['sidebar.expandSidebar'], { ns: 'layout' })} - size="lg" - className="shrink-0" - onClick={() => setShowSidebar(true)} - > -
{sourcesQuery.isPending ? (
- +
) : sourcesQuery.error && !sourcesQuery.data ? (
@@ -724,7 +724,7 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) )} {!filteredSources.length && completingFilteredResults && (
- +
)}
diff --git a/web/features/skills/detail/skill-pdf-preview.tsx b/web/features/skills/detail/skill-pdf-preview.tsx index d407c23f414..838a404230c 100644 --- a/web/features/skills/detail/skill-pdf-preview.tsx +++ b/web/features/skills/detail/skill-pdf-preview.tsx @@ -5,7 +5,7 @@ import { PdfHighlighter, PdfLoader, } from '@/app/components/base/file-uploader/pdf-highlighter-adapter' -import Loading from '@/app/components/base/loading' +import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' export function SkillPdfPreview({ fileName, url }: { fileName: string; url: string }) { return ( @@ -14,11 +14,7 @@ export function SkillPdfPreview({ fileName, url }: { fileName: string; url: stri - -
- } + beforeLoad={} > {(pdfDocument) => ( Date: Fri, 18 Sep 2026 03:56:45 +0000 Subject: [PATCH 12/21] fix: normalize json part before deduplicating (#42196) --- api/core/tools/tool_engine.py | 11 ++- .../unit_tests/core/tools/test_tool_engine.py | 84 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index d9761eb4dbd..9c747acc2f8 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -273,8 +273,15 @@ class ToolEngine: # Add JSON parts, avoiding duplicates from text parts. if json_parts: - existing_parts = set(parts) - parts.extend(p for p in json_parts if p not in existing_parts) + + def normalize_json(text: str) -> str: + try: + return json.dumps(json.loads(text), ensure_ascii=False, sort_keys=True, separators=(",", ":")) + except (ValueError, RecursionError): + return text + + existing_parts = {normalize_json(p) for p in parts} + parts.extend(p for p in json_parts if normalize_json(p) not in existing_parts) return "".join(parts) diff --git a/api/tests/unit_tests/core/tools/test_tool_engine.py b/api/tests/unit_tests/core/tools/test_tool_engine.py index f688c68cfa6..941dbb22a7d 100644 --- a/api/tests/unit_tests/core/tools/test_tool_engine.py +++ b/api/tests/unit_tests/core/tools/test_tool_engine.py @@ -162,6 +162,90 @@ def test_convert_tool_response_to_str_and_extract_binary_messages(): ) +@pytest.mark.parametrize("json_first", [True, False]) +@pytest.mark.parametrize( + ("text", "json_object"), + [ + pytest.param('{"a":1,"b":2}', {"a": 1, "b": 2}, id="compact"), + pytest.param(' \r\n{\n "a": 1,\n "b": 2\n}\t', {"a": 1, "b": 2}, id="whitespace"), + pytest.param('{"b": 2, "a": 1}', {"a": 1, "b": 2}, id="key-order"), + pytest.param( + '{"items":[{"b":2,"a":1}],"nested":{"y":2,"x":1}}', + {"nested": {"x": 1, "y": 2}, "items": [{"a": 1, "b": 2}]}, + id="nested-key-order", + ), + pytest.param(r'{"\u540d\u79f0":"\u4e2d\u6587"}', {"名称": "中文"}, id="unicode-escapes"), + pytest.param(r'{"path":"\/a\/b"}', {"path": "/a/b"}, id="slash-escapes"), + pytest.param('{"a":1e0}', {"a": 1.0}, id="exponent"), + pytest.param(" { } ", {}, id="empty-object"), + pytest.param('[ {"b":2,"a":1},null,true ]', [{"a": 1, "b": 2}, None, True], id="array"), + ], +) +def test_convert_tool_response_deduplicates_equivalent_json( + text: str, json_object: dict[str, Any] | list[Any], json_first: bool +): + tool = _build_tool() + messages = [ + ToolInvokeMessage( + type=ToolInvokeMessage.MessageType.JSON, + message=ToolInvokeMessage.JsonMessage(json_object=json_object), + ), + tool.create_text_message(text), + ] + if not json_first: + messages.reverse() + + assert ToolEngine.tool_response_to_str(messages) == text + + +@pytest.mark.parametrize( + ("text", "json_object", "json_text"), + [ + pytest.param("plain text", {"a": 1}, '{"a": 1}', id="plain-text"), + pytest.param("[" * 2000 + "0" + "]" * 2000, {"a": 1}, '{"a": 1}', id="deeply-nested-text"), + pytest.param("9" * 5000, {"a": 1}, '{"a": 1}', id="oversized-integer"), + pytest.param("{'a': 1}", {"a": 1}, '{"a": 1}', id="invalid-json"), + pytest.param('result: {"a":1}', {"a": 1}, '{"a": 1}', id="embedded-json"), + pytest.param('{"a":1}{"a":1}', {"a": 1}, '{"a": 1}', id="concatenated-json"), + pytest.param('{"a":1}', {"a": 2}, '{"a": 2}', id="different-value"), + pytest.param('{"a":1}', {"b": 1}, '{"b": 1}', id="different-key"), + pytest.param('{"a":[1,2]}', {"a": [2, 1]}, '{"a": [2, 1]}', id="array-order"), + pytest.param('{"a":true}', {"a": 1}, '{"a": 1}', id="boolean-vs-number"), + pytest.param('{"a":null}', {"a": False}, '{"a": false}', id="null-vs-boolean"), + pytest.param('{"a":"1"}', {"a": 1}, '{"a": 1}', id="string-vs-number"), + pytest.param('{"a":"a b"}', {"a": "ab"}, '{"a": "ab"}', id="string-whitespace"), + ], +) +def test_convert_tool_response_preserves_distinct_json(text: str, json_object: dict[str, Any], json_text: str): + tool = _build_tool() + messages = [tool.create_json_message(json_object), tool.create_text_message(text)] + + assert ToolEngine.tool_response_to_str(messages) == text + json_text + + +@pytest.mark.parametrize("suppress_output", [True, False]) +def test_convert_tool_response_preserves_json_only_output(suppress_output: bool): + tool = _build_tool() + messages = [tool.create_json_message({"b": 2, "a": "中文"}, suppress_output=suppress_output)] + + assert ToolEngine.tool_response_to_str(messages) == ("" if suppress_output else '{"b": 2, "a": "中文"}') + + +def test_convert_tool_response_deduplicates_only_matching_json(): + tool = _build_tool() + text = '{"b":2,"a":1}' + messages = [ + tool.create_json_message({"a": 1, "b": 2}), + tool.create_text_message(text), + tool.create_text_message("result"), + tool.create_text_message(text), + tool.create_json_message({"c": 3}), + tool.create_json_message({"c": 3}), + ] + + assert ToolEngine.tool_response_to_str(messages) == text + "result" + text + '{"c": 3}{"c": 3}' + + @pytest.mark.parametrize("sqlite_session", [(MessageFile,)], indirect=True) def test_create_message_files_and_invoke_generator(sqlite_engine: Engine, sqlite_session: Session): binaries = [ From 21d5e4e020b746703b4b764b8b4717aecbf6d828 Mon Sep 17 00:00:00 2001 From: Keith <148296621+keith991001@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:12:28 +0000 Subject: [PATCH 13/21] refactor(models): pass session into WorkflowRun accessors and drop dead ones (#42034) --- api/controllers/console/app/workflow_run.py | 24 +++- .../rag_pipeline/rag_pipeline_workflow.py | 24 ++-- .../console/snippets/snippet_workflow.py | 17 ++- api/fields/base.py | 20 +++ api/fields/conversation_fields.py | 20 +-- api/fields/workflow_run_fields.py | 43 +++++-- api/models/workflow.py | 26 +--- .../console/app/test_workflow_run_api.py | 35 +++--- .../test_rag_pipeline_workflow.py | 2 +- .../test_rag_pipeline_workflow_apis.py | 8 +- .../console/snippets/test_snippet_workflow.py | 6 +- .../models/test_workflow_run_accessors.py | 114 ++++++++++++++++++ 12 files changed, 247 insertions(+), 92 deletions(-) create mode 100644 api/tests/unit_tests/models/test_workflow_run_accessors.py diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 66873a9b523..08097d6d43e 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -3,11 +3,13 @@ from uuid import UUID from flask_restx import Resource from pydantic import BaseModel, Field, field_validator +from sqlalchemy.orm import Session from configs import dify_config from controllers.common.errors import NotFoundError from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models +from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission @@ -24,6 +26,8 @@ from fields.workflow_run_fields import ( WorkflowRunNodeExecutionListResponse, WorkflowRunNodeExecutionResponse, WorkflowRunPaginationResponse, + workflow_run_pagination_response_source, + workflow_run_response_source, ) from libs.custom_inputs import time_duration from libs.helper import dump_response, uuid_value @@ -147,7 +151,8 @@ class AdvancedChatAppWorkflowRunListApi(Resource): ) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @model_validate(WorkflowRunListQuery) - def get(self, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): + @with_session(write=False) + def get(self, session: Session, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): """ Get advanced chat app workflow run list """ @@ -158,7 +163,10 @@ class AdvancedChatAppWorkflowRunListApi(Resource): triggered_from=_triggered_from(req_data.triggered_from), ) - return dump_response(AdvancedChatWorkflowRunPaginationResponse, result) + return dump_response( + AdvancedChatWorkflowRunPaginationResponse, + workflow_run_pagination_response_source(result, session=session), + ) @console_ns.route("/apps//advanced-chat/workflow-runs/count") @@ -208,7 +216,8 @@ class WorkflowRunListApi(Resource): ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(WorkflowRunListQuery) - def get(self, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): + @with_session(write=False) + def get(self, session: Session, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): """ Get workflow run list """ @@ -219,7 +228,9 @@ class WorkflowRunListApi(Resource): triggered_from=_triggered_from(req_data.triggered_from), ) - return dump_response(WorkflowRunPaginationResponse, result) + return dump_response( + WorkflowRunPaginationResponse, workflow_run_pagination_response_source(result, session=session) + ) @console_ns.route("/apps//workflow-runs/count") @@ -267,8 +278,9 @@ class WorkflowRunDetailApi(Resource): @console_account_admission( rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) + @with_session(write=False) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) - def get(self, request_context: RequestContext, app_model: App, run_id: UUID): + def get(self, session: Session, request_context: RequestContext, app_model: App, run_id: UUID): """ Get workflow run detail """ @@ -280,7 +292,7 @@ class WorkflowRunDetailApi(Resource): if workflow_run is None: raise NotFoundError("Workflow run not found") - return dump_response(WorkflowRunDetailResponse, workflow_run) + return dump_response(WorkflowRunDetailResponse, workflow_run_response_source(workflow_run, session=session)) @console_ns.route("/apps//workflow-runs//node-executions") diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 230cc5a3cd5..4df2e49b5ec 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -54,6 +54,8 @@ from fields.workflow_run_fields import ( WorkflowRunNodeExecutionResponse, WorkflowRunPaginationResponse, node_execution_response_source, + workflow_run_pagination_response_source, + workflow_run_response_source, ) from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper @@ -199,12 +201,12 @@ class DraftRagPipelineApi(Resource): @get_rag_pipeline @edit_permission_required @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) - def get(self, pipeline: Pipeline): + @with_session(write=False) + def get(self, session: Session, pipeline: Pipeline): """ Get draft rag pipeline's workflow """ # fetch draft workflow by app_model - session = db.session() rag_pipeline_service = RagPipelineService(session) workflow = rag_pipeline_service.get_draft_workflow(pipeline=pipeline) @@ -913,7 +915,8 @@ class RagPipelineWorkflowRunListApi(Resource): @login_required @account_initialization_required @get_rag_pipeline - def get(self, pipeline: Pipeline): + @with_session(write=False) + def get(self, session: Session, pipeline: Pipeline): """ Get workflow run list """ @@ -928,10 +931,12 @@ class RagPipelineWorkflowRunListApi(Resource): "limit": query.limit, } - rag_pipeline_service = RagPipelineService(db.session()) + rag_pipeline_service = RagPipelineService(session) result = rag_pipeline_service.get_rag_pipeline_paginate_workflow_runs(pipeline=pipeline, args=args) - return WorkflowRunPaginationResponse.model_validate(result, from_attributes=True).model_dump(mode="json") + return WorkflowRunPaginationResponse.model_validate( + workflow_run_pagination_response_source(result, session=session), from_attributes=True + ).model_dump(mode="json") @console_ns.route("/rag/pipelines//workflow-runs/") @@ -945,18 +950,21 @@ class RagPipelineWorkflowRunDetailApi(Resource): @login_required @account_initialization_required @get_rag_pipeline - def get(self, pipeline: Pipeline, run_id: UUID): + @with_session(write=False) + def get(self, session: Session, pipeline: Pipeline, run_id: UUID): """ Get workflow run detail """ run_id_str = str(run_id) - rag_pipeline_service = RagPipelineService(db.session()) + rag_pipeline_service = RagPipelineService(session) workflow_run = rag_pipeline_service.get_rag_pipeline_workflow_run(pipeline=pipeline, run_id=run_id_str) if workflow_run is None: raise NotFound("Workflow run not found") - return WorkflowRunDetailResponse.model_validate(workflow_run, from_attributes=True).model_dump(mode="json") + return WorkflowRunDetailResponse.model_validate( + workflow_run_response_source(workflow_run, session=session), from_attributes=True + ).model_dump(mode="json") @console_ns.route("/rag/pipelines//workflow-runs//node-executions") diff --git a/api/controllers/console/snippets/snippet_workflow.py b/api/controllers/console/snippets/snippet_workflow.py index 5cc8d42d5a6..d4a6497ad7a 100644 --- a/api/controllers/console/snippets/snippet_workflow.py +++ b/api/controllers/console/snippets/snippet_workflow.py @@ -12,6 +12,7 @@ from controllers.common.controller_schemas import WorkflowUpdatePayload from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models +from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync from controllers.console.app.workflow import ( @@ -52,6 +53,8 @@ from fields.workflow_run_fields import ( WorkflowRunNodeExecutionResponse, WorkflowRunPaginationResponse, node_execution_response_source, + workflow_run_pagination_response_source, + workflow_run_response_source, ) from graphon.graph_engine.manager import GraphEngineManager from libs import helper @@ -512,7 +515,8 @@ class SnippetWorkflowRunsApi(Resource): @login_required @account_initialization_required @get_snippet - def get(self, snippet: CustomizedSnippet): + @with_session(write=False) + def get(self, session: Session, snippet: CustomizedSnippet): """List workflow runs for snippet.""" query = WorkflowRunQuery.model_validate( { @@ -528,7 +532,9 @@ class SnippetWorkflowRunsApi(Resource): snippet_service = _snippet_service() result = snippet_service.get_snippet_workflow_runs(snippet=snippet, args=args) - return WorkflowRunPaginationResponse.model_validate(result, from_attributes=True).model_dump(mode="json") + return WorkflowRunPaginationResponse.model_validate( + workflow_run_pagination_response_source(result, session=session), from_attributes=True + ).model_dump(mode="json") @console_ns.route("/snippets//workflow-runs/") @@ -544,7 +550,8 @@ class SnippetWorkflowRunDetailApi(Resource): @login_required @account_initialization_required @get_snippet - def get(self, snippet: CustomizedSnippet, run_id): + @with_session(write=False) + def get(self, session: Session, snippet: CustomizedSnippet, run_id): """Get workflow run detail for snippet.""" run_id = str(run_id) @@ -554,7 +561,9 @@ class SnippetWorkflowRunDetailApi(Resource): if not workflow_run: raise NotFound("Workflow run not found") - return WorkflowRunDetailResponse.model_validate(workflow_run, from_attributes=True).model_dump(mode="json") + return WorkflowRunDetailResponse.model_validate( + workflow_run_response_source(workflow_run, session=session), from_attributes=True + ).model_dump(mode="json") @console_ns.route("/snippets//workflow-runs//node-executions") diff --git a/api/fields/base.py b/api/fields/base.py index 826a07d00ea..c8c1b8c8d56 100644 --- a/api/fields/base.py +++ b/api/fields/base.py @@ -1,7 +1,27 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pydantic import BaseModel, ConfigDict +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + +class SessionResponseSource[SourceT]: + """Wrap a model so session-backed accessors resolve during response validation. + + Subclasses override the accessors that need the session as properties; + every other attribute is delegated to the wrapped model unchanged. + """ + + def __init__(self, source: SourceT, *, session: Session) -> None: + self._source = source + self._session = session + + def __getattr__(self, name: str) -> object: + return getattr(self._source, name) # guard-ignore: no-new-getattr -- delegates model fields + class ResponseModel(BaseModel): model_config = ConfigDict( diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index 37cd8cd6214..bb016b9ca53 100644 --- a/api/fields/conversation_fields.py +++ b/api/fields/conversation_fields.py @@ -5,9 +5,8 @@ from datetime import datetime from typing import Annotated, Any from pydantic import Field, WithJsonSchema, field_validator, model_validator -from sqlalchemy.orm import Session -from fields.base import ResponseModel +from fields.base import ResponseModel, SessionResponseSource from graphon.file import File from libs.helper import to_timestamp from models.account import Account @@ -31,22 +30,13 @@ Int64 = Annotated[int, WithJsonSchema({"format": "int64", "type": "integer"})] URLString = Annotated[str, WithJsonSchema({"format": "url", "type": "string"})] -class _SessionResponseSource[SourceT]: - def __init__(self, source: SourceT, *, session: Session) -> None: - self._source = source - self._session = session - - def __getattr__(self, name: str) -> object: - return getattr(self._source, name) # guard-ignore: no-new-getattr -- delegates model fields - - -class _FeedbackResponseSource(_SessionResponseSource[MessageFeedback]): +class _FeedbackResponseSource(SessionResponseSource[MessageFeedback]): @property def from_account(self) -> Account | None: return self._source.from_account_with_session(session=self._session) -class _AnnotationResponseSource(_SessionResponseSource[MessageAnnotation]): +class _AnnotationResponseSource(SessionResponseSource[MessageAnnotation]): @property def account(self) -> Account | None: return self._source.account_with_session(session=self._session) @@ -56,7 +46,7 @@ class _AnnotationResponseSource(_SessionResponseSource[MessageAnnotation]): return self._source.annotation_create_account_with_session(session=self._session) -class MessageResponseSource(_SessionResponseSource[MessageModel]): +class MessageResponseSource(SessionResponseSource[MessageModel]): @property def inputs(self) -> dict[str, Any]: return self._source.inputs_with_session(session=self._session) @@ -91,7 +81,7 @@ class MessageResponseSource(_SessionResponseSource[MessageModel]): return self._source.message_files_with_session(session=self._session) -class ConversationResponseSource(_SessionResponseSource[ConversationModel]): +class ConversationResponseSource(SessionResponseSource[ConversationModel]): @property def inputs(self) -> dict[str, Any]: return self._source.inputs_with_session(session=self._session) diff --git a/api/fields/workflow_run_fields.py b/api/fields/workflow_run_fields.py index 55caa4893f9..4b69c1a107a 100644 --- a/api/fields/workflow_run_fields.py +++ b/api/fields/workflow_run_fields.py @@ -6,7 +6,6 @@ Pydantic models in this module. from __future__ import annotations -from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Any @@ -14,7 +13,7 @@ from typing import Any from pydantic import AliasChoices, Field, field_validator from sqlalchemy.orm import Session -from fields.base import ResponseModel +from fields.base import ResponseModel, SessionResponseSource from fields.end_user_fields import SimpleEndUser from fields.member_fields import SimpleAccount from libs.helper import to_timestamp @@ -185,27 +184,45 @@ class WorkflowRunNodeExecutionResponse(ResponseModel): return to_timestamp(value) -@dataclass(frozen=True) -class WorkflowNodeExecutionResponseSource: - """Expose session-backed node-execution accessors during response validation.""" - - node_execution: Any - session: Session +class WorkflowRunResponseSource(SessionResponseSource[Any]): + """Expose session-backed workflow-run accessors during response validation.""" @property def created_by_account(self) -> Any: - return self.node_execution.created_by_account(self.session) + return self._source.created_by_account(self._session) @property def created_by_end_user(self) -> Any: - return self.node_execution.created_by_end_user(self.session) + return self._source.created_by_end_user(self._session) - def __getattr__(self, name: str) -> Any: - return getattr(self.node_execution, name) # guard-ignore: no-new-getattr -- delegates model fields + +def workflow_run_response_source(workflow_run: Any, *, session: Session) -> WorkflowRunResponseSource: + return WorkflowRunResponseSource(workflow_run, session=session) + + +def workflow_run_pagination_response_source(pagination: Any, *, session: Session) -> dict[str, Any]: + """Wrap each run in a pagination payload so list responses resolve accessors via the session.""" + return { + "limit": pagination.limit, + "has_more": pagination.has_more, + "data": [workflow_run_response_source(run, session=session) for run in pagination.data], + } + + +class WorkflowNodeExecutionResponseSource(SessionResponseSource[Any]): + """Expose session-backed node-execution accessors during response validation.""" + + @property + def created_by_account(self) -> Any: + return self._source.created_by_account(self._session) + + @property + def created_by_end_user(self) -> Any: + return self._source.created_by_end_user(self._session) def node_execution_response_source(node_execution: Any, *, session: Session) -> WorkflowNodeExecutionResponseSource: - return WorkflowNodeExecutionResponseSource(node_execution=node_execution, session=session) + return WorkflowNodeExecutionResponseSource(node_execution, session=session) class WorkflowRunNodeExecutionListResponse(ResponseModel): diff --git a/api/models/workflow.py b/api/models/workflow.py index dbc248e5a11..a53cd4248c8 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -866,19 +866,15 @@ class WorkflowRun(Base): back_populates="workflow_run", ) - @property - @deprecated("This method is retained for historical reasons; avoid using it if possible.") - def created_by_account(self): + def created_by_account(self, session: orm.Session) -> Account | None: created_by_role = CreatorUserRole(self.created_by_role) - return db.session.get(Account, self.created_by) if created_by_role == CreatorUserRole.ACCOUNT else None + return session.get(Account, self.created_by) if created_by_role == CreatorUserRole.ACCOUNT else None - @property - @deprecated("This method is retained for historical reasons; avoid using it if possible.") - def created_by_end_user(self): + def created_by_end_user(self, session: orm.Session): from .model import EndUser created_by_role = CreatorUserRole(self.created_by_role) - return db.session.get(EndUser, self.created_by) if created_by_role == CreatorUserRole.END_USER else None + return session.get(EndUser, self.created_by) if created_by_role == CreatorUserRole.END_USER else None @property def graph_dict(self) -> Mapping[str, Any]: @@ -892,20 +888,6 @@ class WorkflowRun(Base): def outputs_dict(self) -> Mapping[str, Any]: return json.loads(self.outputs) if self.outputs else {} - @property - @deprecated("This method is retained for historical reasons; avoid using it if possible.") - def message(self): - from .model import Message - - return db.session.scalar( - select(Message).where(Message.app_id == self.app_id, Message.workflow_run_id == self.id) - ) - - @property - @deprecated("This method is retained for historical reasons; avoid using it if possible.") - def workflow(self): - return db.session.scalar(select(Workflow).where(Workflow.id == self.workflow_id)) - def to_dict(self) -> WorkflowRunDict: return WorkflowRunDict( id=self.id, diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py index 2f9e597bcc1..16c85e24f62 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py @@ -153,13 +153,12 @@ def test_workflow_run_list_returns_frontend_history_contract( _account(sqlite_session) workflow_run = _workflow_run_summary(sqlite_session) workflow_runs = Mock() - workflow_runs.get_paginate_workflow_runs.return_value = { - "limit": 10, - "has_more": False, - "data": [workflow_run], - } + workflow_runs.get_paginate_workflow_runs.return_value = SimpleNamespace( + limit=10, + has_more=False, + data=[workflow_run], + ) _mock_application_services(monkeypatch, workflow_runs) - monkeypatch.setattr(db, "session", sqlite_session) request_context = _request_context() api = workflow_run_module.WorkflowRunListApi() @@ -168,6 +167,7 @@ def test_workflow_run_list_returns_frontend_history_contract( with app.test_request_context("/apps/app-1/workflow-runs?limit=10", method="GET"): payload = handler( api, + sqlite_session, workflow_run_module.WorkflowRunListQuery(limit=10), request_context, app_model=_app(), @@ -208,13 +208,12 @@ def test_advanced_chat_workflow_run_list_keeps_message_fields( message_id="message-1", ) workflow_runs = Mock() - workflow_runs.get_paginate_advanced_chat_workflow_runs.return_value = { - "limit": 1, - "has_more": True, - "data": [workflow_run], - } + workflow_runs.get_paginate_advanced_chat_workflow_runs.return_value = SimpleNamespace( + limit=1, + has_more=True, + data=[workflow_run], + ) _mock_application_services(monkeypatch, workflow_runs) - monkeypatch.setattr(db, "session", sqlite_session) request_context = _request_context() api = workflow_run_module.AdvancedChatAppWorkflowRunListApi() @@ -223,6 +222,7 @@ def test_advanced_chat_workflow_run_list_keeps_message_fields( with app.test_request_context("/apps/app-1/advanced-chat/workflow-runs?limit=1", method="GET"): payload = handler( api, + sqlite_session, workflow_run_module.WorkflowRunListQuery(limit=1), request_context, app_model=_app(), @@ -288,14 +288,13 @@ def test_workflow_run_detail_returns_frontend_detail_contract( workflow_runs = Mock() workflow_runs.get_workflow_run.return_value = workflow_run _mock_application_services(monkeypatch, workflow_runs) - monkeypatch.setattr(db, "session", sqlite_session) request_context = _request_context() api = workflow_run_module.WorkflowRunDetailApi() handler = unwrap(api.get) with app.test_request_context("/apps/app-1/workflow-runs/run-1", method="GET"): - payload = handler(api, request_context, app_model=_app(), run_id="run-1") + payload = handler(api, sqlite_session, request_context, app_model=_app(), run_id="run-1") response = _serialize_200_response(api.get, payload) @@ -324,7 +323,9 @@ def test_workflow_run_detail_returns_frontend_detail_contract( ) -def test_workflow_run_detail_maps_missing_run_to_not_found(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: +def test_workflow_run_detail_maps_missing_run_to_not_found( + app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: workflow_runs = Mock() workflow_runs.get_workflow_run.return_value = None _mock_application_services(monkeypatch, workflow_runs) @@ -334,7 +335,7 @@ def test_workflow_run_detail_maps_missing_run_to_not_found(app: Flask, monkeypat with app.test_request_context("/apps/app-1/workflow-runs/run-1", method="GET"): with pytest.raises(NotFoundError, match="Workflow run not found"): - handler(api, request_context, app_model=_app(), run_id="run-1") + handler(api, sqlite_session, request_context, app_model=_app(), run_id="run-1") workflow_runs.get_workflow_run.assert_called_once_with( request_context, @@ -353,7 +354,7 @@ def test_workflow_run_node_executions_return_frontend_trace_contract( node_execution_response_source(execution, session=sqlite_session) ] _mock_application_services(monkeypatch, workflow_runs) - monkeypatch.setattr(db, "session", sqlite_session) + monkeypatch.setattr(db, "session", lambda: sqlite_session) request_context = _request_context() api = workflow_run_module.WorkflowRunNodeExecutionListApi() diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py index f4a2e2c948f..f6ab01e9cce 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py @@ -129,7 +129,7 @@ def test_draft_rag_pipeline_workflow_get_serializes_response_model() -> None: api = module.DraftRagPipelineApi() handler = unwrap_all(api.get) - response = handler(api, _pipeline()) + response = handler(api, db.session(), _pipeline()) assert response["id"] == DEFAULT_WORKFLOW_ID assert response["graph"] == {"nodes": [], "edges": []} diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py index 46009877a09..fd26d0ec33f 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py @@ -255,7 +255,7 @@ def workflow_author(sqlite_database: scoped_session[Session]) -> Account: class TestDraftWorkflowApi: - def test_get_draft_success(self, app: Flask, workflow_author: Account) -> None: + def test_get_draft_success(self, app: Flask, workflow_author: Account, sqlite_session: Session) -> None: api = DraftRagPipelineApi() method = unwrap(api.get) @@ -272,7 +272,7 @@ class TestDraftWorkflowApi: return_value=service, ), ): - result = method(api, pipeline) + result = method(api, sqlite_session, pipeline) assert result["id"] == "workflow-1" assert result["graph"] == {"nodes": [], "edges": []} @@ -285,7 +285,7 @@ class TestDraftWorkflowApi: } assert result["updated_by"] is None - def test_get_draft_not_exist(self, app: Flask) -> None: + def test_get_draft_not_exist(self, app: Flask, sqlite_session: Session) -> None: api = DraftRagPipelineApi() method = unwrap(api.get) @@ -301,7 +301,7 @@ class TestDraftWorkflowApi: ), ): with pytest.raises(DraftWorkflowNotExist): - method(api, pipeline) + method(api, sqlite_session, pipeline) def test_sync_hash_not_match(self, app: Flask) -> None: api = DraftRagPipelineApi() diff --git a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py index 8af0e633620..733ab1fb048 100644 --- a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py +++ b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py @@ -641,7 +641,9 @@ def test_delete_published_snippet_workflow_raises_bad_request_when_in_use( assert exc_info.value.code == 400 -def test_workflow_run_detail_raises_not_found_when_run_missing(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: +def test_workflow_run_detail_raises_not_found_when_run_missing( + app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: snippet = _snippet() monkeypatch.setattr( snippet_workflow_module, @@ -654,7 +656,7 @@ def test_workflow_run_detail_raises_not_found_when_run_missing(app: Flask, monke with app.test_request_context("/snippets/snippet-1/workflow-runs/run-1"): with pytest.raises(NotFound, match="Workflow run not found"): - handler(api, snippet=snippet, run_id="run-1") + handler(api, sqlite_session, snippet=snippet, run_id="run-1") def test_draft_node_last_run_raises_not_found_when_execution_missing( diff --git a/api/tests/unit_tests/models/test_workflow_run_accessors.py b/api/tests/unit_tests/models/test_workflow_run_accessors.py new file mode 100644 index 00000000000..62c94d004ee --- /dev/null +++ b/api/tests/unit_tests/models/test_workflow_run_accessors.py @@ -0,0 +1,114 @@ +"""Regression coverage for ``WorkflowRun`` account accessors. + +Ensures the ``@property``→session-parameter refactor preserves the role-based dispatch: +``created_by_account`` looks up an Account only when role is ACCOUNT; ``created_by_end_user`` +looks up an EndUser only when role is END_USER. Also covers the session-carrying response +sources used by the console endpoints that validate the run detail / pagination responses +with ``from_attributes=True``. +""" + +from types import SimpleNamespace +from uuid import uuid4 + +from sqlalchemy.orm import Session + +from fields.workflow_run_fields import workflow_run_pagination_response_source, workflow_run_response_source +from models.account import Account +from models.enums import CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom +from models.model import EndUser +from models.workflow import WorkflowRun + + +def _run(role: CreatorUserRole, created_by: str) -> WorkflowRun: + """Construct a WorkflowRun without touching the database.""" + return WorkflowRun( + id=str(uuid4()), + tenant_id="00000000-0000-0000-0000-000000000001", + app_id="00000000-0000-0000-0000-000000000002", + workflow_id=str(uuid4()), + type="workflow", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + version="1.0", + status="succeeded", + created_by_role=role, + created_by=created_by, + ) + + +class TestCreatedByAccount: + def test_returns_account_lookup_when_role_is_account(self, sqlite_session: Session) -> None: + account = Account(name="Test Account", email="test@example.com") + sqlite_session.add(account) + sqlite_session.flush() + run = _run(CreatorUserRole.ACCOUNT, created_by=account.id) + + result = run.created_by_account(session=sqlite_session) + + assert result is not None + assert result.id == account.id + + def test_returns_none_when_role_is_end_user(self, sqlite_session: Session) -> None: + account = Account(name="Test Account", email="test@example.com") + sqlite_session.add(account) + sqlite_session.flush() + run = _run(CreatorUserRole.END_USER, created_by=account.id) + + assert run.created_by_account(session=sqlite_session) is None + + +class TestCreatedByEndUser: + def test_returns_end_user_lookup_when_role_is_end_user(self, sqlite_session: Session) -> None: + end_user = EndUser( + tenant_id="00000000-0000-0000-0000-000000000001", + type=EndUserType.BROWSER, + session_id="session-1", + ) + sqlite_session.add(end_user) + sqlite_session.flush() + run = _run(CreatorUserRole.END_USER, created_by=end_user.id) + + result = run.created_by_end_user(session=sqlite_session) + + assert result is not None + assert result.id == end_user.id + + def test_returns_none_when_role_is_account(self, sqlite_session: Session) -> None: + end_user = EndUser( + tenant_id="00000000-0000-0000-0000-000000000001", + type=EndUserType.BROWSER, + session_id="session-1", + ) + sqlite_session.add(end_user) + sqlite_session.flush() + run = _run(CreatorUserRole.ACCOUNT, created_by=end_user.id) + + assert run.created_by_end_user(session=sqlite_session) is None + + +class TestWorkflowRunResponseSources: + def test_detail_source_resolves_accessors_and_proxies_other_attributes(self, sqlite_session: Session) -> None: + account = Account(name="Test Account", email="test@example.com") + sqlite_session.add(account) + sqlite_session.flush() + run = _run(CreatorUserRole.ACCOUNT, created_by=account.id) + + source = workflow_run_response_source(run, session=sqlite_session) + + resolved = source.created_by_account + assert resolved is not None + assert resolved.id == account.id + assert source.created_by_end_user is None + assert source.version == "1.0" + + def test_pagination_source_wraps_each_run(self, sqlite_session: Session) -> None: + account = Account(name="Test Account", email="test@example.com") + sqlite_session.add(account) + sqlite_session.flush() + runs = [_run(CreatorUserRole.ACCOUNT, created_by=account.id) for _ in range(2)] + pagination = SimpleNamespace(data=runs, limit=20, has_more=False) + + payload = workflow_run_pagination_response_source(pagination, session=sqlite_session) + + assert payload["limit"] == 20 + assert payload["has_more"] is False + assert [item.created_by_account.id for item in payload["data"]] == [account.id, account.id] From 3b8fc9c9b685129946f4c1694ddf60f521cefd8e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:13:14 +0000 Subject: [PATCH 14/21] refactor(i18n): colocate runtime and locale resources (#42482) --- .github/scripts/generate-i18n-changes.mjs | 5 +- .github/workflows/post-merge.yml | 4 +- .github/workflows/translate-i18n-claude.yml | 22 ++-- .github/workflows/trigger-i18n-sync.yml | 6 +- README.md | 2 +- docs/ar-SA/README.md | 2 +- docs/bn-BD/README.md | 2 +- docs/de-DE/README.md | 2 +- docs/es-ES/README.md | 2 +- docs/fr-FR/README.md | 2 +- docs/hi-IN/README.md | 2 +- docs/it-IT/README.md | 2 +- docs/ja-JP/README.md | 2 +- docs/ko-KR/README.md | 2 +- docs/pt-BR/README.md | 2 +- docs/sl-SI/README.md | 2 +- docs/tlh/README.md | 2 +- docs/tr-TR/README.md | 2 +- docs/vi-VN/README.md | 2 +- docs/zh-CN/README.md | 2 +- docs/zh-TW/README.md | 2 +- eslint.config.mjs | 2 +- knip.config.ts | 2 +- oxlint-suppressions.json | 2 +- web/.storybook/preview.tsx | 2 +- web/AGENTS.md | 2 +- web/__tests__/app-star-i18n.test.ts | 2 +- web/__tests__/i18n-upload-features.test.ts | 2 +- .../[appId]/access-config/page.tsx | 2 +- .../__tests__/document-title.spec.tsx | 4 +- .../[[...slug]]/__tests__/metadata.spec.ts | 4 +- .../integrations/[[...slug]]/layout.tsx | 2 +- .../creator/[uniqueHandle]/page.tsx | 2 +- .../skills/__tests__/metadata.spec.ts | 4 +- .../[[...category]]/__tests__/page.spec.tsx | 2 +- .../templates/[[...category]]/page.tsx | 2 +- web/app/__tests__/layout.spec.tsx | 2 +- web/app/__tests__/route-metadata.spec.ts | 4 +- .../components/app/access-config/index.tsx | 2 +- .../__tests__/csv-downloader.spec.tsx | 4 +- .../csv-downloader.tsx | 2 +- .../header-opts/__tests__/index.spec.tsx | 4 +- .../app/annotation/header-opts/index.tsx | 2 +- .../publish-with-multiple-model.tsx | 2 +- .../agent-tools/setting-built-in-tool.tsx | 2 +- .../generator-reflow.browser.spec.tsx | 4 +- .../select-dataset/__tests__/index.spec.tsx | 10 -- .../external-data-tool-modal-utils.spec.ts | 4 +- .../tools/external-data-tool-modal-utils.ts | 2 +- web/app/components/app/deploy/index.tsx | 2 +- .../app/overview/settings/index.tsx | 2 +- .../__tests__/hooks.spec.tsx | 2 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- .../answer/agent-roster-response-content.tsx | 4 +- .../__tests__/utils.spec.ts | 2 +- .../chat/answer/human-input-content/utils.ts | 2 +- .../embedded-chatbot/__tests__/hooks.spec.tsx | 8 +- .../base/chat/embedded-chatbot/hooks.tsx | 4 +- .../__tests__/form-generation.spec.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- .../text-to-speech/__tests__/index.spec.tsx | 2 +- .../__tests__/param-config-content.spec.tsx | 2 +- .../text-to-speech/index.tsx | 2 +- .../text-to-speech/param-config-content.tsx | 2 +- web/app/components/base/form/types.ts | 2 +- .../new-audio-button/__tests__/index.spec.tsx | 2 +- .../resize-handle/__tests__/index.spec.tsx | 2 +- .../billing/pricing/__tests__/footer.spec.tsx | 2 +- web/app/components/billing/pricing/footer.tsx | 2 +- .../datasets/access-config/index.tsx | 2 +- .../list/built-in-pipeline-list.tsx | 2 +- .../file-uploader/__tests__/index.spec.tsx | 2 +- .../hooks/__tests__/use-file-upload.spec.tsx | 2 +- .../file-uploader/hooks/use-file-upload.ts | 2 +- .../datasets/create/step-two/index.tsx | 2 +- .../language-select/__tests__/index.spec.tsx | 2 +- .../create/step-two/language-select/index.tsx | 2 +- .../__tests__/use-local-file-upload.spec.tsx | 2 +- .../__tests__/csv-downloader.spec.tsx | 2 +- .../detail/batch-modal/csv-downloader.tsx | 2 +- web/app/components/explore/category.tsx | 2 +- .../actions/__tests__/plugin.spec.ts | 4 +- .../commands/__tests__/language.spec.ts | 2 +- .../actions/commands/__tests__/slash.spec.tsx | 2 +- .../actions/commands/language.tsx | 2 +- .../actions/commands/slash-provider.tsx | 2 +- .../goto-anything/actions/plugin.tsx | 2 +- .../permission-set-modal/index.tsx | 2 +- .../resource-access-rule-section.tsx | 2 +- .../account-setting/members-page/index.tsx | 2 +- .../invite-modal/role-selector.tsx | 2 +- .../member-details-modal/index.tsx | 2 +- .../model-provider-page/hooks.ts | 2 +- .../model-provider-page/model-icon/index.tsx | 2 +- .../model-provider-page/model-name/index.tsx | 2 +- .../model-selector/popup-item.tsx | 2 +- .../model-selector/popup.tsx | 2 +- .../provider-added-card/index.tsx | 2 +- .../provider-icon/index.tsx | 2 +- .../permissions-page/index.tsx | 2 +- .../permissions-page/role-modal/index.tsx | 2 +- .../preference-page/__tests__/index.spec.tsx | 4 +- .../account-setting/preference-page/index.tsx | 6 +- .../workspace-role-checkbox-list.tsx | 2 +- .../integrations/tool-provider-card.tsx | 2 +- .../plugins/card/__tests__/index.spec.tsx | 4 +- web/app/components/plugins/card/index.tsx | 2 +- .../__tests__/server-entry.spec.tsx | 2 +- .../plugins/marketplace/home/home-guide.tsx | 2 +- .../home/marketplace-search-autocomplete.tsx | 2 +- .../components/plugins/marketplace/index.tsx | 2 +- ...st-with-collection-layout.browser.spec.tsx | 4 +- .../__tests__/list-with-collection.spec.tsx | 4 +- .../plugins/marketplace/list/card-wrapper.tsx | 2 +- .../marketplace/list/list-with-collection.tsx | 2 +- .../plugins/marketplace/templates/index.tsx | 4 +- .../__tests__/tts-params-panel.spec.tsx | 2 +- .../model-selector/tts-params-panel.tsx | 2 +- .../plugin-detail-panel/strategy-detail.tsx | 4 +- .../plugin-detail-panel/strategy-item.tsx | 4 +- .../__tests__/index.spec.tsx | 4 +- .../__tests__/plugins-panel.spec.tsx | 4 +- .../components/error-plugin-item.tsx | 2 +- .../plugin-tasks/components/plugin-item.tsx | 2 +- .../components/plugin-section.tsx | 2 +- .../plugin-page/plugins-panel-utils.ts | 2 +- .../plugins/plugin-page/plugins-panel.tsx | 2 +- .../__tests__/tool-item.spec.tsx | 4 +- .../auto-update-setting/tool-item.tsx | 2 +- web/app/components/plugins/types.ts | 4 +- web/app/components/provider/i18n-server.tsx | 2 +- web/app/components/provider/i18n.tsx | 4 +- .../use-text-generation-app-state.spec.ts | 2 +- .../hooks/use-text-generation-app-state.ts | 2 +- .../edit-custom-collection-modal/test-api.tsx | 2 +- .../components/tools/mcp/detail/tool-item.tsx | 2 +- .../__tests__/custom-create-card.spec.tsx | 4 +- .../tools/provider/__tests__/detail.spec.tsx | 4 +- .../provider/__tests__/tool-item.spec.tsx | 4 +- web/app/components/tools/provider/detail.tsx | 2 +- .../components/tools/provider/tool-item.tsx | 2 +- .../block-selector/featured-tools.tsx | 2 +- .../block-selector/featured-triggers.tsx | 2 +- .../workflow/nodes/agent/default.ts | 2 +- .../__tests__/integration.spec.tsx | 2 +- .../__tests__/panel.spec.tsx | 2 +- .../nodes/document-extractor/panel.tsx | 2 +- .../workflow/nodes/knowledge-base/node.tsx | 2 +- web/app/device/_header.tsx | 4 +- web/app/device/utils/error-copy.ts | 2 +- web/app/layout.tsx | 2 +- web/app/route-metadata.ts | 4 +- web/app/signin/__tests__/_header.spec.tsx | 4 +- web/app/signin/_header.tsx | 4 +- .../invite-settings/__tests__/page.spec.tsx | 4 +- web/app/signin/invite-settings/page.tsx | 8 +- web/app/signin/one-more-step.tsx | 2 +- web/context/i18n.spec.ts | 8 +- web/context/i18n.ts | 6 +- .../agent-detail/access-config/page.tsx | 2 +- .../__tests__/delete-agent-dialog.spec.tsx | 2 +- web/features/home/page.tsx | 2 +- .../__tests__/crawl-selection-form.spec.tsx | 4 +- .../new-rag/__tests__/sources-page.spec.tsx | 2 +- web/hooks/use-format-time-from-now.ts | 2 +- web/hooks/use-i18n.ts | 2 +- web/{i18n-config => i18n}/README.md | 8 +- .../__tests__/client-locale.spec.ts | 0 .../__tests__/load-resource.spec.ts | 0 .../__tests__/metadata.spec.ts | 0 .../__tests__/plural-selector.spec.ts | 2 +- .../__tests__/server.spec.ts | 0 web/{i18n-config => i18n}/client.ts | 0 web/{i18n-config => i18n}/index.ts | 0 web/{i18n-config => i18n}/language.ts | 0 web/{i18n-config => i18n}/languages.ts | 0 web/{i18n-config => i18n}/lib.client.ts | 0 web/{i18n-config => i18n}/lib.server.ts | 0 web/{i18n-config => i18n}/load-resource.ts | 0 .../locale-resources/ar-TN.ts | 2 +- .../locale-resources/az-AZ.ts | 2 +- .../locale-resources/de-DE.ts | 2 +- .../locale-resources/en-US.ts | 2 +- .../locale-resources/es-ES.ts | 2 +- .../locale-resources/fa-IR.ts | 2 +- .../locale-resources/fr-FR.ts | 2 +- .../locale-resources/hi-IN.ts | 2 +- .../locale-resources/id-ID.ts | 2 +- .../locale-resources/it-IT.ts | 2 +- .../locale-resources/ja-JP.ts | 2 +- .../locale-resources/ko-KR.ts | 2 +- .../locale-resources/lo-LA.ts | 2 +- .../locale-resources/nl-NL.ts | 2 +- .../locale-resources/pl-PL.ts | 2 +- .../locale-resources/pt-BR.ts | 2 +- .../locale-resources/ro-RO.ts | 2 +- .../locale-resources/ru-RU.ts | 2 +- .../locale-resources/sl-SI.ts | 2 +- .../locale-resources/th-TH.ts | 2 +- .../locale-resources/tr-TR.ts | 2 +- .../locale-resources/uk-UA.ts | 2 +- .../locale-resources/vi-VN.ts | 2 +- .../locale-resources/zh-Hans.ts | 2 +- .../locale-resources/zh-Hant.ts | 2 +- web/{i18n-config => i18n}/locale.ts | 0 web/i18n/{ => locales}/ar-TN/agent-v-2.json | 0 .../{ => locales}/ar-TN/app-annotation.json | 0 web/i18n/{ => locales}/ar-TN/app-api.json | 0 web/i18n/{ => locales}/ar-TN/app-debug.json | 0 web/i18n/{ => locales}/ar-TN/app-log.json | 0 .../{ => locales}/ar-TN/app-overview.json | 0 web/i18n/{ => locales}/ar-TN/app.json | 0 web/i18n/{ => locales}/ar-TN/billing.json | 0 web/i18n/{ => locales}/ar-TN/common.json | 0 web/i18n/{ => locales}/ar-TN/custom.json | 0 .../{ => locales}/ar-TN/dataset-creation.json | 0 .../ar-TN/dataset-documents.json | 0 .../ar-TN/dataset-hit-testing.json | 0 .../{ => locales}/ar-TN/dataset-pipeline.json | 0 .../{ => locales}/ar-TN/dataset-settings.json | 0 web/i18n/{ => locales}/ar-TN/dataset.json | 0 web/i18n/{ => locales}/ar-TN/deployments.json | 0 web/i18n/{ => locales}/ar-TN/device-flow.json | 0 web/i18n/{ => locales}/ar-TN/education.json | 0 web/i18n/{ => locales}/ar-TN/explore.json | 0 web/i18n/{ => locales}/ar-TN/layout.json | 0 web/i18n/{ => locales}/ar-TN/login.json | 0 web/i18n/{ => locales}/ar-TN/oauth.json | 0 .../{ => locales}/ar-TN/permission-keys.json | 0 web/i18n/{ => locales}/ar-TN/permission.json | 0 web/i18n/{ => locales}/ar-TN/pipeline.json | 0 web/i18n/{ => locales}/ar-TN/plugin-tags.json | 0 .../{ => locales}/ar-TN/plugin-trigger.json | 0 web/i18n/{ => locales}/ar-TN/plugin.json | 0 web/i18n/{ => locales}/ar-TN/register.json | 0 web/i18n/{ => locales}/ar-TN/run-log.json | 0 web/i18n/{ => locales}/ar-TN/share.json | 0 web/i18n/{ => locales}/ar-TN/skill.json | 0 web/i18n/{ => locales}/ar-TN/snippet.json | 0 web/i18n/{ => locales}/ar-TN/time.json | 0 web/i18n/{ => locales}/ar-TN/tools.json | 0 web/i18n/{ => locales}/ar-TN/workflow.json | 0 web/i18n/{ => locales}/az-AZ/agent-v-2.json | 0 .../{ => locales}/az-AZ/app-annotation.json | 0 web/i18n/{ => locales}/az-AZ/app-api.json | 0 web/i18n/{ => locales}/az-AZ/app-debug.json | 0 web/i18n/{ => locales}/az-AZ/app-log.json | 0 .../{ => locales}/az-AZ/app-overview.json | 0 web/i18n/{ => locales}/az-AZ/app.json | 0 web/i18n/{ => locales}/az-AZ/billing.json | 0 web/i18n/{ => locales}/az-AZ/common.json | 0 web/i18n/{ => locales}/az-AZ/custom.json | 0 .../{ => locales}/az-AZ/dataset-creation.json | 0 .../az-AZ/dataset-documents.json | 0 .../az-AZ/dataset-hit-testing.json | 0 .../{ => locales}/az-AZ/dataset-pipeline.json | 0 .../{ => locales}/az-AZ/dataset-settings.json | 0 web/i18n/{ => locales}/az-AZ/dataset.json | 0 web/i18n/{ => locales}/az-AZ/deployments.json | 0 web/i18n/{ => locales}/az-AZ/device-flow.json | 0 web/i18n/{ => locales}/az-AZ/education.json | 0 web/i18n/{ => locales}/az-AZ/explore.json | 0 web/i18n/{ => locales}/az-AZ/layout.json | 0 web/i18n/{ => locales}/az-AZ/login.json | 0 web/i18n/{ => locales}/az-AZ/oauth.json | 0 .../{ => locales}/az-AZ/permission-keys.json | 0 web/i18n/{ => locales}/az-AZ/permission.json | 0 web/i18n/{ => locales}/az-AZ/pipeline.json | 0 web/i18n/{ => locales}/az-AZ/plugin-tags.json | 0 .../{ => locales}/az-AZ/plugin-trigger.json | 0 web/i18n/{ => locales}/az-AZ/plugin.json | 0 web/i18n/{ => locales}/az-AZ/register.json | 0 web/i18n/{ => locales}/az-AZ/run-log.json | 0 web/i18n/{ => locales}/az-AZ/share.json | 0 web/i18n/{ => locales}/az-AZ/skill.json | 0 web/i18n/{ => locales}/az-AZ/snippet.json | 0 web/i18n/{ => locales}/az-AZ/time.json | 0 web/i18n/{ => locales}/az-AZ/tools.json | 0 web/i18n/{ => locales}/az-AZ/workflow.json | 0 web/i18n/{ => locales}/de-DE/agent-v-2.json | 0 .../{ => locales}/de-DE/app-annotation.json | 0 web/i18n/{ => locales}/de-DE/app-api.json | 0 web/i18n/{ => locales}/de-DE/app-debug.json | 0 web/i18n/{ => locales}/de-DE/app-log.json | 0 .../{ => locales}/de-DE/app-overview.json | 0 web/i18n/{ => locales}/de-DE/app.json | 0 web/i18n/{ => locales}/de-DE/billing.json | 0 web/i18n/{ => locales}/de-DE/common.json | 0 web/i18n/{ => locales}/de-DE/custom.json | 0 .../{ => locales}/de-DE/dataset-creation.json | 0 .../de-DE/dataset-documents.json | 0 .../de-DE/dataset-hit-testing.json | 0 .../{ => locales}/de-DE/dataset-pipeline.json | 0 .../{ => locales}/de-DE/dataset-settings.json | 0 web/i18n/{ => locales}/de-DE/dataset.json | 0 web/i18n/{ => locales}/de-DE/deployments.json | 0 web/i18n/{ => locales}/de-DE/device-flow.json | 0 web/i18n/{ => locales}/de-DE/education.json | 0 web/i18n/{ => locales}/de-DE/explore.json | 0 web/i18n/{ => locales}/de-DE/layout.json | 0 web/i18n/{ => locales}/de-DE/login.json | 0 web/i18n/{ => locales}/de-DE/oauth.json | 0 .../{ => locales}/de-DE/permission-keys.json | 0 web/i18n/{ => locales}/de-DE/permission.json | 0 web/i18n/{ => locales}/de-DE/pipeline.json | 0 web/i18n/{ => locales}/de-DE/plugin-tags.json | 0 .../{ => locales}/de-DE/plugin-trigger.json | 0 web/i18n/{ => locales}/de-DE/plugin.json | 0 web/i18n/{ => locales}/de-DE/register.json | 0 web/i18n/{ => locales}/de-DE/run-log.json | 0 web/i18n/{ => locales}/de-DE/share.json | 0 web/i18n/{ => locales}/de-DE/skill.json | 0 web/i18n/{ => locales}/de-DE/snippet.json | 0 web/i18n/{ => locales}/de-DE/time.json | 0 web/i18n/{ => locales}/de-DE/tools.json | 0 web/i18n/{ => locales}/de-DE/workflow.json | 0 web/i18n/{ => locales}/en-US/agent-v-2.json | 0 .../{ => locales}/en-US/app-annotation.json | 0 web/i18n/{ => locales}/en-US/app-api.json | 0 web/i18n/{ => locales}/en-US/app-debug.json | 0 web/i18n/{ => locales}/en-US/app-log.json | 0 .../{ => locales}/en-US/app-overview.json | 0 web/i18n/{ => locales}/en-US/app.json | 0 web/i18n/{ => locales}/en-US/billing.json | 0 web/i18n/{ => locales}/en-US/common.json | 0 web/i18n/{ => locales}/en-US/custom.json | 0 .../{ => locales}/en-US/dataset-creation.json | 0 .../en-US/dataset-documents.json | 0 .../en-US/dataset-hit-testing.json | 0 .../{ => locales}/en-US/dataset-pipeline.json | 0 .../{ => locales}/en-US/dataset-settings.json | 0 web/i18n/{ => locales}/en-US/dataset.json | 0 web/i18n/{ => locales}/en-US/deployments.json | 0 web/i18n/{ => locales}/en-US/device-flow.json | 0 web/i18n/{ => locales}/en-US/education.json | 0 web/i18n/{ => locales}/en-US/explore.json | 0 web/i18n/{ => locales}/en-US/layout.json | 0 web/i18n/{ => locales}/en-US/login.json | 0 web/i18n/{ => locales}/en-US/oauth.json | 0 .../{ => locales}/en-US/permission-keys.json | 0 web/i18n/{ => locales}/en-US/permission.json | 0 web/i18n/{ => locales}/en-US/pipeline.json | 0 web/i18n/{ => locales}/en-US/plugin-tags.json | 0 .../{ => locales}/en-US/plugin-trigger.json | 0 web/i18n/{ => locales}/en-US/plugin.json | 0 web/i18n/{ => locales}/en-US/register.json | 0 web/i18n/{ => locales}/en-US/run-log.json | 0 web/i18n/{ => locales}/en-US/share.json | 0 web/i18n/{ => locales}/en-US/skill.json | 0 web/i18n/{ => locales}/en-US/snippet.json | 0 web/i18n/{ => locales}/en-US/time.json | 0 web/i18n/{ => locales}/en-US/tools.json | 0 web/i18n/{ => locales}/en-US/workflow.json | 0 web/i18n/{ => locales}/es-ES/agent-v-2.json | 0 .../{ => locales}/es-ES/app-annotation.json | 0 web/i18n/{ => locales}/es-ES/app-api.json | 0 web/i18n/{ => locales}/es-ES/app-debug.json | 0 web/i18n/{ => locales}/es-ES/app-log.json | 0 .../{ => locales}/es-ES/app-overview.json | 0 web/i18n/{ => locales}/es-ES/app.json | 0 web/i18n/{ => locales}/es-ES/billing.json | 0 web/i18n/{ => locales}/es-ES/common.json | 0 web/i18n/{ => locales}/es-ES/custom.json | 0 .../{ => locales}/es-ES/dataset-creation.json | 0 .../es-ES/dataset-documents.json | 0 .../es-ES/dataset-hit-testing.json | 0 .../{ => locales}/es-ES/dataset-pipeline.json | 0 .../{ => locales}/es-ES/dataset-settings.json | 0 web/i18n/{ => locales}/es-ES/dataset.json | 0 web/i18n/{ => locales}/es-ES/deployments.json | 0 web/i18n/{ => locales}/es-ES/device-flow.json | 0 web/i18n/{ => locales}/es-ES/education.json | 0 web/i18n/{ => locales}/es-ES/explore.json | 0 web/i18n/{ => locales}/es-ES/layout.json | 0 web/i18n/{ => locales}/es-ES/login.json | 0 web/i18n/{ => locales}/es-ES/oauth.json | 0 .../{ => locales}/es-ES/permission-keys.json | 0 web/i18n/{ => locales}/es-ES/permission.json | 0 web/i18n/{ => locales}/es-ES/pipeline.json | 0 web/i18n/{ => locales}/es-ES/plugin-tags.json | 0 .../{ => locales}/es-ES/plugin-trigger.json | 0 web/i18n/{ => locales}/es-ES/plugin.json | 0 web/i18n/{ => locales}/es-ES/register.json | 0 web/i18n/{ => locales}/es-ES/run-log.json | 0 web/i18n/{ => locales}/es-ES/share.json | 0 web/i18n/{ => locales}/es-ES/skill.json | 0 web/i18n/{ => locales}/es-ES/snippet.json | 0 web/i18n/{ => locales}/es-ES/time.json | 0 web/i18n/{ => locales}/es-ES/tools.json | 0 web/i18n/{ => locales}/es-ES/workflow.json | 0 web/i18n/{ => locales}/fa-IR/agent-v-2.json | 0 .../{ => locales}/fa-IR/app-annotation.json | 0 web/i18n/{ => locales}/fa-IR/app-api.json | 0 web/i18n/{ => locales}/fa-IR/app-debug.json | 0 web/i18n/{ => locales}/fa-IR/app-log.json | 0 .../{ => locales}/fa-IR/app-overview.json | 0 web/i18n/{ => locales}/fa-IR/app.json | 0 web/i18n/{ => locales}/fa-IR/billing.json | 0 web/i18n/{ => locales}/fa-IR/common.json | 0 web/i18n/{ => locales}/fa-IR/custom.json | 0 .../{ => locales}/fa-IR/dataset-creation.json | 0 .../fa-IR/dataset-documents.json | 0 .../fa-IR/dataset-hit-testing.json | 0 .../{ => locales}/fa-IR/dataset-pipeline.json | 0 .../{ => locales}/fa-IR/dataset-settings.json | 0 web/i18n/{ => locales}/fa-IR/dataset.json | 0 web/i18n/{ => locales}/fa-IR/deployments.json | 0 web/i18n/{ => locales}/fa-IR/device-flow.json | 0 web/i18n/{ => locales}/fa-IR/education.json | 0 web/i18n/{ => locales}/fa-IR/explore.json | 0 web/i18n/{ => locales}/fa-IR/layout.json | 0 web/i18n/{ => locales}/fa-IR/login.json | 0 web/i18n/{ => locales}/fa-IR/oauth.json | 0 .../{ => locales}/fa-IR/permission-keys.json | 0 web/i18n/{ => locales}/fa-IR/permission.json | 0 web/i18n/{ => locales}/fa-IR/pipeline.json | 0 web/i18n/{ => locales}/fa-IR/plugin-tags.json | 0 .../{ => locales}/fa-IR/plugin-trigger.json | 0 web/i18n/{ => locales}/fa-IR/plugin.json | 0 web/i18n/{ => locales}/fa-IR/register.json | 0 web/i18n/{ => locales}/fa-IR/run-log.json | 0 web/i18n/{ => locales}/fa-IR/share.json | 0 web/i18n/{ => locales}/fa-IR/skill.json | 0 web/i18n/{ => locales}/fa-IR/snippet.json | 0 web/i18n/{ => locales}/fa-IR/time.json | 0 web/i18n/{ => locales}/fa-IR/tools.json | 0 web/i18n/{ => locales}/fa-IR/workflow.json | 0 web/i18n/{ => locales}/fr-FR/agent-v-2.json | 0 .../{ => locales}/fr-FR/app-annotation.json | 0 web/i18n/{ => locales}/fr-FR/app-api.json | 0 web/i18n/{ => locales}/fr-FR/app-debug.json | 0 web/i18n/{ => locales}/fr-FR/app-log.json | 0 .../{ => locales}/fr-FR/app-overview.json | 0 web/i18n/{ => locales}/fr-FR/app.json | 0 web/i18n/{ => locales}/fr-FR/billing.json | 0 web/i18n/{ => locales}/fr-FR/common.json | 0 web/i18n/{ => locales}/fr-FR/custom.json | 0 .../{ => locales}/fr-FR/dataset-creation.json | 0 .../fr-FR/dataset-documents.json | 0 .../fr-FR/dataset-hit-testing.json | 0 .../{ => locales}/fr-FR/dataset-pipeline.json | 0 .../{ => locales}/fr-FR/dataset-settings.json | 0 web/i18n/{ => locales}/fr-FR/dataset.json | 0 web/i18n/{ => locales}/fr-FR/deployments.json | 0 web/i18n/{ => locales}/fr-FR/device-flow.json | 0 web/i18n/{ => locales}/fr-FR/education.json | 0 web/i18n/{ => locales}/fr-FR/explore.json | 0 web/i18n/{ => locales}/fr-FR/layout.json | 0 web/i18n/{ => locales}/fr-FR/login.json | 0 web/i18n/{ => locales}/fr-FR/oauth.json | 0 .../{ => locales}/fr-FR/permission-keys.json | 0 web/i18n/{ => locales}/fr-FR/permission.json | 0 web/i18n/{ => locales}/fr-FR/pipeline.json | 0 web/i18n/{ => locales}/fr-FR/plugin-tags.json | 0 .../{ => locales}/fr-FR/plugin-trigger.json | 0 web/i18n/{ => locales}/fr-FR/plugin.json | 0 web/i18n/{ => locales}/fr-FR/register.json | 0 web/i18n/{ => locales}/fr-FR/run-log.json | 0 web/i18n/{ => locales}/fr-FR/share.json | 0 web/i18n/{ => locales}/fr-FR/skill.json | 0 web/i18n/{ => locales}/fr-FR/snippet.json | 0 web/i18n/{ => locales}/fr-FR/time.json | 0 web/i18n/{ => locales}/fr-FR/tools.json | 0 web/i18n/{ => locales}/fr-FR/workflow.json | 0 web/i18n/{ => locales}/hi-IN/agent-v-2.json | 0 .../{ => locales}/hi-IN/app-annotation.json | 0 web/i18n/{ => locales}/hi-IN/app-api.json | 0 web/i18n/{ => locales}/hi-IN/app-debug.json | 0 web/i18n/{ => locales}/hi-IN/app-log.json | 0 .../{ => locales}/hi-IN/app-overview.json | 0 web/i18n/{ => locales}/hi-IN/app.json | 0 web/i18n/{ => locales}/hi-IN/billing.json | 0 web/i18n/{ => locales}/hi-IN/common.json | 0 web/i18n/{ => locales}/hi-IN/custom.json | 0 .../{ => locales}/hi-IN/dataset-creation.json | 0 .../hi-IN/dataset-documents.json | 0 .../hi-IN/dataset-hit-testing.json | 0 .../{ => locales}/hi-IN/dataset-pipeline.json | 0 .../{ => locales}/hi-IN/dataset-settings.json | 0 web/i18n/{ => locales}/hi-IN/dataset.json | 0 web/i18n/{ => locales}/hi-IN/deployments.json | 0 web/i18n/{ => locales}/hi-IN/device-flow.json | 0 web/i18n/{ => locales}/hi-IN/education.json | 0 web/i18n/{ => locales}/hi-IN/explore.json | 0 web/i18n/{ => locales}/hi-IN/layout.json | 0 web/i18n/{ => locales}/hi-IN/login.json | 0 web/i18n/{ => locales}/hi-IN/oauth.json | 0 .../{ => locales}/hi-IN/permission-keys.json | 0 web/i18n/{ => locales}/hi-IN/permission.json | 0 web/i18n/{ => locales}/hi-IN/pipeline.json | 0 web/i18n/{ => locales}/hi-IN/plugin-tags.json | 0 .../{ => locales}/hi-IN/plugin-trigger.json | 0 web/i18n/{ => locales}/hi-IN/plugin.json | 0 web/i18n/{ => locales}/hi-IN/register.json | 0 web/i18n/{ => locales}/hi-IN/run-log.json | 0 web/i18n/{ => locales}/hi-IN/share.json | 0 web/i18n/{ => locales}/hi-IN/skill.json | 0 web/i18n/{ => locales}/hi-IN/snippet.json | 0 web/i18n/{ => locales}/hi-IN/time.json | 0 web/i18n/{ => locales}/hi-IN/tools.json | 0 web/i18n/{ => locales}/hi-IN/workflow.json | 0 web/i18n/{ => locales}/id-ID/agent-v-2.json | 0 .../{ => locales}/id-ID/app-annotation.json | 0 web/i18n/{ => locales}/id-ID/app-api.json | 0 web/i18n/{ => locales}/id-ID/app-debug.json | 0 web/i18n/{ => locales}/id-ID/app-log.json | 0 .../{ => locales}/id-ID/app-overview.json | 0 web/i18n/{ => locales}/id-ID/app.json | 0 web/i18n/{ => locales}/id-ID/billing.json | 0 web/i18n/{ => locales}/id-ID/common.json | 0 web/i18n/{ => locales}/id-ID/custom.json | 0 .../{ => locales}/id-ID/dataset-creation.json | 0 .../id-ID/dataset-documents.json | 0 .../id-ID/dataset-hit-testing.json | 0 .../{ => locales}/id-ID/dataset-pipeline.json | 0 .../{ => locales}/id-ID/dataset-settings.json | 0 web/i18n/{ => locales}/id-ID/dataset.json | 0 web/i18n/{ => locales}/id-ID/deployments.json | 0 web/i18n/{ => locales}/id-ID/device-flow.json | 0 web/i18n/{ => locales}/id-ID/education.json | 0 web/i18n/{ => locales}/id-ID/explore.json | 0 web/i18n/{ => locales}/id-ID/layout.json | 0 web/i18n/{ => locales}/id-ID/login.json | 0 web/i18n/{ => locales}/id-ID/oauth.json | 0 .../{ => locales}/id-ID/permission-keys.json | 0 web/i18n/{ => locales}/id-ID/permission.json | 0 web/i18n/{ => locales}/id-ID/pipeline.json | 0 web/i18n/{ => locales}/id-ID/plugin-tags.json | 0 .../{ => locales}/id-ID/plugin-trigger.json | 0 web/i18n/{ => locales}/id-ID/plugin.json | 0 web/i18n/{ => locales}/id-ID/register.json | 0 web/i18n/{ => locales}/id-ID/run-log.json | 0 web/i18n/{ => locales}/id-ID/share.json | 0 web/i18n/{ => locales}/id-ID/skill.json | 0 web/i18n/{ => locales}/id-ID/snippet.json | 0 web/i18n/{ => locales}/id-ID/time.json | 0 web/i18n/{ => locales}/id-ID/tools.json | 0 web/i18n/{ => locales}/id-ID/workflow.json | 0 web/i18n/{ => locales}/it-IT/agent-v-2.json | 0 .../{ => locales}/it-IT/app-annotation.json | 0 web/i18n/{ => locales}/it-IT/app-api.json | 0 web/i18n/{ => locales}/it-IT/app-debug.json | 0 web/i18n/{ => locales}/it-IT/app-log.json | 0 .../{ => locales}/it-IT/app-overview.json | 0 web/i18n/{ => locales}/it-IT/app.json | 0 web/i18n/{ => locales}/it-IT/billing.json | 0 web/i18n/{ => locales}/it-IT/common.json | 0 web/i18n/{ => locales}/it-IT/custom.json | 0 .../{ => locales}/it-IT/dataset-creation.json | 0 .../it-IT/dataset-documents.json | 0 .../it-IT/dataset-hit-testing.json | 0 .../{ => locales}/it-IT/dataset-pipeline.json | 0 .../{ => locales}/it-IT/dataset-settings.json | 0 web/i18n/{ => locales}/it-IT/dataset.json | 0 web/i18n/{ => locales}/it-IT/deployments.json | 0 web/i18n/{ => locales}/it-IT/device-flow.json | 0 web/i18n/{ => locales}/it-IT/education.json | 0 web/i18n/{ => locales}/it-IT/explore.json | 0 web/i18n/{ => locales}/it-IT/layout.json | 0 web/i18n/{ => locales}/it-IT/login.json | 0 web/i18n/{ => locales}/it-IT/oauth.json | 0 .../{ => locales}/it-IT/permission-keys.json | 0 web/i18n/{ => locales}/it-IT/permission.json | 0 web/i18n/{ => locales}/it-IT/pipeline.json | 0 web/i18n/{ => locales}/it-IT/plugin-tags.json | 0 .../{ => locales}/it-IT/plugin-trigger.json | 0 web/i18n/{ => locales}/it-IT/plugin.json | 0 web/i18n/{ => locales}/it-IT/register.json | 0 web/i18n/{ => locales}/it-IT/run-log.json | 0 web/i18n/{ => locales}/it-IT/share.json | 0 web/i18n/{ => locales}/it-IT/skill.json | 0 web/i18n/{ => locales}/it-IT/snippet.json | 0 web/i18n/{ => locales}/it-IT/time.json | 0 web/i18n/{ => locales}/it-IT/tools.json | 0 web/i18n/{ => locales}/it-IT/workflow.json | 0 web/i18n/{ => locales}/ja-JP/agent-v-2.json | 0 .../{ => locales}/ja-JP/app-annotation.json | 0 web/i18n/{ => locales}/ja-JP/app-api.json | 0 web/i18n/{ => locales}/ja-JP/app-debug.json | 0 web/i18n/{ => locales}/ja-JP/app-log.json | 0 .../{ => locales}/ja-JP/app-overview.json | 0 web/i18n/{ => locales}/ja-JP/app.json | 0 web/i18n/{ => locales}/ja-JP/billing.json | 0 web/i18n/{ => locales}/ja-JP/common.json | 0 web/i18n/{ => locales}/ja-JP/custom.json | 0 .../{ => locales}/ja-JP/dataset-creation.json | 0 .../ja-JP/dataset-documents.json | 0 .../ja-JP/dataset-hit-testing.json | 0 .../{ => locales}/ja-JP/dataset-pipeline.json | 0 .../{ => locales}/ja-JP/dataset-settings.json | 0 web/i18n/{ => locales}/ja-JP/dataset.json | 0 web/i18n/{ => locales}/ja-JP/deployments.json | 0 web/i18n/{ => locales}/ja-JP/device-flow.json | 0 web/i18n/{ => locales}/ja-JP/education.json | 0 web/i18n/{ => locales}/ja-JP/explore.json | 0 web/i18n/{ => locales}/ja-JP/layout.json | 0 web/i18n/{ => locales}/ja-JP/login.json | 0 web/i18n/{ => locales}/ja-JP/oauth.json | 0 .../{ => locales}/ja-JP/permission-keys.json | 0 web/i18n/{ => locales}/ja-JP/permission.json | 0 web/i18n/{ => locales}/ja-JP/pipeline.json | 0 web/i18n/{ => locales}/ja-JP/plugin-tags.json | 0 .../{ => locales}/ja-JP/plugin-trigger.json | 0 web/i18n/{ => locales}/ja-JP/plugin.json | 0 web/i18n/{ => locales}/ja-JP/register.json | 0 web/i18n/{ => locales}/ja-JP/run-log.json | 0 web/i18n/{ => locales}/ja-JP/share.json | 0 web/i18n/{ => locales}/ja-JP/skill.json | 0 web/i18n/{ => locales}/ja-JP/snippet.json | 0 web/i18n/{ => locales}/ja-JP/time.json | 0 web/i18n/{ => locales}/ja-JP/tools.json | 0 web/i18n/{ => locales}/ja-JP/workflow.json | 0 web/i18n/{ => locales}/ko-KR/agent-v-2.json | 0 .../{ => locales}/ko-KR/app-annotation.json | 0 web/i18n/{ => locales}/ko-KR/app-api.json | 0 web/i18n/{ => locales}/ko-KR/app-debug.json | 0 web/i18n/{ => locales}/ko-KR/app-log.json | 0 .../{ => locales}/ko-KR/app-overview.json | 0 web/i18n/{ => locales}/ko-KR/app.json | 0 web/i18n/{ => locales}/ko-KR/billing.json | 0 web/i18n/{ => locales}/ko-KR/common.json | 0 web/i18n/{ => locales}/ko-KR/custom.json | 0 .../{ => locales}/ko-KR/dataset-creation.json | 0 .../ko-KR/dataset-documents.json | 0 .../ko-KR/dataset-hit-testing.json | 0 .../{ => locales}/ko-KR/dataset-pipeline.json | 0 .../{ => locales}/ko-KR/dataset-settings.json | 0 web/i18n/{ => locales}/ko-KR/dataset.json | 0 web/i18n/{ => locales}/ko-KR/deployments.json | 0 web/i18n/{ => locales}/ko-KR/device-flow.json | 0 web/i18n/{ => locales}/ko-KR/education.json | 0 web/i18n/{ => locales}/ko-KR/explore.json | 0 web/i18n/{ => locales}/ko-KR/layout.json | 0 web/i18n/{ => locales}/ko-KR/login.json | 0 web/i18n/{ => locales}/ko-KR/oauth.json | 0 .../{ => locales}/ko-KR/permission-keys.json | 0 web/i18n/{ => locales}/ko-KR/permission.json | 0 web/i18n/{ => locales}/ko-KR/pipeline.json | 0 web/i18n/{ => locales}/ko-KR/plugin-tags.json | 0 .../{ => locales}/ko-KR/plugin-trigger.json | 0 web/i18n/{ => locales}/ko-KR/plugin.json | 0 web/i18n/{ => locales}/ko-KR/register.json | 0 web/i18n/{ => locales}/ko-KR/run-log.json | 0 web/i18n/{ => locales}/ko-KR/share.json | 0 web/i18n/{ => locales}/ko-KR/skill.json | 0 web/i18n/{ => locales}/ko-KR/snippet.json | 0 web/i18n/{ => locales}/ko-KR/time.json | 0 web/i18n/{ => locales}/ko-KR/tools.json | 0 web/i18n/{ => locales}/ko-KR/workflow.json | 0 web/i18n/{ => locales}/lo-LA/agent-v-2.json | 0 .../{ => locales}/lo-LA/app-annotation.json | 0 web/i18n/{ => locales}/lo-LA/app-api.json | 0 web/i18n/{ => locales}/lo-LA/app-debug.json | 0 web/i18n/{ => locales}/lo-LA/app-log.json | 0 .../{ => locales}/lo-LA/app-overview.json | 0 web/i18n/{ => locales}/lo-LA/app.json | 0 web/i18n/{ => locales}/lo-LA/billing.json | 0 web/i18n/{ => locales}/lo-LA/common.json | 0 web/i18n/{ => locales}/lo-LA/custom.json | 0 .../{ => locales}/lo-LA/dataset-creation.json | 0 .../lo-LA/dataset-documents.json | 0 .../lo-LA/dataset-hit-testing.json | 0 .../{ => locales}/lo-LA/dataset-pipeline.json | 0 .../{ => locales}/lo-LA/dataset-settings.json | 0 web/i18n/{ => locales}/lo-LA/dataset.json | 0 web/i18n/{ => locales}/lo-LA/deployments.json | 0 web/i18n/{ => locales}/lo-LA/device-flow.json | 0 web/i18n/{ => locales}/lo-LA/education.json | 0 web/i18n/{ => locales}/lo-LA/explore.json | 0 web/i18n/{ => locales}/lo-LA/layout.json | 0 web/i18n/{ => locales}/lo-LA/login.json | 0 web/i18n/{ => locales}/lo-LA/oauth.json | 0 .../{ => locales}/lo-LA/permission-keys.json | 0 web/i18n/{ => locales}/lo-LA/permission.json | 0 web/i18n/{ => locales}/lo-LA/pipeline.json | 0 web/i18n/{ => locales}/lo-LA/plugin-tags.json | 0 .../{ => locales}/lo-LA/plugin-trigger.json | 0 web/i18n/{ => locales}/lo-LA/plugin.json | 0 web/i18n/{ => locales}/lo-LA/register.json | 0 web/i18n/{ => locales}/lo-LA/run-log.json | 0 web/i18n/{ => locales}/lo-LA/share.json | 0 web/i18n/{ => locales}/lo-LA/skill.json | 0 web/i18n/{ => locales}/lo-LA/snippet.json | 0 web/i18n/{ => locales}/lo-LA/time.json | 0 web/i18n/{ => locales}/lo-LA/tools.json | 0 web/i18n/{ => locales}/lo-LA/workflow.json | 0 web/i18n/{ => locales}/nl-NL/agent-v-2.json | 0 .../{ => locales}/nl-NL/app-annotation.json | 0 web/i18n/{ => locales}/nl-NL/app-api.json | 0 web/i18n/{ => locales}/nl-NL/app-debug.json | 0 web/i18n/{ => locales}/nl-NL/app-log.json | 0 .../{ => locales}/nl-NL/app-overview.json | 0 web/i18n/{ => locales}/nl-NL/app.json | 0 web/i18n/{ => locales}/nl-NL/billing.json | 0 web/i18n/{ => locales}/nl-NL/common.json | 0 web/i18n/{ => locales}/nl-NL/custom.json | 0 .../{ => locales}/nl-NL/dataset-creation.json | 0 .../nl-NL/dataset-documents.json | 0 .../nl-NL/dataset-hit-testing.json | 0 .../{ => locales}/nl-NL/dataset-pipeline.json | 0 .../{ => locales}/nl-NL/dataset-settings.json | 0 web/i18n/{ => locales}/nl-NL/dataset.json | 0 web/i18n/{ => locales}/nl-NL/deployments.json | 0 web/i18n/{ => locales}/nl-NL/device-flow.json | 0 web/i18n/{ => locales}/nl-NL/education.json | 0 web/i18n/{ => locales}/nl-NL/explore.json | 0 web/i18n/{ => locales}/nl-NL/layout.json | 0 web/i18n/{ => locales}/nl-NL/login.json | 0 web/i18n/{ => locales}/nl-NL/oauth.json | 0 .../{ => locales}/nl-NL/permission-keys.json | 0 web/i18n/{ => locales}/nl-NL/permission.json | 0 web/i18n/{ => locales}/nl-NL/pipeline.json | 0 web/i18n/{ => locales}/nl-NL/plugin-tags.json | 0 .../{ => locales}/nl-NL/plugin-trigger.json | 0 web/i18n/{ => locales}/nl-NL/plugin.json | 0 web/i18n/{ => locales}/nl-NL/register.json | 0 web/i18n/{ => locales}/nl-NL/run-log.json | 0 web/i18n/{ => locales}/nl-NL/share.json | 0 web/i18n/{ => locales}/nl-NL/skill.json | 0 web/i18n/{ => locales}/nl-NL/snippet.json | 0 web/i18n/{ => locales}/nl-NL/time.json | 0 web/i18n/{ => locales}/nl-NL/tools.json | 0 web/i18n/{ => locales}/nl-NL/workflow.json | 0 web/i18n/{ => locales}/pl-PL/agent-v-2.json | 0 .../{ => locales}/pl-PL/app-annotation.json | 0 web/i18n/{ => locales}/pl-PL/app-api.json | 0 web/i18n/{ => locales}/pl-PL/app-debug.json | 0 web/i18n/{ => locales}/pl-PL/app-log.json | 0 .../{ => locales}/pl-PL/app-overview.json | 0 web/i18n/{ => locales}/pl-PL/app.json | 0 web/i18n/{ => locales}/pl-PL/billing.json | 0 web/i18n/{ => locales}/pl-PL/common.json | 0 web/i18n/{ => locales}/pl-PL/custom.json | 0 .../{ => locales}/pl-PL/dataset-creation.json | 0 .../pl-PL/dataset-documents.json | 0 .../pl-PL/dataset-hit-testing.json | 0 .../{ => locales}/pl-PL/dataset-pipeline.json | 0 .../{ => locales}/pl-PL/dataset-settings.json | 0 web/i18n/{ => locales}/pl-PL/dataset.json | 0 web/i18n/{ => locales}/pl-PL/deployments.json | 0 web/i18n/{ => locales}/pl-PL/device-flow.json | 0 web/i18n/{ => locales}/pl-PL/education.json | 0 web/i18n/{ => locales}/pl-PL/explore.json | 0 web/i18n/{ => locales}/pl-PL/layout.json | 0 web/i18n/{ => locales}/pl-PL/login.json | 0 web/i18n/{ => locales}/pl-PL/oauth.json | 0 .../{ => locales}/pl-PL/permission-keys.json | 0 web/i18n/{ => locales}/pl-PL/permission.json | 0 web/i18n/{ => locales}/pl-PL/pipeline.json | 0 web/i18n/{ => locales}/pl-PL/plugin-tags.json | 0 .../{ => locales}/pl-PL/plugin-trigger.json | 0 web/i18n/{ => locales}/pl-PL/plugin.json | 0 web/i18n/{ => locales}/pl-PL/register.json | 0 web/i18n/{ => locales}/pl-PL/run-log.json | 0 web/i18n/{ => locales}/pl-PL/share.json | 0 web/i18n/{ => locales}/pl-PL/skill.json | 0 web/i18n/{ => locales}/pl-PL/snippet.json | 0 web/i18n/{ => locales}/pl-PL/time.json | 0 web/i18n/{ => locales}/pl-PL/tools.json | 0 web/i18n/{ => locales}/pl-PL/workflow.json | 0 web/i18n/{ => locales}/pt-BR/agent-v-2.json | 0 .../{ => locales}/pt-BR/app-annotation.json | 0 web/i18n/{ => locales}/pt-BR/app-api.json | 0 web/i18n/{ => locales}/pt-BR/app-debug.json | 0 web/i18n/{ => locales}/pt-BR/app-log.json | 0 .../{ => locales}/pt-BR/app-overview.json | 0 web/i18n/{ => locales}/pt-BR/app.json | 0 web/i18n/{ => locales}/pt-BR/billing.json | 0 web/i18n/{ => locales}/pt-BR/common.json | 0 web/i18n/{ => locales}/pt-BR/custom.json | 0 .../{ => locales}/pt-BR/dataset-creation.json | 0 .../pt-BR/dataset-documents.json | 0 .../pt-BR/dataset-hit-testing.json | 0 .../{ => locales}/pt-BR/dataset-pipeline.json | 0 .../{ => locales}/pt-BR/dataset-settings.json | 0 web/i18n/{ => locales}/pt-BR/dataset.json | 0 web/i18n/{ => locales}/pt-BR/deployments.json | 0 web/i18n/{ => locales}/pt-BR/device-flow.json | 0 web/i18n/{ => locales}/pt-BR/education.json | 0 web/i18n/{ => locales}/pt-BR/explore.json | 0 web/i18n/{ => locales}/pt-BR/layout.json | 0 web/i18n/{ => locales}/pt-BR/login.json | 0 web/i18n/{ => locales}/pt-BR/oauth.json | 0 .../{ => locales}/pt-BR/permission-keys.json | 0 web/i18n/{ => locales}/pt-BR/permission.json | 0 web/i18n/{ => locales}/pt-BR/pipeline.json | 0 web/i18n/{ => locales}/pt-BR/plugin-tags.json | 0 .../{ => locales}/pt-BR/plugin-trigger.json | 0 web/i18n/{ => locales}/pt-BR/plugin.json | 0 web/i18n/{ => locales}/pt-BR/register.json | 0 web/i18n/{ => locales}/pt-BR/run-log.json | 0 web/i18n/{ => locales}/pt-BR/share.json | 0 web/i18n/{ => locales}/pt-BR/skill.json | 0 web/i18n/{ => locales}/pt-BR/snippet.json | 0 web/i18n/{ => locales}/pt-BR/time.json | 0 web/i18n/{ => locales}/pt-BR/tools.json | 0 web/i18n/{ => locales}/pt-BR/workflow.json | 0 web/i18n/{ => locales}/ro-RO/agent-v-2.json | 0 .../{ => locales}/ro-RO/app-annotation.json | 0 web/i18n/{ => locales}/ro-RO/app-api.json | 0 web/i18n/{ => locales}/ro-RO/app-debug.json | 0 web/i18n/{ => locales}/ro-RO/app-log.json | 0 .../{ => locales}/ro-RO/app-overview.json | 0 web/i18n/{ => locales}/ro-RO/app.json | 0 web/i18n/{ => locales}/ro-RO/billing.json | 0 web/i18n/{ => locales}/ro-RO/common.json | 0 web/i18n/{ => locales}/ro-RO/custom.json | 0 .../{ => locales}/ro-RO/dataset-creation.json | 0 .../ro-RO/dataset-documents.json | 0 .../ro-RO/dataset-hit-testing.json | 0 .../{ => locales}/ro-RO/dataset-pipeline.json | 0 .../{ => locales}/ro-RO/dataset-settings.json | 0 web/i18n/{ => locales}/ro-RO/dataset.json | 0 web/i18n/{ => locales}/ro-RO/deployments.json | 0 web/i18n/{ => locales}/ro-RO/device-flow.json | 0 web/i18n/{ => locales}/ro-RO/education.json | 0 web/i18n/{ => locales}/ro-RO/explore.json | 0 web/i18n/{ => locales}/ro-RO/layout.json | 0 web/i18n/{ => locales}/ro-RO/login.json | 0 web/i18n/{ => locales}/ro-RO/oauth.json | 0 .../{ => locales}/ro-RO/permission-keys.json | 0 web/i18n/{ => locales}/ro-RO/permission.json | 0 web/i18n/{ => locales}/ro-RO/pipeline.json | 0 web/i18n/{ => locales}/ro-RO/plugin-tags.json | 0 .../{ => locales}/ro-RO/plugin-trigger.json | 0 web/i18n/{ => locales}/ro-RO/plugin.json | 0 web/i18n/{ => locales}/ro-RO/register.json | 0 web/i18n/{ => locales}/ro-RO/run-log.json | 0 web/i18n/{ => locales}/ro-RO/share.json | 0 web/i18n/{ => locales}/ro-RO/skill.json | 0 web/i18n/{ => locales}/ro-RO/snippet.json | 0 web/i18n/{ => locales}/ro-RO/time.json | 0 web/i18n/{ => locales}/ro-RO/tools.json | 0 web/i18n/{ => locales}/ro-RO/workflow.json | 0 web/i18n/{ => locales}/ru-RU/agent-v-2.json | 0 .../{ => locales}/ru-RU/app-annotation.json | 0 web/i18n/{ => locales}/ru-RU/app-api.json | 0 web/i18n/{ => locales}/ru-RU/app-debug.json | 0 web/i18n/{ => locales}/ru-RU/app-log.json | 0 .../{ => locales}/ru-RU/app-overview.json | 0 web/i18n/{ => locales}/ru-RU/app.json | 0 web/i18n/{ => locales}/ru-RU/billing.json | 0 web/i18n/{ => locales}/ru-RU/common.json | 0 web/i18n/{ => locales}/ru-RU/custom.json | 0 .../{ => locales}/ru-RU/dataset-creation.json | 0 .../ru-RU/dataset-documents.json | 0 .../ru-RU/dataset-hit-testing.json | 0 .../{ => locales}/ru-RU/dataset-pipeline.json | 0 .../{ => locales}/ru-RU/dataset-settings.json | 0 web/i18n/{ => locales}/ru-RU/dataset.json | 0 web/i18n/{ => locales}/ru-RU/deployments.json | 0 web/i18n/{ => locales}/ru-RU/device-flow.json | 0 web/i18n/{ => locales}/ru-RU/education.json | 0 web/i18n/{ => locales}/ru-RU/explore.json | 0 web/i18n/{ => locales}/ru-RU/layout.json | 0 web/i18n/{ => locales}/ru-RU/login.json | 0 web/i18n/{ => locales}/ru-RU/oauth.json | 0 .../{ => locales}/ru-RU/permission-keys.json | 0 web/i18n/{ => locales}/ru-RU/permission.json | 0 web/i18n/{ => locales}/ru-RU/pipeline.json | 0 web/i18n/{ => locales}/ru-RU/plugin-tags.json | 0 .../{ => locales}/ru-RU/plugin-trigger.json | 0 web/i18n/{ => locales}/ru-RU/plugin.json | 0 web/i18n/{ => locales}/ru-RU/register.json | 0 web/i18n/{ => locales}/ru-RU/run-log.json | 0 web/i18n/{ => locales}/ru-RU/share.json | 0 web/i18n/{ => locales}/ru-RU/skill.json | 0 web/i18n/{ => locales}/ru-RU/snippet.json | 0 web/i18n/{ => locales}/ru-RU/time.json | 0 web/i18n/{ => locales}/ru-RU/tools.json | 0 web/i18n/{ => locales}/ru-RU/workflow.json | 0 web/i18n/{ => locales}/sl-SI/agent-v-2.json | 0 .../{ => locales}/sl-SI/app-annotation.json | 0 web/i18n/{ => locales}/sl-SI/app-api.json | 0 web/i18n/{ => locales}/sl-SI/app-debug.json | 0 web/i18n/{ => locales}/sl-SI/app-log.json | 0 .../{ => locales}/sl-SI/app-overview.json | 0 web/i18n/{ => locales}/sl-SI/app.json | 0 web/i18n/{ => locales}/sl-SI/billing.json | 0 web/i18n/{ => locales}/sl-SI/common.json | 0 web/i18n/{ => locales}/sl-SI/custom.json | 0 .../{ => locales}/sl-SI/dataset-creation.json | 0 .../sl-SI/dataset-documents.json | 0 .../sl-SI/dataset-hit-testing.json | 0 .../{ => locales}/sl-SI/dataset-pipeline.json | 0 .../{ => locales}/sl-SI/dataset-settings.json | 0 web/i18n/{ => locales}/sl-SI/dataset.json | 0 web/i18n/{ => locales}/sl-SI/deployments.json | 0 web/i18n/{ => locales}/sl-SI/device-flow.json | 0 web/i18n/{ => locales}/sl-SI/education.json | 0 web/i18n/{ => locales}/sl-SI/explore.json | 0 web/i18n/{ => locales}/sl-SI/layout.json | 0 web/i18n/{ => locales}/sl-SI/login.json | 0 web/i18n/{ => locales}/sl-SI/oauth.json | 0 .../{ => locales}/sl-SI/permission-keys.json | 0 web/i18n/{ => locales}/sl-SI/permission.json | 0 web/i18n/{ => locales}/sl-SI/pipeline.json | 0 web/i18n/{ => locales}/sl-SI/plugin-tags.json | 0 .../{ => locales}/sl-SI/plugin-trigger.json | 0 web/i18n/{ => locales}/sl-SI/plugin.json | 0 web/i18n/{ => locales}/sl-SI/register.json | 0 web/i18n/{ => locales}/sl-SI/run-log.json | 0 web/i18n/{ => locales}/sl-SI/share.json | 0 web/i18n/{ => locales}/sl-SI/skill.json | 0 web/i18n/{ => locales}/sl-SI/snippet.json | 0 web/i18n/{ => locales}/sl-SI/time.json | 0 web/i18n/{ => locales}/sl-SI/tools.json | 0 web/i18n/{ => locales}/sl-SI/workflow.json | 0 web/i18n/{ => locales}/th-TH/agent-v-2.json | 0 .../{ => locales}/th-TH/app-annotation.json | 0 web/i18n/{ => locales}/th-TH/app-api.json | 0 web/i18n/{ => locales}/th-TH/app-debug.json | 0 web/i18n/{ => locales}/th-TH/app-log.json | 0 .../{ => locales}/th-TH/app-overview.json | 0 web/i18n/{ => locales}/th-TH/app.json | 0 web/i18n/{ => locales}/th-TH/billing.json | 0 web/i18n/{ => locales}/th-TH/common.json | 0 web/i18n/{ => locales}/th-TH/custom.json | 0 .../{ => locales}/th-TH/dataset-creation.json | 0 .../th-TH/dataset-documents.json | 0 .../th-TH/dataset-hit-testing.json | 0 .../{ => locales}/th-TH/dataset-pipeline.json | 0 .../{ => locales}/th-TH/dataset-settings.json | 0 web/i18n/{ => locales}/th-TH/dataset.json | 0 web/i18n/{ => locales}/th-TH/deployments.json | 0 web/i18n/{ => locales}/th-TH/device-flow.json | 0 web/i18n/{ => locales}/th-TH/education.json | 0 web/i18n/{ => locales}/th-TH/explore.json | 0 web/i18n/{ => locales}/th-TH/layout.json | 0 web/i18n/{ => locales}/th-TH/login.json | 0 web/i18n/{ => locales}/th-TH/oauth.json | 0 .../{ => locales}/th-TH/permission-keys.json | 0 web/i18n/{ => locales}/th-TH/permission.json | 0 web/i18n/{ => locales}/th-TH/pipeline.json | 0 web/i18n/{ => locales}/th-TH/plugin-tags.json | 0 .../{ => locales}/th-TH/plugin-trigger.json | 0 web/i18n/{ => locales}/th-TH/plugin.json | 0 web/i18n/{ => locales}/th-TH/register.json | 0 web/i18n/{ => locales}/th-TH/run-log.json | 0 web/i18n/{ => locales}/th-TH/share.json | 0 web/i18n/{ => locales}/th-TH/skill.json | 0 web/i18n/{ => locales}/th-TH/snippet.json | 0 web/i18n/{ => locales}/th-TH/time.json | 0 web/i18n/{ => locales}/th-TH/tools.json | 0 web/i18n/{ => locales}/th-TH/workflow.json | 0 web/i18n/{ => locales}/tr-TR/agent-v-2.json | 0 .../{ => locales}/tr-TR/app-annotation.json | 0 web/i18n/{ => locales}/tr-TR/app-api.json | 0 web/i18n/{ => locales}/tr-TR/app-debug.json | 0 web/i18n/{ => locales}/tr-TR/app-log.json | 0 .../{ => locales}/tr-TR/app-overview.json | 0 web/i18n/{ => locales}/tr-TR/app.json | 0 web/i18n/{ => locales}/tr-TR/billing.json | 0 web/i18n/{ => locales}/tr-TR/common.json | 0 web/i18n/{ => locales}/tr-TR/custom.json | 0 .../{ => locales}/tr-TR/dataset-creation.json | 0 .../tr-TR/dataset-documents.json | 0 .../tr-TR/dataset-hit-testing.json | 0 .../{ => locales}/tr-TR/dataset-pipeline.json | 0 .../{ => locales}/tr-TR/dataset-settings.json | 0 web/i18n/{ => locales}/tr-TR/dataset.json | 0 web/i18n/{ => locales}/tr-TR/deployments.json | 0 web/i18n/{ => locales}/tr-TR/device-flow.json | 0 web/i18n/{ => locales}/tr-TR/education.json | 0 web/i18n/{ => locales}/tr-TR/explore.json | 0 web/i18n/{ => locales}/tr-TR/layout.json | 0 web/i18n/{ => locales}/tr-TR/login.json | 0 web/i18n/{ => locales}/tr-TR/oauth.json | 0 .../{ => locales}/tr-TR/permission-keys.json | 0 web/i18n/{ => locales}/tr-TR/permission.json | 0 web/i18n/{ => locales}/tr-TR/pipeline.json | 0 web/i18n/{ => locales}/tr-TR/plugin-tags.json | 0 .../{ => locales}/tr-TR/plugin-trigger.json | 0 web/i18n/{ => locales}/tr-TR/plugin.json | 0 web/i18n/{ => locales}/tr-TR/register.json | 0 web/i18n/{ => locales}/tr-TR/run-log.json | 0 web/i18n/{ => locales}/tr-TR/share.json | 0 web/i18n/{ => locales}/tr-TR/skill.json | 0 web/i18n/{ => locales}/tr-TR/snippet.json | 0 web/i18n/{ => locales}/tr-TR/time.json | 0 web/i18n/{ => locales}/tr-TR/tools.json | 0 web/i18n/{ => locales}/tr-TR/workflow.json | 0 web/i18n/{ => locales}/uk-UA/agent-v-2.json | 0 .../{ => locales}/uk-UA/app-annotation.json | 0 web/i18n/{ => locales}/uk-UA/app-api.json | 0 web/i18n/{ => locales}/uk-UA/app-debug.json | 0 web/i18n/{ => locales}/uk-UA/app-log.json | 0 .../{ => locales}/uk-UA/app-overview.json | 0 web/i18n/{ => locales}/uk-UA/app.json | 0 web/i18n/{ => locales}/uk-UA/billing.json | 0 web/i18n/{ => locales}/uk-UA/common.json | 0 web/i18n/{ => locales}/uk-UA/custom.json | 0 .../{ => locales}/uk-UA/dataset-creation.json | 0 .../uk-UA/dataset-documents.json | 0 .../uk-UA/dataset-hit-testing.json | 0 .../{ => locales}/uk-UA/dataset-pipeline.json | 0 .../{ => locales}/uk-UA/dataset-settings.json | 0 web/i18n/{ => locales}/uk-UA/dataset.json | 0 web/i18n/{ => locales}/uk-UA/deployments.json | 0 web/i18n/{ => locales}/uk-UA/device-flow.json | 0 web/i18n/{ => locales}/uk-UA/education.json | 0 web/i18n/{ => locales}/uk-UA/explore.json | 0 web/i18n/{ => locales}/uk-UA/layout.json | 0 web/i18n/{ => locales}/uk-UA/login.json | 0 web/i18n/{ => locales}/uk-UA/oauth.json | 0 .../{ => locales}/uk-UA/permission-keys.json | 0 web/i18n/{ => locales}/uk-UA/permission.json | 0 web/i18n/{ => locales}/uk-UA/pipeline.json | 0 web/i18n/{ => locales}/uk-UA/plugin-tags.json | 0 .../{ => locales}/uk-UA/plugin-trigger.json | 0 web/i18n/{ => locales}/uk-UA/plugin.json | 0 web/i18n/{ => locales}/uk-UA/register.json | 0 web/i18n/{ => locales}/uk-UA/run-log.json | 0 web/i18n/{ => locales}/uk-UA/share.json | 0 web/i18n/{ => locales}/uk-UA/skill.json | 0 web/i18n/{ => locales}/uk-UA/snippet.json | 0 web/i18n/{ => locales}/uk-UA/time.json | 0 web/i18n/{ => locales}/uk-UA/tools.json | 0 web/i18n/{ => locales}/uk-UA/workflow.json | 0 web/i18n/{ => locales}/vi-VN/agent-v-2.json | 0 .../{ => locales}/vi-VN/app-annotation.json | 0 web/i18n/{ => locales}/vi-VN/app-api.json | 0 web/i18n/{ => locales}/vi-VN/app-debug.json | 0 web/i18n/{ => locales}/vi-VN/app-log.json | 0 .../{ => locales}/vi-VN/app-overview.json | 0 web/i18n/{ => locales}/vi-VN/app.json | 0 web/i18n/{ => locales}/vi-VN/billing.json | 0 web/i18n/{ => locales}/vi-VN/common.json | 0 web/i18n/{ => locales}/vi-VN/custom.json | 0 .../{ => locales}/vi-VN/dataset-creation.json | 0 .../vi-VN/dataset-documents.json | 0 .../vi-VN/dataset-hit-testing.json | 0 .../{ => locales}/vi-VN/dataset-pipeline.json | 0 .../{ => locales}/vi-VN/dataset-settings.json | 0 web/i18n/{ => locales}/vi-VN/dataset.json | 0 web/i18n/{ => locales}/vi-VN/deployments.json | 0 web/i18n/{ => locales}/vi-VN/device-flow.json | 0 web/i18n/{ => locales}/vi-VN/education.json | 0 web/i18n/{ => locales}/vi-VN/explore.json | 0 web/i18n/{ => locales}/vi-VN/layout.json | 0 web/i18n/{ => locales}/vi-VN/login.json | 0 web/i18n/{ => locales}/vi-VN/oauth.json | 0 .../{ => locales}/vi-VN/permission-keys.json | 0 web/i18n/{ => locales}/vi-VN/permission.json | 0 web/i18n/{ => locales}/vi-VN/pipeline.json | 0 web/i18n/{ => locales}/vi-VN/plugin-tags.json | 0 .../{ => locales}/vi-VN/plugin-trigger.json | 0 web/i18n/{ => locales}/vi-VN/plugin.json | 0 web/i18n/{ => locales}/vi-VN/register.json | 0 web/i18n/{ => locales}/vi-VN/run-log.json | 0 web/i18n/{ => locales}/vi-VN/share.json | 0 web/i18n/{ => locales}/vi-VN/skill.json | 0 web/i18n/{ => locales}/vi-VN/snippet.json | 0 web/i18n/{ => locales}/vi-VN/time.json | 0 web/i18n/{ => locales}/vi-VN/tools.json | 0 web/i18n/{ => locales}/vi-VN/workflow.json | 0 web/i18n/{ => locales}/zh-Hans/agent-v-2.json | 0 .../{ => locales}/zh-Hans/app-annotation.json | 0 web/i18n/{ => locales}/zh-Hans/app-api.json | 0 web/i18n/{ => locales}/zh-Hans/app-debug.json | 0 web/i18n/{ => locales}/zh-Hans/app-log.json | 0 .../{ => locales}/zh-Hans/app-overview.json | 0 web/i18n/{ => locales}/zh-Hans/app.json | 0 web/i18n/{ => locales}/zh-Hans/billing.json | 0 web/i18n/{ => locales}/zh-Hans/common.json | 0 web/i18n/{ => locales}/zh-Hans/custom.json | 0 .../zh-Hans/dataset-creation.json | 0 .../zh-Hans/dataset-documents.json | 0 .../zh-Hans/dataset-hit-testing.json | 0 .../zh-Hans/dataset-pipeline.json | 0 .../zh-Hans/dataset-settings.json | 0 web/i18n/{ => locales}/zh-Hans/dataset.json | 0 .../{ => locales}/zh-Hans/deployments.json | 0 .../{ => locales}/zh-Hans/device-flow.json | 0 web/i18n/{ => locales}/zh-Hans/education.json | 0 web/i18n/{ => locales}/zh-Hans/explore.json | 0 web/i18n/{ => locales}/zh-Hans/layout.json | 0 web/i18n/{ => locales}/zh-Hans/login.json | 0 web/i18n/{ => locales}/zh-Hans/oauth.json | 0 .../zh-Hans/permission-keys.json | 0 .../{ => locales}/zh-Hans/permission.json | 0 web/i18n/{ => locales}/zh-Hans/pipeline.json | 0 .../{ => locales}/zh-Hans/plugin-tags.json | 0 .../{ => locales}/zh-Hans/plugin-trigger.json | 0 web/i18n/{ => locales}/zh-Hans/plugin.json | 0 web/i18n/{ => locales}/zh-Hans/register.json | 0 web/i18n/{ => locales}/zh-Hans/run-log.json | 0 web/i18n/{ => locales}/zh-Hans/share.json | 0 web/i18n/{ => locales}/zh-Hans/skill.json | 0 web/i18n/{ => locales}/zh-Hans/snippet.json | 0 web/i18n/{ => locales}/zh-Hans/time.json | 0 web/i18n/{ => locales}/zh-Hans/tools.json | 0 web/i18n/{ => locales}/zh-Hans/workflow.json | 0 web/i18n/{ => locales}/zh-Hant/agent-v-2.json | 0 .../{ => locales}/zh-Hant/app-annotation.json | 0 web/i18n/{ => locales}/zh-Hant/app-api.json | 0 web/i18n/{ => locales}/zh-Hant/app-debug.json | 0 web/i18n/{ => locales}/zh-Hant/app-log.json | 0 .../{ => locales}/zh-Hant/app-overview.json | 0 web/i18n/{ => locales}/zh-Hant/app.json | 0 web/i18n/{ => locales}/zh-Hant/billing.json | 0 web/i18n/{ => locales}/zh-Hant/common.json | 0 web/i18n/{ => locales}/zh-Hant/custom.json | 0 .../zh-Hant/dataset-creation.json | 0 .../zh-Hant/dataset-documents.json | 0 .../zh-Hant/dataset-hit-testing.json | 0 .../zh-Hant/dataset-pipeline.json | 0 .../zh-Hant/dataset-settings.json | 0 web/i18n/{ => locales}/zh-Hant/dataset.json | 0 .../{ => locales}/zh-Hant/deployments.json | 0 .../{ => locales}/zh-Hant/device-flow.json | 0 web/i18n/{ => locales}/zh-Hant/education.json | 0 web/i18n/{ => locales}/zh-Hant/explore.json | 0 web/i18n/{ => locales}/zh-Hant/layout.json | 0 web/i18n/{ => locales}/zh-Hant/login.json | 0 web/i18n/{ => locales}/zh-Hant/oauth.json | 0 .../zh-Hant/permission-keys.json | 0 .../{ => locales}/zh-Hant/permission.json | 0 web/i18n/{ => locales}/zh-Hant/pipeline.json | 0 .../{ => locales}/zh-Hant/plugin-tags.json | 0 .../{ => locales}/zh-Hant/plugin-trigger.json | 0 web/i18n/{ => locales}/zh-Hant/plugin.json | 0 web/i18n/{ => locales}/zh-Hant/register.json | 0 web/i18n/{ => locales}/zh-Hant/run-log.json | 0 web/i18n/{ => locales}/zh-Hant/share.json | 0 web/i18n/{ => locales}/zh-Hant/skill.json | 0 web/i18n/{ => locales}/zh-Hant/snippet.json | 0 web/i18n/{ => locales}/zh-Hant/time.json | 0 web/i18n/{ => locales}/zh-Hant/tools.json | 0 web/i18n/{ => locales}/zh-Hant/workflow.json | 0 web/{i18n-config => i18n}/metadata.ts | 0 web/{i18n-config => i18n}/resources.ts | 74 ++++++------ web/{i18n-config => i18n}/server.ts | 0 web/{i18n-config => i18n}/settings.ts | 0 web/models/access-control.ts | 2 +- web/models/common.ts | 2 +- web/models/share.ts | 2 +- web/package.json | 4 +- web/plugins/eslint/rules/no-extra-keys.js | 4 +- web/plugins/vite/__tests__/i18n-graph.spec.ts | 111 +++++++++--------- web/plugins/vite/__tests__/i18n-prune.spec.ts | 4 +- web/plugins/vite/custom-i18n-hmr.ts | 4 +- web/plugins/vite/i18n-prune.ts | 2 +- web/plugins/vite/i18n-prune/graph.ts | 2 +- .../__tests__/migrate-i18n-selectors.spec.ts | 4 +- web/scripts/analyze-i18n-diff.ts | 6 +- web/scripts/check-i18n.js | 8 +- web/scripts/migrate-i18n-selectors.ts | 2 +- .../access-control/use-app-access-config.ts | 2 +- .../use-dataset-access-config.ts | 2 +- .../access-control/use-member-roles.ts | 2 +- web/service/use-common.ts | 2 +- web/types/app.ts | 2 +- web/types/i18n.d.ts | 2 +- web/utils/format.ts | 4 +- 1155 files changed, 381 insertions(+), 385 deletions(-) rename web/{i18n-config => i18n}/README.md (80%) rename web/{i18n-config => i18n}/__tests__/client-locale.spec.ts (100%) rename web/{i18n-config => i18n}/__tests__/load-resource.spec.ts (100%) rename web/{i18n-config => i18n}/__tests__/metadata.spec.ts (100%) rename web/{i18n-config => i18n}/__tests__/plural-selector.spec.ts (97%) rename web/{i18n-config => i18n}/__tests__/server.spec.ts (100%) rename web/{i18n-config => i18n}/client.ts (100%) rename web/{i18n-config => i18n}/index.ts (100%) rename web/{i18n-config => i18n}/language.ts (100%) rename web/{i18n-config => i18n}/languages.ts (100%) rename web/{i18n-config => i18n}/lib.client.ts (100%) rename web/{i18n-config => i18n}/lib.server.ts (100%) rename web/{i18n-config => i18n}/load-resource.ts (100%) rename web/{i18n-config => i18n}/locale-resources/ar-TN.ts (51%) rename web/{i18n-config => i18n}/locale-resources/az-AZ.ts (51%) rename web/{i18n-config => i18n}/locale-resources/de-DE.ts (51%) rename web/{i18n-config => i18n}/locale-resources/en-US.ts (51%) rename web/{i18n-config => i18n}/locale-resources/es-ES.ts (51%) rename web/{i18n-config => i18n}/locale-resources/fa-IR.ts (51%) rename web/{i18n-config => i18n}/locale-resources/fr-FR.ts (51%) rename web/{i18n-config => i18n}/locale-resources/hi-IN.ts (51%) rename web/{i18n-config => i18n}/locale-resources/id-ID.ts (51%) rename web/{i18n-config => i18n}/locale-resources/it-IT.ts (51%) rename web/{i18n-config => i18n}/locale-resources/ja-JP.ts (51%) rename web/{i18n-config => i18n}/locale-resources/ko-KR.ts (51%) rename web/{i18n-config => i18n}/locale-resources/lo-LA.ts (51%) rename web/{i18n-config => i18n}/locale-resources/nl-NL.ts (51%) rename web/{i18n-config => i18n}/locale-resources/pl-PL.ts (51%) rename web/{i18n-config => i18n}/locale-resources/pt-BR.ts (51%) rename web/{i18n-config => i18n}/locale-resources/ro-RO.ts (51%) rename web/{i18n-config => i18n}/locale-resources/ru-RU.ts (51%) rename web/{i18n-config => i18n}/locale-resources/sl-SI.ts (51%) rename web/{i18n-config => i18n}/locale-resources/th-TH.ts (51%) rename web/{i18n-config => i18n}/locale-resources/tr-TR.ts (51%) rename web/{i18n-config => i18n}/locale-resources/uk-UA.ts (51%) rename web/{i18n-config => i18n}/locale-resources/vi-VN.ts (51%) rename web/{i18n-config => i18n}/locale-resources/zh-Hans.ts (50%) rename web/{i18n-config => i18n}/locale-resources/zh-Hant.ts (50%) rename web/{i18n-config => i18n}/locale.ts (100%) rename web/i18n/{ => locales}/ar-TN/agent-v-2.json (100%) rename web/i18n/{ => locales}/ar-TN/app-annotation.json (100%) rename web/i18n/{ => locales}/ar-TN/app-api.json (100%) rename web/i18n/{ => locales}/ar-TN/app-debug.json (100%) rename web/i18n/{ => locales}/ar-TN/app-log.json (100%) rename web/i18n/{ => locales}/ar-TN/app-overview.json (100%) rename web/i18n/{ => locales}/ar-TN/app.json (100%) rename web/i18n/{ => locales}/ar-TN/billing.json (100%) rename web/i18n/{ => locales}/ar-TN/common.json (100%) rename web/i18n/{ => locales}/ar-TN/custom.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset-creation.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset-documents.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset-settings.json (100%) rename web/i18n/{ => locales}/ar-TN/dataset.json (100%) rename web/i18n/{ => locales}/ar-TN/deployments.json (100%) rename web/i18n/{ => locales}/ar-TN/device-flow.json (100%) rename web/i18n/{ => locales}/ar-TN/education.json (100%) rename web/i18n/{ => locales}/ar-TN/explore.json (100%) rename web/i18n/{ => locales}/ar-TN/layout.json (100%) rename web/i18n/{ => locales}/ar-TN/login.json (100%) rename web/i18n/{ => locales}/ar-TN/oauth.json (100%) rename web/i18n/{ => locales}/ar-TN/permission-keys.json (100%) rename web/i18n/{ => locales}/ar-TN/permission.json (100%) rename web/i18n/{ => locales}/ar-TN/pipeline.json (100%) rename web/i18n/{ => locales}/ar-TN/plugin-tags.json (100%) rename web/i18n/{ => locales}/ar-TN/plugin-trigger.json (100%) rename web/i18n/{ => locales}/ar-TN/plugin.json (100%) rename web/i18n/{ => locales}/ar-TN/register.json (100%) rename web/i18n/{ => locales}/ar-TN/run-log.json (100%) rename web/i18n/{ => locales}/ar-TN/share.json (100%) rename web/i18n/{ => locales}/ar-TN/skill.json (100%) rename web/i18n/{ => locales}/ar-TN/snippet.json (100%) rename web/i18n/{ => locales}/ar-TN/time.json (100%) rename web/i18n/{ => locales}/ar-TN/tools.json (100%) rename web/i18n/{ => locales}/ar-TN/workflow.json (100%) rename web/i18n/{ => locales}/az-AZ/agent-v-2.json (100%) rename web/i18n/{ => locales}/az-AZ/app-annotation.json (100%) rename web/i18n/{ => locales}/az-AZ/app-api.json (100%) rename web/i18n/{ => locales}/az-AZ/app-debug.json (100%) rename web/i18n/{ => locales}/az-AZ/app-log.json (100%) rename web/i18n/{ => locales}/az-AZ/app-overview.json (100%) rename web/i18n/{ => locales}/az-AZ/app.json (100%) rename web/i18n/{ => locales}/az-AZ/billing.json (100%) rename web/i18n/{ => locales}/az-AZ/common.json (100%) rename web/i18n/{ => locales}/az-AZ/custom.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset-creation.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset-documents.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset-settings.json (100%) rename web/i18n/{ => locales}/az-AZ/dataset.json (100%) rename web/i18n/{ => locales}/az-AZ/deployments.json (100%) rename web/i18n/{ => locales}/az-AZ/device-flow.json (100%) rename web/i18n/{ => locales}/az-AZ/education.json (100%) rename web/i18n/{ => locales}/az-AZ/explore.json (100%) rename web/i18n/{ => locales}/az-AZ/layout.json (100%) rename web/i18n/{ => locales}/az-AZ/login.json (100%) rename web/i18n/{ => locales}/az-AZ/oauth.json (100%) rename web/i18n/{ => locales}/az-AZ/permission-keys.json (100%) rename web/i18n/{ => locales}/az-AZ/permission.json (100%) rename web/i18n/{ => locales}/az-AZ/pipeline.json (100%) rename web/i18n/{ => locales}/az-AZ/plugin-tags.json (100%) rename web/i18n/{ => locales}/az-AZ/plugin-trigger.json (100%) rename web/i18n/{ => locales}/az-AZ/plugin.json (100%) rename web/i18n/{ => locales}/az-AZ/register.json (100%) rename web/i18n/{ => locales}/az-AZ/run-log.json (100%) rename web/i18n/{ => locales}/az-AZ/share.json (100%) rename web/i18n/{ => locales}/az-AZ/skill.json (100%) rename web/i18n/{ => locales}/az-AZ/snippet.json (100%) rename web/i18n/{ => locales}/az-AZ/time.json (100%) rename web/i18n/{ => locales}/az-AZ/tools.json (100%) rename web/i18n/{ => locales}/az-AZ/workflow.json (100%) rename web/i18n/{ => locales}/de-DE/agent-v-2.json (100%) rename web/i18n/{ => locales}/de-DE/app-annotation.json (100%) rename web/i18n/{ => locales}/de-DE/app-api.json (100%) rename web/i18n/{ => locales}/de-DE/app-debug.json (100%) rename web/i18n/{ => locales}/de-DE/app-log.json (100%) rename web/i18n/{ => locales}/de-DE/app-overview.json (100%) rename web/i18n/{ => locales}/de-DE/app.json (100%) rename web/i18n/{ => locales}/de-DE/billing.json (100%) rename web/i18n/{ => locales}/de-DE/common.json (100%) rename web/i18n/{ => locales}/de-DE/custom.json (100%) rename web/i18n/{ => locales}/de-DE/dataset-creation.json (100%) rename web/i18n/{ => locales}/de-DE/dataset-documents.json (100%) rename web/i18n/{ => locales}/de-DE/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/de-DE/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/de-DE/dataset-settings.json (100%) rename web/i18n/{ => locales}/de-DE/dataset.json (100%) rename web/i18n/{ => locales}/de-DE/deployments.json (100%) rename web/i18n/{ => locales}/de-DE/device-flow.json (100%) rename web/i18n/{ => locales}/de-DE/education.json (100%) rename web/i18n/{ => locales}/de-DE/explore.json (100%) rename web/i18n/{ => locales}/de-DE/layout.json (100%) rename web/i18n/{ => locales}/de-DE/login.json (100%) rename web/i18n/{ => locales}/de-DE/oauth.json (100%) rename web/i18n/{ => locales}/de-DE/permission-keys.json (100%) rename web/i18n/{ => locales}/de-DE/permission.json (100%) rename web/i18n/{ => locales}/de-DE/pipeline.json (100%) rename web/i18n/{ => locales}/de-DE/plugin-tags.json (100%) rename web/i18n/{ => locales}/de-DE/plugin-trigger.json (100%) rename web/i18n/{ => locales}/de-DE/plugin.json (100%) rename web/i18n/{ => locales}/de-DE/register.json (100%) rename web/i18n/{ => locales}/de-DE/run-log.json (100%) rename web/i18n/{ => locales}/de-DE/share.json (100%) rename web/i18n/{ => locales}/de-DE/skill.json (100%) rename web/i18n/{ => locales}/de-DE/snippet.json (100%) rename web/i18n/{ => locales}/de-DE/time.json (100%) rename web/i18n/{ => locales}/de-DE/tools.json (100%) rename web/i18n/{ => locales}/de-DE/workflow.json (100%) rename web/i18n/{ => locales}/en-US/agent-v-2.json (100%) rename web/i18n/{ => locales}/en-US/app-annotation.json (100%) rename web/i18n/{ => locales}/en-US/app-api.json (100%) rename web/i18n/{ => locales}/en-US/app-debug.json (100%) rename web/i18n/{ => locales}/en-US/app-log.json (100%) rename web/i18n/{ => locales}/en-US/app-overview.json (100%) rename web/i18n/{ => locales}/en-US/app.json (100%) rename web/i18n/{ => locales}/en-US/billing.json (100%) rename web/i18n/{ => locales}/en-US/common.json (100%) rename web/i18n/{ => locales}/en-US/custom.json (100%) rename web/i18n/{ => locales}/en-US/dataset-creation.json (100%) rename web/i18n/{ => locales}/en-US/dataset-documents.json (100%) rename web/i18n/{ => locales}/en-US/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/en-US/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/en-US/dataset-settings.json (100%) rename web/i18n/{ => locales}/en-US/dataset.json (100%) rename web/i18n/{ => locales}/en-US/deployments.json (100%) rename web/i18n/{ => locales}/en-US/device-flow.json (100%) rename web/i18n/{ => locales}/en-US/education.json (100%) rename web/i18n/{ => locales}/en-US/explore.json (100%) rename web/i18n/{ => locales}/en-US/layout.json (100%) rename web/i18n/{ => locales}/en-US/login.json (100%) rename web/i18n/{ => locales}/en-US/oauth.json (100%) rename web/i18n/{ => locales}/en-US/permission-keys.json (100%) rename web/i18n/{ => locales}/en-US/permission.json (100%) rename web/i18n/{ => locales}/en-US/pipeline.json (100%) rename web/i18n/{ => locales}/en-US/plugin-tags.json (100%) rename web/i18n/{ => locales}/en-US/plugin-trigger.json (100%) rename web/i18n/{ => locales}/en-US/plugin.json (100%) rename web/i18n/{ => locales}/en-US/register.json (100%) rename web/i18n/{ => locales}/en-US/run-log.json (100%) rename web/i18n/{ => locales}/en-US/share.json (100%) rename web/i18n/{ => locales}/en-US/skill.json (100%) rename web/i18n/{ => locales}/en-US/snippet.json (100%) rename web/i18n/{ => locales}/en-US/time.json (100%) rename web/i18n/{ => locales}/en-US/tools.json (100%) rename web/i18n/{ => locales}/en-US/workflow.json (100%) rename web/i18n/{ => locales}/es-ES/agent-v-2.json (100%) rename web/i18n/{ => locales}/es-ES/app-annotation.json (100%) rename web/i18n/{ => locales}/es-ES/app-api.json (100%) rename web/i18n/{ => locales}/es-ES/app-debug.json (100%) rename web/i18n/{ => locales}/es-ES/app-log.json (100%) rename web/i18n/{ => locales}/es-ES/app-overview.json (100%) rename web/i18n/{ => locales}/es-ES/app.json (100%) rename web/i18n/{ => locales}/es-ES/billing.json (100%) rename web/i18n/{ => locales}/es-ES/common.json (100%) rename web/i18n/{ => locales}/es-ES/custom.json (100%) rename web/i18n/{ => locales}/es-ES/dataset-creation.json (100%) rename web/i18n/{ => locales}/es-ES/dataset-documents.json (100%) rename web/i18n/{ => locales}/es-ES/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/es-ES/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/es-ES/dataset-settings.json (100%) rename web/i18n/{ => locales}/es-ES/dataset.json (100%) rename web/i18n/{ => locales}/es-ES/deployments.json (100%) rename web/i18n/{ => locales}/es-ES/device-flow.json (100%) rename web/i18n/{ => locales}/es-ES/education.json (100%) rename web/i18n/{ => locales}/es-ES/explore.json (100%) rename web/i18n/{ => locales}/es-ES/layout.json (100%) rename web/i18n/{ => locales}/es-ES/login.json (100%) rename web/i18n/{ => locales}/es-ES/oauth.json (100%) rename web/i18n/{ => locales}/es-ES/permission-keys.json (100%) rename web/i18n/{ => locales}/es-ES/permission.json (100%) rename web/i18n/{ => locales}/es-ES/pipeline.json (100%) rename web/i18n/{ => locales}/es-ES/plugin-tags.json (100%) rename web/i18n/{ => locales}/es-ES/plugin-trigger.json (100%) rename web/i18n/{ => locales}/es-ES/plugin.json (100%) rename web/i18n/{ => locales}/es-ES/register.json (100%) rename web/i18n/{ => locales}/es-ES/run-log.json (100%) rename web/i18n/{ => locales}/es-ES/share.json (100%) rename web/i18n/{ => locales}/es-ES/skill.json (100%) rename web/i18n/{ => locales}/es-ES/snippet.json (100%) rename web/i18n/{ => locales}/es-ES/time.json (100%) rename web/i18n/{ => locales}/es-ES/tools.json (100%) rename web/i18n/{ => locales}/es-ES/workflow.json (100%) rename web/i18n/{ => locales}/fa-IR/agent-v-2.json (100%) rename web/i18n/{ => locales}/fa-IR/app-annotation.json (100%) rename web/i18n/{ => locales}/fa-IR/app-api.json (100%) rename web/i18n/{ => locales}/fa-IR/app-debug.json (100%) rename web/i18n/{ => locales}/fa-IR/app-log.json (100%) rename web/i18n/{ => locales}/fa-IR/app-overview.json (100%) rename web/i18n/{ => locales}/fa-IR/app.json (100%) rename web/i18n/{ => locales}/fa-IR/billing.json (100%) rename web/i18n/{ => locales}/fa-IR/common.json (100%) rename web/i18n/{ => locales}/fa-IR/custom.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset-creation.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset-documents.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset-settings.json (100%) rename web/i18n/{ => locales}/fa-IR/dataset.json (100%) rename web/i18n/{ => locales}/fa-IR/deployments.json (100%) rename web/i18n/{ => locales}/fa-IR/device-flow.json (100%) rename web/i18n/{ => locales}/fa-IR/education.json (100%) rename web/i18n/{ => locales}/fa-IR/explore.json (100%) rename web/i18n/{ => locales}/fa-IR/layout.json (100%) rename web/i18n/{ => locales}/fa-IR/login.json (100%) rename web/i18n/{ => locales}/fa-IR/oauth.json (100%) rename web/i18n/{ => locales}/fa-IR/permission-keys.json (100%) rename web/i18n/{ => locales}/fa-IR/permission.json (100%) rename web/i18n/{ => locales}/fa-IR/pipeline.json (100%) rename web/i18n/{ => locales}/fa-IR/plugin-tags.json (100%) rename web/i18n/{ => locales}/fa-IR/plugin-trigger.json (100%) rename web/i18n/{ => locales}/fa-IR/plugin.json (100%) rename web/i18n/{ => locales}/fa-IR/register.json (100%) rename web/i18n/{ => locales}/fa-IR/run-log.json (100%) rename web/i18n/{ => locales}/fa-IR/share.json (100%) rename web/i18n/{ => locales}/fa-IR/skill.json (100%) rename web/i18n/{ => locales}/fa-IR/snippet.json (100%) rename web/i18n/{ => locales}/fa-IR/time.json (100%) rename web/i18n/{ => locales}/fa-IR/tools.json (100%) rename web/i18n/{ => locales}/fa-IR/workflow.json (100%) rename web/i18n/{ => locales}/fr-FR/agent-v-2.json (100%) rename web/i18n/{ => locales}/fr-FR/app-annotation.json (100%) rename web/i18n/{ => locales}/fr-FR/app-api.json (100%) rename web/i18n/{ => locales}/fr-FR/app-debug.json (100%) rename web/i18n/{ => locales}/fr-FR/app-log.json (100%) rename web/i18n/{ => locales}/fr-FR/app-overview.json (100%) rename web/i18n/{ => locales}/fr-FR/app.json (100%) rename web/i18n/{ => locales}/fr-FR/billing.json (100%) rename web/i18n/{ => locales}/fr-FR/common.json (100%) rename web/i18n/{ => locales}/fr-FR/custom.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset-creation.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset-documents.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset-settings.json (100%) rename web/i18n/{ => locales}/fr-FR/dataset.json (100%) rename web/i18n/{ => locales}/fr-FR/deployments.json (100%) rename web/i18n/{ => locales}/fr-FR/device-flow.json (100%) rename web/i18n/{ => locales}/fr-FR/education.json (100%) rename web/i18n/{ => locales}/fr-FR/explore.json (100%) rename web/i18n/{ => locales}/fr-FR/layout.json (100%) rename web/i18n/{ => locales}/fr-FR/login.json (100%) rename web/i18n/{ => locales}/fr-FR/oauth.json (100%) rename web/i18n/{ => locales}/fr-FR/permission-keys.json (100%) rename web/i18n/{ => locales}/fr-FR/permission.json (100%) rename web/i18n/{ => locales}/fr-FR/pipeline.json (100%) rename web/i18n/{ => locales}/fr-FR/plugin-tags.json (100%) rename web/i18n/{ => locales}/fr-FR/plugin-trigger.json (100%) rename web/i18n/{ => locales}/fr-FR/plugin.json (100%) rename web/i18n/{ => locales}/fr-FR/register.json (100%) rename web/i18n/{ => locales}/fr-FR/run-log.json (100%) rename web/i18n/{ => locales}/fr-FR/share.json (100%) rename web/i18n/{ => locales}/fr-FR/skill.json (100%) rename web/i18n/{ => locales}/fr-FR/snippet.json (100%) rename web/i18n/{ => locales}/fr-FR/time.json (100%) rename web/i18n/{ => locales}/fr-FR/tools.json (100%) rename web/i18n/{ => locales}/fr-FR/workflow.json (100%) rename web/i18n/{ => locales}/hi-IN/agent-v-2.json (100%) rename web/i18n/{ => locales}/hi-IN/app-annotation.json (100%) rename web/i18n/{ => locales}/hi-IN/app-api.json (100%) rename web/i18n/{ => locales}/hi-IN/app-debug.json (100%) rename web/i18n/{ => locales}/hi-IN/app-log.json (100%) rename web/i18n/{ => locales}/hi-IN/app-overview.json (100%) rename web/i18n/{ => locales}/hi-IN/app.json (100%) rename web/i18n/{ => locales}/hi-IN/billing.json (100%) rename web/i18n/{ => locales}/hi-IN/common.json (100%) rename web/i18n/{ => locales}/hi-IN/custom.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset-creation.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset-documents.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset-settings.json (100%) rename web/i18n/{ => locales}/hi-IN/dataset.json (100%) rename web/i18n/{ => locales}/hi-IN/deployments.json (100%) rename web/i18n/{ => locales}/hi-IN/device-flow.json (100%) rename web/i18n/{ => locales}/hi-IN/education.json (100%) rename web/i18n/{ => locales}/hi-IN/explore.json (100%) rename web/i18n/{ => locales}/hi-IN/layout.json (100%) rename web/i18n/{ => locales}/hi-IN/login.json (100%) rename web/i18n/{ => locales}/hi-IN/oauth.json (100%) rename web/i18n/{ => locales}/hi-IN/permission-keys.json (100%) rename web/i18n/{ => locales}/hi-IN/permission.json (100%) rename web/i18n/{ => locales}/hi-IN/pipeline.json (100%) rename web/i18n/{ => locales}/hi-IN/plugin-tags.json (100%) rename web/i18n/{ => locales}/hi-IN/plugin-trigger.json (100%) rename web/i18n/{ => locales}/hi-IN/plugin.json (100%) rename web/i18n/{ => locales}/hi-IN/register.json (100%) rename web/i18n/{ => locales}/hi-IN/run-log.json (100%) rename web/i18n/{ => locales}/hi-IN/share.json (100%) rename web/i18n/{ => locales}/hi-IN/skill.json (100%) rename web/i18n/{ => locales}/hi-IN/snippet.json (100%) rename web/i18n/{ => locales}/hi-IN/time.json (100%) rename web/i18n/{ => locales}/hi-IN/tools.json (100%) rename web/i18n/{ => locales}/hi-IN/workflow.json (100%) rename web/i18n/{ => locales}/id-ID/agent-v-2.json (100%) rename web/i18n/{ => locales}/id-ID/app-annotation.json (100%) rename web/i18n/{ => locales}/id-ID/app-api.json (100%) rename web/i18n/{ => locales}/id-ID/app-debug.json (100%) rename web/i18n/{ => locales}/id-ID/app-log.json (100%) rename web/i18n/{ => locales}/id-ID/app-overview.json (100%) rename web/i18n/{ => locales}/id-ID/app.json (100%) rename web/i18n/{ => locales}/id-ID/billing.json (100%) rename web/i18n/{ => locales}/id-ID/common.json (100%) rename web/i18n/{ => locales}/id-ID/custom.json (100%) rename web/i18n/{ => locales}/id-ID/dataset-creation.json (100%) rename web/i18n/{ => locales}/id-ID/dataset-documents.json (100%) rename web/i18n/{ => locales}/id-ID/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/id-ID/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/id-ID/dataset-settings.json (100%) rename web/i18n/{ => locales}/id-ID/dataset.json (100%) rename web/i18n/{ => locales}/id-ID/deployments.json (100%) rename web/i18n/{ => locales}/id-ID/device-flow.json (100%) rename web/i18n/{ => locales}/id-ID/education.json (100%) rename web/i18n/{ => locales}/id-ID/explore.json (100%) rename web/i18n/{ => locales}/id-ID/layout.json (100%) rename web/i18n/{ => locales}/id-ID/login.json (100%) rename web/i18n/{ => locales}/id-ID/oauth.json (100%) rename web/i18n/{ => locales}/id-ID/permission-keys.json (100%) rename web/i18n/{ => locales}/id-ID/permission.json (100%) rename web/i18n/{ => locales}/id-ID/pipeline.json (100%) rename web/i18n/{ => locales}/id-ID/plugin-tags.json (100%) rename web/i18n/{ => locales}/id-ID/plugin-trigger.json (100%) rename web/i18n/{ => locales}/id-ID/plugin.json (100%) rename web/i18n/{ => locales}/id-ID/register.json (100%) rename web/i18n/{ => locales}/id-ID/run-log.json (100%) rename web/i18n/{ => locales}/id-ID/share.json (100%) rename web/i18n/{ => locales}/id-ID/skill.json (100%) rename web/i18n/{ => locales}/id-ID/snippet.json (100%) rename web/i18n/{ => locales}/id-ID/time.json (100%) rename web/i18n/{ => locales}/id-ID/tools.json (100%) rename web/i18n/{ => locales}/id-ID/workflow.json (100%) rename web/i18n/{ => locales}/it-IT/agent-v-2.json (100%) rename web/i18n/{ => locales}/it-IT/app-annotation.json (100%) rename web/i18n/{ => locales}/it-IT/app-api.json (100%) rename web/i18n/{ => locales}/it-IT/app-debug.json (100%) rename web/i18n/{ => locales}/it-IT/app-log.json (100%) rename web/i18n/{ => locales}/it-IT/app-overview.json (100%) rename web/i18n/{ => locales}/it-IT/app.json (100%) rename web/i18n/{ => locales}/it-IT/billing.json (100%) rename web/i18n/{ => locales}/it-IT/common.json (100%) rename web/i18n/{ => locales}/it-IT/custom.json (100%) rename web/i18n/{ => locales}/it-IT/dataset-creation.json (100%) rename web/i18n/{ => locales}/it-IT/dataset-documents.json (100%) rename web/i18n/{ => locales}/it-IT/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/it-IT/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/it-IT/dataset-settings.json (100%) rename web/i18n/{ => locales}/it-IT/dataset.json (100%) rename web/i18n/{ => locales}/it-IT/deployments.json (100%) rename web/i18n/{ => locales}/it-IT/device-flow.json (100%) rename web/i18n/{ => locales}/it-IT/education.json (100%) rename web/i18n/{ => locales}/it-IT/explore.json (100%) rename web/i18n/{ => locales}/it-IT/layout.json (100%) rename web/i18n/{ => locales}/it-IT/login.json (100%) rename web/i18n/{ => locales}/it-IT/oauth.json (100%) rename web/i18n/{ => locales}/it-IT/permission-keys.json (100%) rename web/i18n/{ => locales}/it-IT/permission.json (100%) rename web/i18n/{ => locales}/it-IT/pipeline.json (100%) rename web/i18n/{ => locales}/it-IT/plugin-tags.json (100%) rename web/i18n/{ => locales}/it-IT/plugin-trigger.json (100%) rename web/i18n/{ => locales}/it-IT/plugin.json (100%) rename web/i18n/{ => locales}/it-IT/register.json (100%) rename web/i18n/{ => locales}/it-IT/run-log.json (100%) rename web/i18n/{ => locales}/it-IT/share.json (100%) rename web/i18n/{ => locales}/it-IT/skill.json (100%) rename web/i18n/{ => locales}/it-IT/snippet.json (100%) rename web/i18n/{ => locales}/it-IT/time.json (100%) rename web/i18n/{ => locales}/it-IT/tools.json (100%) rename web/i18n/{ => locales}/it-IT/workflow.json (100%) rename web/i18n/{ => locales}/ja-JP/agent-v-2.json (100%) rename web/i18n/{ => locales}/ja-JP/app-annotation.json (100%) rename web/i18n/{ => locales}/ja-JP/app-api.json (100%) rename web/i18n/{ => locales}/ja-JP/app-debug.json (100%) rename web/i18n/{ => locales}/ja-JP/app-log.json (100%) rename web/i18n/{ => locales}/ja-JP/app-overview.json (100%) rename web/i18n/{ => locales}/ja-JP/app.json (100%) rename web/i18n/{ => locales}/ja-JP/billing.json (100%) rename web/i18n/{ => locales}/ja-JP/common.json (100%) rename web/i18n/{ => locales}/ja-JP/custom.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset-creation.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset-documents.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset-settings.json (100%) rename web/i18n/{ => locales}/ja-JP/dataset.json (100%) rename web/i18n/{ => locales}/ja-JP/deployments.json (100%) rename web/i18n/{ => locales}/ja-JP/device-flow.json (100%) rename web/i18n/{ => locales}/ja-JP/education.json (100%) rename web/i18n/{ => locales}/ja-JP/explore.json (100%) rename web/i18n/{ => locales}/ja-JP/layout.json (100%) rename web/i18n/{ => locales}/ja-JP/login.json (100%) rename web/i18n/{ => locales}/ja-JP/oauth.json (100%) rename web/i18n/{ => locales}/ja-JP/permission-keys.json (100%) rename web/i18n/{ => locales}/ja-JP/permission.json (100%) rename web/i18n/{ => locales}/ja-JP/pipeline.json (100%) rename web/i18n/{ => locales}/ja-JP/plugin-tags.json (100%) rename web/i18n/{ => locales}/ja-JP/plugin-trigger.json (100%) rename web/i18n/{ => locales}/ja-JP/plugin.json (100%) rename web/i18n/{ => locales}/ja-JP/register.json (100%) rename web/i18n/{ => locales}/ja-JP/run-log.json (100%) rename web/i18n/{ => locales}/ja-JP/share.json (100%) rename web/i18n/{ => locales}/ja-JP/skill.json (100%) rename web/i18n/{ => locales}/ja-JP/snippet.json (100%) rename web/i18n/{ => locales}/ja-JP/time.json (100%) rename web/i18n/{ => locales}/ja-JP/tools.json (100%) rename web/i18n/{ => locales}/ja-JP/workflow.json (100%) rename web/i18n/{ => locales}/ko-KR/agent-v-2.json (100%) rename web/i18n/{ => locales}/ko-KR/app-annotation.json (100%) rename web/i18n/{ => locales}/ko-KR/app-api.json (100%) rename web/i18n/{ => locales}/ko-KR/app-debug.json (100%) rename web/i18n/{ => locales}/ko-KR/app-log.json (100%) rename web/i18n/{ => locales}/ko-KR/app-overview.json (100%) rename web/i18n/{ => locales}/ko-KR/app.json (100%) rename web/i18n/{ => locales}/ko-KR/billing.json (100%) rename web/i18n/{ => locales}/ko-KR/common.json (100%) rename web/i18n/{ => locales}/ko-KR/custom.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset-creation.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset-documents.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset-settings.json (100%) rename web/i18n/{ => locales}/ko-KR/dataset.json (100%) rename web/i18n/{ => locales}/ko-KR/deployments.json (100%) rename web/i18n/{ => locales}/ko-KR/device-flow.json (100%) rename web/i18n/{ => locales}/ko-KR/education.json (100%) rename web/i18n/{ => locales}/ko-KR/explore.json (100%) rename web/i18n/{ => locales}/ko-KR/layout.json (100%) rename web/i18n/{ => locales}/ko-KR/login.json (100%) rename web/i18n/{ => locales}/ko-KR/oauth.json (100%) rename web/i18n/{ => locales}/ko-KR/permission-keys.json (100%) rename web/i18n/{ => locales}/ko-KR/permission.json (100%) rename web/i18n/{ => locales}/ko-KR/pipeline.json (100%) rename web/i18n/{ => locales}/ko-KR/plugin-tags.json (100%) rename web/i18n/{ => locales}/ko-KR/plugin-trigger.json (100%) rename web/i18n/{ => locales}/ko-KR/plugin.json (100%) rename web/i18n/{ => locales}/ko-KR/register.json (100%) rename web/i18n/{ => locales}/ko-KR/run-log.json (100%) rename web/i18n/{ => locales}/ko-KR/share.json (100%) rename web/i18n/{ => locales}/ko-KR/skill.json (100%) rename web/i18n/{ => locales}/ko-KR/snippet.json (100%) rename web/i18n/{ => locales}/ko-KR/time.json (100%) rename web/i18n/{ => locales}/ko-KR/tools.json (100%) rename web/i18n/{ => locales}/ko-KR/workflow.json (100%) rename web/i18n/{ => locales}/lo-LA/agent-v-2.json (100%) rename web/i18n/{ => locales}/lo-LA/app-annotation.json (100%) rename web/i18n/{ => locales}/lo-LA/app-api.json (100%) rename web/i18n/{ => locales}/lo-LA/app-debug.json (100%) rename web/i18n/{ => locales}/lo-LA/app-log.json (100%) rename web/i18n/{ => locales}/lo-LA/app-overview.json (100%) rename web/i18n/{ => locales}/lo-LA/app.json (100%) rename web/i18n/{ => locales}/lo-LA/billing.json (100%) rename web/i18n/{ => locales}/lo-LA/common.json (100%) rename web/i18n/{ => locales}/lo-LA/custom.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset-creation.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset-documents.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset-settings.json (100%) rename web/i18n/{ => locales}/lo-LA/dataset.json (100%) rename web/i18n/{ => locales}/lo-LA/deployments.json (100%) rename web/i18n/{ => locales}/lo-LA/device-flow.json (100%) rename web/i18n/{ => locales}/lo-LA/education.json (100%) rename web/i18n/{ => locales}/lo-LA/explore.json (100%) rename web/i18n/{ => locales}/lo-LA/layout.json (100%) rename web/i18n/{ => locales}/lo-LA/login.json (100%) rename web/i18n/{ => locales}/lo-LA/oauth.json (100%) rename web/i18n/{ => locales}/lo-LA/permission-keys.json (100%) rename web/i18n/{ => locales}/lo-LA/permission.json (100%) rename web/i18n/{ => locales}/lo-LA/pipeline.json (100%) rename web/i18n/{ => locales}/lo-LA/plugin-tags.json (100%) rename web/i18n/{ => locales}/lo-LA/plugin-trigger.json (100%) rename web/i18n/{ => locales}/lo-LA/plugin.json (100%) rename web/i18n/{ => locales}/lo-LA/register.json (100%) rename web/i18n/{ => locales}/lo-LA/run-log.json (100%) rename web/i18n/{ => locales}/lo-LA/share.json (100%) rename web/i18n/{ => locales}/lo-LA/skill.json (100%) rename web/i18n/{ => locales}/lo-LA/snippet.json (100%) rename web/i18n/{ => locales}/lo-LA/time.json (100%) rename web/i18n/{ => locales}/lo-LA/tools.json (100%) rename web/i18n/{ => locales}/lo-LA/workflow.json (100%) rename web/i18n/{ => locales}/nl-NL/agent-v-2.json (100%) rename web/i18n/{ => locales}/nl-NL/app-annotation.json (100%) rename web/i18n/{ => locales}/nl-NL/app-api.json (100%) rename web/i18n/{ => locales}/nl-NL/app-debug.json (100%) rename web/i18n/{ => locales}/nl-NL/app-log.json (100%) rename web/i18n/{ => locales}/nl-NL/app-overview.json (100%) rename web/i18n/{ => locales}/nl-NL/app.json (100%) rename web/i18n/{ => locales}/nl-NL/billing.json (100%) rename web/i18n/{ => locales}/nl-NL/common.json (100%) rename web/i18n/{ => locales}/nl-NL/custom.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset-creation.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset-documents.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset-settings.json (100%) rename web/i18n/{ => locales}/nl-NL/dataset.json (100%) rename web/i18n/{ => locales}/nl-NL/deployments.json (100%) rename web/i18n/{ => locales}/nl-NL/device-flow.json (100%) rename web/i18n/{ => locales}/nl-NL/education.json (100%) rename web/i18n/{ => locales}/nl-NL/explore.json (100%) rename web/i18n/{ => locales}/nl-NL/layout.json (100%) rename web/i18n/{ => locales}/nl-NL/login.json (100%) rename web/i18n/{ => locales}/nl-NL/oauth.json (100%) rename web/i18n/{ => locales}/nl-NL/permission-keys.json (100%) rename web/i18n/{ => locales}/nl-NL/permission.json (100%) rename web/i18n/{ => locales}/nl-NL/pipeline.json (100%) rename web/i18n/{ => locales}/nl-NL/plugin-tags.json (100%) rename web/i18n/{ => locales}/nl-NL/plugin-trigger.json (100%) rename web/i18n/{ => locales}/nl-NL/plugin.json (100%) rename web/i18n/{ => locales}/nl-NL/register.json (100%) rename web/i18n/{ => locales}/nl-NL/run-log.json (100%) rename web/i18n/{ => locales}/nl-NL/share.json (100%) rename web/i18n/{ => locales}/nl-NL/skill.json (100%) rename web/i18n/{ => locales}/nl-NL/snippet.json (100%) rename web/i18n/{ => locales}/nl-NL/time.json (100%) rename web/i18n/{ => locales}/nl-NL/tools.json (100%) rename web/i18n/{ => locales}/nl-NL/workflow.json (100%) rename web/i18n/{ => locales}/pl-PL/agent-v-2.json (100%) rename web/i18n/{ => locales}/pl-PL/app-annotation.json (100%) rename web/i18n/{ => locales}/pl-PL/app-api.json (100%) rename web/i18n/{ => locales}/pl-PL/app-debug.json (100%) rename web/i18n/{ => locales}/pl-PL/app-log.json (100%) rename web/i18n/{ => locales}/pl-PL/app-overview.json (100%) rename web/i18n/{ => locales}/pl-PL/app.json (100%) rename web/i18n/{ => locales}/pl-PL/billing.json (100%) rename web/i18n/{ => locales}/pl-PL/common.json (100%) rename web/i18n/{ => locales}/pl-PL/custom.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset-creation.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset-documents.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset-settings.json (100%) rename web/i18n/{ => locales}/pl-PL/dataset.json (100%) rename web/i18n/{ => locales}/pl-PL/deployments.json (100%) rename web/i18n/{ => locales}/pl-PL/device-flow.json (100%) rename web/i18n/{ => locales}/pl-PL/education.json (100%) rename web/i18n/{ => locales}/pl-PL/explore.json (100%) rename web/i18n/{ => locales}/pl-PL/layout.json (100%) rename web/i18n/{ => locales}/pl-PL/login.json (100%) rename web/i18n/{ => locales}/pl-PL/oauth.json (100%) rename web/i18n/{ => locales}/pl-PL/permission-keys.json (100%) rename web/i18n/{ => locales}/pl-PL/permission.json (100%) rename web/i18n/{ => locales}/pl-PL/pipeline.json (100%) rename web/i18n/{ => locales}/pl-PL/plugin-tags.json (100%) rename web/i18n/{ => locales}/pl-PL/plugin-trigger.json (100%) rename web/i18n/{ => locales}/pl-PL/plugin.json (100%) rename web/i18n/{ => locales}/pl-PL/register.json (100%) rename web/i18n/{ => locales}/pl-PL/run-log.json (100%) rename web/i18n/{ => locales}/pl-PL/share.json (100%) rename web/i18n/{ => locales}/pl-PL/skill.json (100%) rename web/i18n/{ => locales}/pl-PL/snippet.json (100%) rename web/i18n/{ => locales}/pl-PL/time.json (100%) rename web/i18n/{ => locales}/pl-PL/tools.json (100%) rename web/i18n/{ => locales}/pl-PL/workflow.json (100%) rename web/i18n/{ => locales}/pt-BR/agent-v-2.json (100%) rename web/i18n/{ => locales}/pt-BR/app-annotation.json (100%) rename web/i18n/{ => locales}/pt-BR/app-api.json (100%) rename web/i18n/{ => locales}/pt-BR/app-debug.json (100%) rename web/i18n/{ => locales}/pt-BR/app-log.json (100%) rename web/i18n/{ => locales}/pt-BR/app-overview.json (100%) rename web/i18n/{ => locales}/pt-BR/app.json (100%) rename web/i18n/{ => locales}/pt-BR/billing.json (100%) rename web/i18n/{ => locales}/pt-BR/common.json (100%) rename web/i18n/{ => locales}/pt-BR/custom.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset-creation.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset-documents.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset-settings.json (100%) rename web/i18n/{ => locales}/pt-BR/dataset.json (100%) rename web/i18n/{ => locales}/pt-BR/deployments.json (100%) rename web/i18n/{ => locales}/pt-BR/device-flow.json (100%) rename web/i18n/{ => locales}/pt-BR/education.json (100%) rename web/i18n/{ => locales}/pt-BR/explore.json (100%) rename web/i18n/{ => locales}/pt-BR/layout.json (100%) rename web/i18n/{ => locales}/pt-BR/login.json (100%) rename web/i18n/{ => locales}/pt-BR/oauth.json (100%) rename web/i18n/{ => locales}/pt-BR/permission-keys.json (100%) rename web/i18n/{ => locales}/pt-BR/permission.json (100%) rename web/i18n/{ => locales}/pt-BR/pipeline.json (100%) rename web/i18n/{ => locales}/pt-BR/plugin-tags.json (100%) rename web/i18n/{ => locales}/pt-BR/plugin-trigger.json (100%) rename web/i18n/{ => locales}/pt-BR/plugin.json (100%) rename web/i18n/{ => locales}/pt-BR/register.json (100%) rename web/i18n/{ => locales}/pt-BR/run-log.json (100%) rename web/i18n/{ => locales}/pt-BR/share.json (100%) rename web/i18n/{ => locales}/pt-BR/skill.json (100%) rename web/i18n/{ => locales}/pt-BR/snippet.json (100%) rename web/i18n/{ => locales}/pt-BR/time.json (100%) rename web/i18n/{ => locales}/pt-BR/tools.json (100%) rename web/i18n/{ => locales}/pt-BR/workflow.json (100%) rename web/i18n/{ => locales}/ro-RO/agent-v-2.json (100%) rename web/i18n/{ => locales}/ro-RO/app-annotation.json (100%) rename web/i18n/{ => locales}/ro-RO/app-api.json (100%) rename web/i18n/{ => locales}/ro-RO/app-debug.json (100%) rename web/i18n/{ => locales}/ro-RO/app-log.json (100%) rename web/i18n/{ => locales}/ro-RO/app-overview.json (100%) rename web/i18n/{ => locales}/ro-RO/app.json (100%) rename web/i18n/{ => locales}/ro-RO/billing.json (100%) rename web/i18n/{ => locales}/ro-RO/common.json (100%) rename web/i18n/{ => locales}/ro-RO/custom.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset-creation.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset-documents.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset-settings.json (100%) rename web/i18n/{ => locales}/ro-RO/dataset.json (100%) rename web/i18n/{ => locales}/ro-RO/deployments.json (100%) rename web/i18n/{ => locales}/ro-RO/device-flow.json (100%) rename web/i18n/{ => locales}/ro-RO/education.json (100%) rename web/i18n/{ => locales}/ro-RO/explore.json (100%) rename web/i18n/{ => locales}/ro-RO/layout.json (100%) rename web/i18n/{ => locales}/ro-RO/login.json (100%) rename web/i18n/{ => locales}/ro-RO/oauth.json (100%) rename web/i18n/{ => locales}/ro-RO/permission-keys.json (100%) rename web/i18n/{ => locales}/ro-RO/permission.json (100%) rename web/i18n/{ => locales}/ro-RO/pipeline.json (100%) rename web/i18n/{ => locales}/ro-RO/plugin-tags.json (100%) rename web/i18n/{ => locales}/ro-RO/plugin-trigger.json (100%) rename web/i18n/{ => locales}/ro-RO/plugin.json (100%) rename web/i18n/{ => locales}/ro-RO/register.json (100%) rename web/i18n/{ => locales}/ro-RO/run-log.json (100%) rename web/i18n/{ => locales}/ro-RO/share.json (100%) rename web/i18n/{ => locales}/ro-RO/skill.json (100%) rename web/i18n/{ => locales}/ro-RO/snippet.json (100%) rename web/i18n/{ => locales}/ro-RO/time.json (100%) rename web/i18n/{ => locales}/ro-RO/tools.json (100%) rename web/i18n/{ => locales}/ro-RO/workflow.json (100%) rename web/i18n/{ => locales}/ru-RU/agent-v-2.json (100%) rename web/i18n/{ => locales}/ru-RU/app-annotation.json (100%) rename web/i18n/{ => locales}/ru-RU/app-api.json (100%) rename web/i18n/{ => locales}/ru-RU/app-debug.json (100%) rename web/i18n/{ => locales}/ru-RU/app-log.json (100%) rename web/i18n/{ => locales}/ru-RU/app-overview.json (100%) rename web/i18n/{ => locales}/ru-RU/app.json (100%) rename web/i18n/{ => locales}/ru-RU/billing.json (100%) rename web/i18n/{ => locales}/ru-RU/common.json (100%) rename web/i18n/{ => locales}/ru-RU/custom.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset-creation.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset-documents.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset-settings.json (100%) rename web/i18n/{ => locales}/ru-RU/dataset.json (100%) rename web/i18n/{ => locales}/ru-RU/deployments.json (100%) rename web/i18n/{ => locales}/ru-RU/device-flow.json (100%) rename web/i18n/{ => locales}/ru-RU/education.json (100%) rename web/i18n/{ => locales}/ru-RU/explore.json (100%) rename web/i18n/{ => locales}/ru-RU/layout.json (100%) rename web/i18n/{ => locales}/ru-RU/login.json (100%) rename web/i18n/{ => locales}/ru-RU/oauth.json (100%) rename web/i18n/{ => locales}/ru-RU/permission-keys.json (100%) rename web/i18n/{ => locales}/ru-RU/permission.json (100%) rename web/i18n/{ => locales}/ru-RU/pipeline.json (100%) rename web/i18n/{ => locales}/ru-RU/plugin-tags.json (100%) rename web/i18n/{ => locales}/ru-RU/plugin-trigger.json (100%) rename web/i18n/{ => locales}/ru-RU/plugin.json (100%) rename web/i18n/{ => locales}/ru-RU/register.json (100%) rename web/i18n/{ => locales}/ru-RU/run-log.json (100%) rename web/i18n/{ => locales}/ru-RU/share.json (100%) rename web/i18n/{ => locales}/ru-RU/skill.json (100%) rename web/i18n/{ => locales}/ru-RU/snippet.json (100%) rename web/i18n/{ => locales}/ru-RU/time.json (100%) rename web/i18n/{ => locales}/ru-RU/tools.json (100%) rename web/i18n/{ => locales}/ru-RU/workflow.json (100%) rename web/i18n/{ => locales}/sl-SI/agent-v-2.json (100%) rename web/i18n/{ => locales}/sl-SI/app-annotation.json (100%) rename web/i18n/{ => locales}/sl-SI/app-api.json (100%) rename web/i18n/{ => locales}/sl-SI/app-debug.json (100%) rename web/i18n/{ => locales}/sl-SI/app-log.json (100%) rename web/i18n/{ => locales}/sl-SI/app-overview.json (100%) rename web/i18n/{ => locales}/sl-SI/app.json (100%) rename web/i18n/{ => locales}/sl-SI/billing.json (100%) rename web/i18n/{ => locales}/sl-SI/common.json (100%) rename web/i18n/{ => locales}/sl-SI/custom.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset-creation.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset-documents.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset-settings.json (100%) rename web/i18n/{ => locales}/sl-SI/dataset.json (100%) rename web/i18n/{ => locales}/sl-SI/deployments.json (100%) rename web/i18n/{ => locales}/sl-SI/device-flow.json (100%) rename web/i18n/{ => locales}/sl-SI/education.json (100%) rename web/i18n/{ => locales}/sl-SI/explore.json (100%) rename web/i18n/{ => locales}/sl-SI/layout.json (100%) rename web/i18n/{ => locales}/sl-SI/login.json (100%) rename web/i18n/{ => locales}/sl-SI/oauth.json (100%) rename web/i18n/{ => locales}/sl-SI/permission-keys.json (100%) rename web/i18n/{ => locales}/sl-SI/permission.json (100%) rename web/i18n/{ => locales}/sl-SI/pipeline.json (100%) rename web/i18n/{ => locales}/sl-SI/plugin-tags.json (100%) rename web/i18n/{ => locales}/sl-SI/plugin-trigger.json (100%) rename web/i18n/{ => locales}/sl-SI/plugin.json (100%) rename web/i18n/{ => locales}/sl-SI/register.json (100%) rename web/i18n/{ => locales}/sl-SI/run-log.json (100%) rename web/i18n/{ => locales}/sl-SI/share.json (100%) rename web/i18n/{ => locales}/sl-SI/skill.json (100%) rename web/i18n/{ => locales}/sl-SI/snippet.json (100%) rename web/i18n/{ => locales}/sl-SI/time.json (100%) rename web/i18n/{ => locales}/sl-SI/tools.json (100%) rename web/i18n/{ => locales}/sl-SI/workflow.json (100%) rename web/i18n/{ => locales}/th-TH/agent-v-2.json (100%) rename web/i18n/{ => locales}/th-TH/app-annotation.json (100%) rename web/i18n/{ => locales}/th-TH/app-api.json (100%) rename web/i18n/{ => locales}/th-TH/app-debug.json (100%) rename web/i18n/{ => locales}/th-TH/app-log.json (100%) rename web/i18n/{ => locales}/th-TH/app-overview.json (100%) rename web/i18n/{ => locales}/th-TH/app.json (100%) rename web/i18n/{ => locales}/th-TH/billing.json (100%) rename web/i18n/{ => locales}/th-TH/common.json (100%) rename web/i18n/{ => locales}/th-TH/custom.json (100%) rename web/i18n/{ => locales}/th-TH/dataset-creation.json (100%) rename web/i18n/{ => locales}/th-TH/dataset-documents.json (100%) rename web/i18n/{ => locales}/th-TH/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/th-TH/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/th-TH/dataset-settings.json (100%) rename web/i18n/{ => locales}/th-TH/dataset.json (100%) rename web/i18n/{ => locales}/th-TH/deployments.json (100%) rename web/i18n/{ => locales}/th-TH/device-flow.json (100%) rename web/i18n/{ => locales}/th-TH/education.json (100%) rename web/i18n/{ => locales}/th-TH/explore.json (100%) rename web/i18n/{ => locales}/th-TH/layout.json (100%) rename web/i18n/{ => locales}/th-TH/login.json (100%) rename web/i18n/{ => locales}/th-TH/oauth.json (100%) rename web/i18n/{ => locales}/th-TH/permission-keys.json (100%) rename web/i18n/{ => locales}/th-TH/permission.json (100%) rename web/i18n/{ => locales}/th-TH/pipeline.json (100%) rename web/i18n/{ => locales}/th-TH/plugin-tags.json (100%) rename web/i18n/{ => locales}/th-TH/plugin-trigger.json (100%) rename web/i18n/{ => locales}/th-TH/plugin.json (100%) rename web/i18n/{ => locales}/th-TH/register.json (100%) rename web/i18n/{ => locales}/th-TH/run-log.json (100%) rename web/i18n/{ => locales}/th-TH/share.json (100%) rename web/i18n/{ => locales}/th-TH/skill.json (100%) rename web/i18n/{ => locales}/th-TH/snippet.json (100%) rename web/i18n/{ => locales}/th-TH/time.json (100%) rename web/i18n/{ => locales}/th-TH/tools.json (100%) rename web/i18n/{ => locales}/th-TH/workflow.json (100%) rename web/i18n/{ => locales}/tr-TR/agent-v-2.json (100%) rename web/i18n/{ => locales}/tr-TR/app-annotation.json (100%) rename web/i18n/{ => locales}/tr-TR/app-api.json (100%) rename web/i18n/{ => locales}/tr-TR/app-debug.json (100%) rename web/i18n/{ => locales}/tr-TR/app-log.json (100%) rename web/i18n/{ => locales}/tr-TR/app-overview.json (100%) rename web/i18n/{ => locales}/tr-TR/app.json (100%) rename web/i18n/{ => locales}/tr-TR/billing.json (100%) rename web/i18n/{ => locales}/tr-TR/common.json (100%) rename web/i18n/{ => locales}/tr-TR/custom.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset-creation.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset-documents.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset-settings.json (100%) rename web/i18n/{ => locales}/tr-TR/dataset.json (100%) rename web/i18n/{ => locales}/tr-TR/deployments.json (100%) rename web/i18n/{ => locales}/tr-TR/device-flow.json (100%) rename web/i18n/{ => locales}/tr-TR/education.json (100%) rename web/i18n/{ => locales}/tr-TR/explore.json (100%) rename web/i18n/{ => locales}/tr-TR/layout.json (100%) rename web/i18n/{ => locales}/tr-TR/login.json (100%) rename web/i18n/{ => locales}/tr-TR/oauth.json (100%) rename web/i18n/{ => locales}/tr-TR/permission-keys.json (100%) rename web/i18n/{ => locales}/tr-TR/permission.json (100%) rename web/i18n/{ => locales}/tr-TR/pipeline.json (100%) rename web/i18n/{ => locales}/tr-TR/plugin-tags.json (100%) rename web/i18n/{ => locales}/tr-TR/plugin-trigger.json (100%) rename web/i18n/{ => locales}/tr-TR/plugin.json (100%) rename web/i18n/{ => locales}/tr-TR/register.json (100%) rename web/i18n/{ => locales}/tr-TR/run-log.json (100%) rename web/i18n/{ => locales}/tr-TR/share.json (100%) rename web/i18n/{ => locales}/tr-TR/skill.json (100%) rename web/i18n/{ => locales}/tr-TR/snippet.json (100%) rename web/i18n/{ => locales}/tr-TR/time.json (100%) rename web/i18n/{ => locales}/tr-TR/tools.json (100%) rename web/i18n/{ => locales}/tr-TR/workflow.json (100%) rename web/i18n/{ => locales}/uk-UA/agent-v-2.json (100%) rename web/i18n/{ => locales}/uk-UA/app-annotation.json (100%) rename web/i18n/{ => locales}/uk-UA/app-api.json (100%) rename web/i18n/{ => locales}/uk-UA/app-debug.json (100%) rename web/i18n/{ => locales}/uk-UA/app-log.json (100%) rename web/i18n/{ => locales}/uk-UA/app-overview.json (100%) rename web/i18n/{ => locales}/uk-UA/app.json (100%) rename web/i18n/{ => locales}/uk-UA/billing.json (100%) rename web/i18n/{ => locales}/uk-UA/common.json (100%) rename web/i18n/{ => locales}/uk-UA/custom.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset-creation.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset-documents.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset-settings.json (100%) rename web/i18n/{ => locales}/uk-UA/dataset.json (100%) rename web/i18n/{ => locales}/uk-UA/deployments.json (100%) rename web/i18n/{ => locales}/uk-UA/device-flow.json (100%) rename web/i18n/{ => locales}/uk-UA/education.json (100%) rename web/i18n/{ => locales}/uk-UA/explore.json (100%) rename web/i18n/{ => locales}/uk-UA/layout.json (100%) rename web/i18n/{ => locales}/uk-UA/login.json (100%) rename web/i18n/{ => locales}/uk-UA/oauth.json (100%) rename web/i18n/{ => locales}/uk-UA/permission-keys.json (100%) rename web/i18n/{ => locales}/uk-UA/permission.json (100%) rename web/i18n/{ => locales}/uk-UA/pipeline.json (100%) rename web/i18n/{ => locales}/uk-UA/plugin-tags.json (100%) rename web/i18n/{ => locales}/uk-UA/plugin-trigger.json (100%) rename web/i18n/{ => locales}/uk-UA/plugin.json (100%) rename web/i18n/{ => locales}/uk-UA/register.json (100%) rename web/i18n/{ => locales}/uk-UA/run-log.json (100%) rename web/i18n/{ => locales}/uk-UA/share.json (100%) rename web/i18n/{ => locales}/uk-UA/skill.json (100%) rename web/i18n/{ => locales}/uk-UA/snippet.json (100%) rename web/i18n/{ => locales}/uk-UA/time.json (100%) rename web/i18n/{ => locales}/uk-UA/tools.json (100%) rename web/i18n/{ => locales}/uk-UA/workflow.json (100%) rename web/i18n/{ => locales}/vi-VN/agent-v-2.json (100%) rename web/i18n/{ => locales}/vi-VN/app-annotation.json (100%) rename web/i18n/{ => locales}/vi-VN/app-api.json (100%) rename web/i18n/{ => locales}/vi-VN/app-debug.json (100%) rename web/i18n/{ => locales}/vi-VN/app-log.json (100%) rename web/i18n/{ => locales}/vi-VN/app-overview.json (100%) rename web/i18n/{ => locales}/vi-VN/app.json (100%) rename web/i18n/{ => locales}/vi-VN/billing.json (100%) rename web/i18n/{ => locales}/vi-VN/common.json (100%) rename web/i18n/{ => locales}/vi-VN/custom.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset-creation.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset-documents.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset-settings.json (100%) rename web/i18n/{ => locales}/vi-VN/dataset.json (100%) rename web/i18n/{ => locales}/vi-VN/deployments.json (100%) rename web/i18n/{ => locales}/vi-VN/device-flow.json (100%) rename web/i18n/{ => locales}/vi-VN/education.json (100%) rename web/i18n/{ => locales}/vi-VN/explore.json (100%) rename web/i18n/{ => locales}/vi-VN/layout.json (100%) rename web/i18n/{ => locales}/vi-VN/login.json (100%) rename web/i18n/{ => locales}/vi-VN/oauth.json (100%) rename web/i18n/{ => locales}/vi-VN/permission-keys.json (100%) rename web/i18n/{ => locales}/vi-VN/permission.json (100%) rename web/i18n/{ => locales}/vi-VN/pipeline.json (100%) rename web/i18n/{ => locales}/vi-VN/plugin-tags.json (100%) rename web/i18n/{ => locales}/vi-VN/plugin-trigger.json (100%) rename web/i18n/{ => locales}/vi-VN/plugin.json (100%) rename web/i18n/{ => locales}/vi-VN/register.json (100%) rename web/i18n/{ => locales}/vi-VN/run-log.json (100%) rename web/i18n/{ => locales}/vi-VN/share.json (100%) rename web/i18n/{ => locales}/vi-VN/skill.json (100%) rename web/i18n/{ => locales}/vi-VN/snippet.json (100%) rename web/i18n/{ => locales}/vi-VN/time.json (100%) rename web/i18n/{ => locales}/vi-VN/tools.json (100%) rename web/i18n/{ => locales}/vi-VN/workflow.json (100%) rename web/i18n/{ => locales}/zh-Hans/agent-v-2.json (100%) rename web/i18n/{ => locales}/zh-Hans/app-annotation.json (100%) rename web/i18n/{ => locales}/zh-Hans/app-api.json (100%) rename web/i18n/{ => locales}/zh-Hans/app-debug.json (100%) rename web/i18n/{ => locales}/zh-Hans/app-log.json (100%) rename web/i18n/{ => locales}/zh-Hans/app-overview.json (100%) rename web/i18n/{ => locales}/zh-Hans/app.json (100%) rename web/i18n/{ => locales}/zh-Hans/billing.json (100%) rename web/i18n/{ => locales}/zh-Hans/common.json (100%) rename web/i18n/{ => locales}/zh-Hans/custom.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset-creation.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset-documents.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset-settings.json (100%) rename web/i18n/{ => locales}/zh-Hans/dataset.json (100%) rename web/i18n/{ => locales}/zh-Hans/deployments.json (100%) rename web/i18n/{ => locales}/zh-Hans/device-flow.json (100%) rename web/i18n/{ => locales}/zh-Hans/education.json (100%) rename web/i18n/{ => locales}/zh-Hans/explore.json (100%) rename web/i18n/{ => locales}/zh-Hans/layout.json (100%) rename web/i18n/{ => locales}/zh-Hans/login.json (100%) rename web/i18n/{ => locales}/zh-Hans/oauth.json (100%) rename web/i18n/{ => locales}/zh-Hans/permission-keys.json (100%) rename web/i18n/{ => locales}/zh-Hans/permission.json (100%) rename web/i18n/{ => locales}/zh-Hans/pipeline.json (100%) rename web/i18n/{ => locales}/zh-Hans/plugin-tags.json (100%) rename web/i18n/{ => locales}/zh-Hans/plugin-trigger.json (100%) rename web/i18n/{ => locales}/zh-Hans/plugin.json (100%) rename web/i18n/{ => locales}/zh-Hans/register.json (100%) rename web/i18n/{ => locales}/zh-Hans/run-log.json (100%) rename web/i18n/{ => locales}/zh-Hans/share.json (100%) rename web/i18n/{ => locales}/zh-Hans/skill.json (100%) rename web/i18n/{ => locales}/zh-Hans/snippet.json (100%) rename web/i18n/{ => locales}/zh-Hans/time.json (100%) rename web/i18n/{ => locales}/zh-Hans/tools.json (100%) rename web/i18n/{ => locales}/zh-Hans/workflow.json (100%) rename web/i18n/{ => locales}/zh-Hant/agent-v-2.json (100%) rename web/i18n/{ => locales}/zh-Hant/app-annotation.json (100%) rename web/i18n/{ => locales}/zh-Hant/app-api.json (100%) rename web/i18n/{ => locales}/zh-Hant/app-debug.json (100%) rename web/i18n/{ => locales}/zh-Hant/app-log.json (100%) rename web/i18n/{ => locales}/zh-Hant/app-overview.json (100%) rename web/i18n/{ => locales}/zh-Hant/app.json (100%) rename web/i18n/{ => locales}/zh-Hant/billing.json (100%) rename web/i18n/{ => locales}/zh-Hant/common.json (100%) rename web/i18n/{ => locales}/zh-Hant/custom.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset-creation.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset-documents.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset-hit-testing.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset-pipeline.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset-settings.json (100%) rename web/i18n/{ => locales}/zh-Hant/dataset.json (100%) rename web/i18n/{ => locales}/zh-Hant/deployments.json (100%) rename web/i18n/{ => locales}/zh-Hant/device-flow.json (100%) rename web/i18n/{ => locales}/zh-Hant/education.json (100%) rename web/i18n/{ => locales}/zh-Hant/explore.json (100%) rename web/i18n/{ => locales}/zh-Hant/layout.json (100%) rename web/i18n/{ => locales}/zh-Hant/login.json (100%) rename web/i18n/{ => locales}/zh-Hant/oauth.json (100%) rename web/i18n/{ => locales}/zh-Hant/permission-keys.json (100%) rename web/i18n/{ => locales}/zh-Hant/permission.json (100%) rename web/i18n/{ => locales}/zh-Hant/pipeline.json (100%) rename web/i18n/{ => locales}/zh-Hant/plugin-tags.json (100%) rename web/i18n/{ => locales}/zh-Hant/plugin-trigger.json (100%) rename web/i18n/{ => locales}/zh-Hant/plugin.json (100%) rename web/i18n/{ => locales}/zh-Hant/register.json (100%) rename web/i18n/{ => locales}/zh-Hant/run-log.json (100%) rename web/i18n/{ => locales}/zh-Hant/share.json (100%) rename web/i18n/{ => locales}/zh-Hant/skill.json (100%) rename web/i18n/{ => locales}/zh-Hant/snippet.json (100%) rename web/i18n/{ => locales}/zh-Hant/time.json (100%) rename web/i18n/{ => locales}/zh-Hant/tools.json (100%) rename web/i18n/{ => locales}/zh-Hant/workflow.json (100%) rename web/{i18n-config => i18n}/metadata.ts (100%) rename web/{i18n-config => i18n}/resources.ts (63%) rename web/{i18n-config => i18n}/server.ts (100%) rename web/{i18n-config => i18n}/settings.ts (100%) diff --git a/.github/scripts/generate-i18n-changes.mjs b/.github/scripts/generate-i18n-changes.mjs index 9fb3f979db9..5bd8eb30c26 100644 --- a/.github/scripts/generate-i18n-changes.mjs +++ b/.github/scripts/generate-i18n-changes.mjs @@ -8,7 +8,8 @@ const headSha = process.env.HEAD_SHA || '' const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean) const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json' -const englishPath = (fileStem) => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`) +const englishPath = (fileStem) => + path.join(repoRoot, 'web', 'i18n', 'locales', 'en-US', `${fileStem}.json`) const readCurrentJson = (fileStem) => { const filePath = englishPath(fileStem) @@ -21,7 +22,7 @@ const readBaseJson = (fileStem) => { if (!baseSha) return null try { - const relativePath = `web/i18n/en-US/${fileStem}.json` + const relativePath = `web/i18n/locales/en-US/${fileStem}.json` const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8', }) diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 89c06f6e5ed..b84461ce3b4 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -100,8 +100,8 @@ jobs: - 'web/app/components/workflow/hooks/use-nodes-sync-draft.ts' - 'web/app/components/workflow/nodes/_base/**' - 'web/app/components/workflow/nodes/agent-v2/**' - - 'web/i18n/en-US/agent-v-2.json' - - 'web/i18n/en-US/common.json' + - 'web/i18n/locales/en-US/agent-v-2.json' + - 'web/i18n/locales/en-US/common.json' - 'web/service/base.ts' - 'web/service/console/**' - 'web/service/share.ts' diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index f2d1114ae17..f9aef3c605a 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -69,7 +69,7 @@ jobs: /supported: true/ && value != \"en-US\" { printf \"%s \", value } - " web/i18n-config/languages.ts | sed 's/[[:space:]]*$//') + " web/i18n/languages.ts | sed 's/[[:space:]]*$//') generate_changes_json() { node .github/scripts/generate-i18n-changes.mjs @@ -110,9 +110,9 @@ jobs: elif [ "$SYNC_MODE" = "incremental" ]; then BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true) if [ -n "$BASE_SHA" ]; then - CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') + CHANGED_FILES=$(git diff --name-only --find-renames=100% --diff-filter=AMDT "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/' 2>/dev/null | sed -n '\@^web/i18n/locales/en-US/[^/]*\.json$@p' | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') else - CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') + CHANGED_FILES=$(find web/i18n/locales/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') fi elif [ "$SYNC_MODE" = "full" ]; then echo "workflow_dispatch full mode requires the files input to stay within CI limits." >&2 @@ -170,12 +170,12 @@ jobs: show_full_output: ${{ github.event_name == 'workflow_dispatch' }} prompt: | You are the i18n sync agent for the Dify repository. - Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`. + Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/locales/en-US/`. Use absolute paths at all times: - Repo root: `${{ github.workspace }}` - Web directory: `${{ github.workspace }}/web` - - Language config: `${{ github.workspace }}/web/i18n-config/languages.ts` + - Language config: `${{ github.workspace }}/web/i18n/languages.ts` Inputs: - Files in scope: `${{ steps.context.outputs.CHANGED_FILES }}` @@ -198,12 +198,12 @@ jobs: Required execution plan: 1. Resolve target languages. - Use the provided `Target languages` value as the source of truth. - - If it is unexpectedly empty, read `${{ github.workspace }}/web/i18n-config/languages.ts` and use every language with `supported: true` except `en-US`. + - If it is unexpectedly empty, read `${{ github.workspace }}/web/i18n/languages.ts` and use every language with `supported: true` except `en-US`. 2. Stay strictly in scope. - Only process the files listed in `Files in scope`. - Only process the resolved target languages, never `en-US`. - Do not touch unrelated i18n files. - - Do not modify `${{ github.workspace }}/web/i18n/en-US/`. + - Do not modify `${{ github.workspace }}/web/i18n/locales/en-US/`. 3. Resolve source changes. - If `Structured change set available` is `true`, read `/tmp/i18n-changes.json` and use it as the source of truth for file-level and key-level changes. - For each file entry: @@ -227,7 +227,7 @@ jobs: - Match the existing terminology and register used by each locale. - Prefer one Edit per file when stable, but prioritize correctness over batching. 6. Verify only the edited files. - - Run `vp fmt ` + - Run `vp fmt ` - Run `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}` - If verification fails, fix the remaining problems before continuing. 7. Stop after the scoped locale files are updated and verification passes. @@ -241,7 +241,7 @@ jobs: if: steps.context.outputs.CHANGED_FILES != '' shell: bash run: | - if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/)" ]; then + if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/locales/)" ]; then echo "has_changes=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -260,7 +260,7 @@ jobs: shell: bash run: | git -C "${{ github.workspace }}" checkout -B "${{ steps.pr_meta.outputs.branch_name }}" - git -C "${{ github.workspace }}" add web/i18n/ + git -C "${{ github.workspace }}" add web/i18n/locales/ git -C "${{ github.workspace }}" commit -m "chore(i18n): sync translations with en-US" - name: Push translation branch @@ -326,7 +326,7 @@ jobs: '## Verification', '', `- \`vp run dify-web#i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``, - `- \`vp fmt \``, + `- \`vp fmt \``, '', '## Notes', '', diff --git a/.github/workflows/trigger-i18n-sync.yml b/.github/workflows/trigger-i18n-sync.yml index 6cb096562ff..c576094d3b8 100644 --- a/.github/workflows/trigger-i18n-sync.yml +++ b/.github/workflows/trigger-i18n-sync.yml @@ -4,7 +4,7 @@ on: push: branches: [main] paths: - - 'web/i18n/en-US/*.json' + - 'web/i18n/locales/en-US/*.json' permissions: contents: write @@ -36,9 +36,9 @@ jobs: HEAD_SHA="${{ github.sha }}" if [ -n "$BASE_SHA" ]; then - CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') + CHANGED_FILES=$(git diff --name-only --find-renames=100% --diff-filter=AMDT "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/' 2>/dev/null | sed -n '\@^web/i18n/locales/en-US/[^/]*\.json$@p' | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') else - CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') + CHANGED_FILES=$(find web/i18n/locales/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') fi export BASE_SHA HEAD_SHA CHANGED_FILES diff --git a/README.md b/README.md index 410ade141eb..0b4df324ba5 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Dify welcomes contributions of all kinds: - **Code**: Read the [Contribution Guide](CONTRIBUTING.md), then browse [good first issues](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideas and feedback**: Start or join a [GitHub Discussion](https://github.com/langgenius/dify/discussions). -- **Translations**: Follow the [internationalization guide](web/i18n-config/README.md) to add or update a locale. +- **Translations**: Follow the [internationalization guide](web/i18n/README.md) to add or update a locale. - **Community**: Share the apps you build, help other users, and spread the word about Dify. ### Contributors diff --git a/docs/ar-SA/README.md b/docs/ar-SA/README.md index 47ec128c0ac..b095b2d850d 100644 --- a/docs/ar-SA/README.md +++ b/docs/ar-SA/README.md @@ -129,7 +129,7 @@ docker compose up -d - **الشفرة البرمجية**: اقرأ [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)، ثم تصفح [المشكلات المناسبة للمساهمين الجدد](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **الأفكار والملاحظات**: ابدأ أو انضم إلى [مناقشة على GitHub](https://github.com/langgenius/dify/discussions). -- **الترجمات**: اتبع [دليل التدويل](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) لإضافة لغة أو تحديثها. +- **الترجمات**: اتبع [دليل التدويل](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) لإضافة لغة أو تحديثها. - **المجتمع**: شارك التطبيقات التي تبنيها، وساعد المستخدمين الآخرين، وساهم في نشر Dify. ### المساهمون diff --git a/docs/bn-BD/README.md b/docs/bn-BD/README.md index 2e30e5ee25b..91dc71bd034 100644 --- a/docs/bn-BD/README.md +++ b/docs/bn-BD/README.md @@ -145,7 +145,7 @@ Dify সব ধরনের অবদানকে স্বাগত জান - **কোড**: [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) পড়ুন, তারপর [নতুন অবদানকারীদের উপযোগী সমস্যাগুলো](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) দেখুন। - **ধারণা ও প্রতিক্রিয়া**: একটি [GitHub আলোচনা](https://github.com/langgenius/dify/discussions) শুরু করুন বা বিদ্যমান আলোচনায় যোগ দিন। -- **অনুবাদ**: কোনো লোকেল যোগ বা হালনাগাদ করতে [আন্তর্জাতিকীকরণ নির্দেশিকা](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) অনুসরণ করুন। +- **অনুবাদ**: কোনো লোকেল যোগ বা হালনাগাদ করতে [আন্তর্জাতিকীকরণ নির্দেশিকা](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) অনুসরণ করুন। - **কমিউনিটি**: আপনার তৈরি অ্যাপ শেয়ার করুন, অন্য ব্যবহারকারীদের সাহায্য করুন এবং Dify সম্পর্কে সবাইকে জানান। ### অবদানকারীরা diff --git a/docs/de-DE/README.md b/docs/de-DE/README.md index 1608b0106e6..7f295d84534 100644 --- a/docs/de-DE/README.md +++ b/docs/de-DE/README.md @@ -144,7 +144,7 @@ Dify freut sich über Beiträge jeder Art: - **Code**: Lesen Sie den [Contribution Guide](./CONTRIBUTING.md) und stöbern Sie anschließend in den [Good First Issues](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideen und Feedback**: Starten Sie eine [GitHub-Diskussion](https://github.com/langgenius/dify/discussions) oder beteiligen Sie sich an einer bestehenden. -- **Übersetzungen**: Folgen Sie dem [Internationalisierungsleitfaden](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md), um eine Sprache hinzuzufügen oder zu aktualisieren. +- **Übersetzungen**: Folgen Sie dem [Internationalisierungsleitfaden](https://github.com/langgenius/dify/blob/main/web/i18n/README.md), um eine Sprache hinzuzufügen oder zu aktualisieren. - **Community**: Teilen Sie Ihre Apps, helfen Sie anderen Nutzern und machen Sie Dify bekannter. ### Mitwirkende diff --git a/docs/es-ES/README.md b/docs/es-ES/README.md index fb914dc1be7..f578318ba65 100644 --- a/docs/es-ES/README.md +++ b/docs/es-ES/README.md @@ -140,7 +140,7 @@ Dify acepta contribuciones de todo tipo: - **Código**: Lee la [Guía de contribución](./CONTRIBUTING.md) y consulta las [incidencias para principiantes](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideas y comentarios**: Inicia o únete a una [discusión en GitHub](https://github.com/langgenius/dify/discussions). -- **Traducciones**: Sigue la [guía de internacionalización](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) para añadir o actualizar un idioma. +- **Traducciones**: Sigue la [guía de internacionalización](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) para añadir o actualizar un idioma. - **Comunidad**: Comparte las aplicaciones que crees, ayuda a otros usuarios y da a conocer Dify. ### Contribuidores diff --git a/docs/fr-FR/README.md b/docs/fr-FR/README.md index 8d9f3941f8e..72b4fc3a7d6 100644 --- a/docs/fr-FR/README.md +++ b/docs/fr-FR/README.md @@ -140,7 +140,7 @@ Dify accueille les contributions de toutes sortes : - **Code** : consultez le [guide de contribution](./CONTRIBUTING.md), puis parcourez les [issues accessibles aux nouveaux contributeurs](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Idées et retours** : lancez ou rejoignez une [discussion GitHub](https://github.com/langgenius/dify/discussions). -- **Traductions** : suivez le [guide d’internationalisation](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) pour ajouter ou mettre à jour une langue. +- **Traductions** : suivez le [guide d’internationalisation](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) pour ajouter ou mettre à jour une langue. - **Communauté** : partagez les applications que vous créez, aidez les autres utilisateurs et faites connaître Dify. ### Contributeurs diff --git a/docs/hi-IN/README.md b/docs/hi-IN/README.md index 747987f3a47..0a927af5ecb 100644 --- a/docs/hi-IN/README.md +++ b/docs/hi-IN/README.md @@ -146,7 +146,7 @@ Dify सभी प्रकार के योगदानों का स् - **कोड**: [Contribution Guide](./CONTRIBUTING.md) पढ़ें, फिर [नए योगदानकर्ताओं के लिए उपयुक्त issues](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) देखें। - **विचार और प्रतिक्रिया**: [GitHub Discussion](https://github.com/langgenius/dify/discussions) शुरू करें या किसी मौजूदा चर्चा में शामिल हों। -- **अनुवाद**: किसी locale को जोड़ने या अपडेट करने के लिए [internationalization guide](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) का पालन करें। +- **अनुवाद**: किसी locale को जोड़ने या अपडेट करने के लिए [internationalization guide](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) का पालन करें। - **समुदाय**: अपने बनाए हुए ऐप साझा करें, अन्य उपयोगकर्ताओं की मदद करें और Dify के बारे में लोगों को बताएं। ### योगदानकर्ता diff --git a/docs/it-IT/README.md b/docs/it-IT/README.md index 0d258a07707..427d3a42382 100644 --- a/docs/it-IT/README.md +++ b/docs/it-IT/README.md @@ -144,7 +144,7 @@ Dify accoglie contributi di ogni tipo: - **Codice**: leggi la [Guida ai contributi](../../CONTRIBUTING.md), quindi consulta le [issue adatte ai nuovi contributori](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Idee e feedback**: avvia o partecipa a una [discussione su GitHub](https://github.com/langgenius/dify/discussions). -- **Traduzioni**: segui la [guida all’internazionalizzazione](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) per aggiungere o aggiornare una lingua. +- **Traduzioni**: segui la [guida all’internazionalizzazione](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) per aggiungere o aggiornare una lingua. - **Community**: condividi le app che crei, aiuta gli altri utenti e fai conoscere Dify. ### Collaboratori diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md index fe1ea07c551..10403ad6d6d 100644 --- a/docs/ja-JP/README.md +++ b/docs/ja-JP/README.md @@ -141,7 +141,7 @@ Difyでは、あらゆる形の貢献を歓迎しています。 - **コード**:[コントリビューションガイド](./CONTRIBUTING.md)を読み、[good first issue](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)を確認してください。 - **アイデアとフィードバック**:[GitHub Discussions](https://github.com/langgenius/dify/discussions)で新しい議論を始めるか、既存の議論に参加してください。 -- **翻訳**:言語の追加や更新については、[国際化ガイド](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md)に従ってください。 +- **翻訳**:言語の追加や更新については、[国際化ガイド](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)に従ってください。 - **コミュニティ**:作成したアプリを共有し、他のユーザーを支援し、Difyを広めてください。 ### 貢献者 diff --git a/docs/ko-KR/README.md b/docs/ko-KR/README.md index 40638aa2449..f49404c6d7f 100644 --- a/docs/ko-KR/README.md +++ b/docs/ko-KR/README.md @@ -134,7 +134,7 @@ Dify는 모든 형태의 기여를 환영합니다. - **코드**: [기여 가이드](./CONTRIBUTING.md)를 읽고 [good first issue](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)를 살펴보세요. - **아이디어 및 피드백**: [GitHub 토론](https://github.com/langgenius/dify/discussions)을 시작하거나 기존 토론에 참여하세요. -- **번역**: 언어를 추가하거나 업데이트하려면 [국제화 가이드](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md)를 따르세요. +- **번역**: 언어를 추가하거나 업데이트하려면 [국제화 가이드](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)를 따르세요. - **커뮤니티**: 만든 앱을 공유하고, 다른 사용자를 돕고, Dify를 널리 알려주세요. ### 기여자 diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index 7dc7026c647..7fd3d738305 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -139,7 +139,7 @@ O Dify aceita contribuições de todos os tipos: - **Código**: leia o [Guia de Contribuição](./CONTRIBUTING.md) e consulte as [issues para iniciantes](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideias e feedback**: inicie ou participe de uma [discussão no GitHub](https://github.com/langgenius/dify/discussions). -- **Traduções**: siga o [guia de internacionalização](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) para adicionar ou atualizar um idioma. +- **Traduções**: siga o [guia de internacionalização](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) para adicionar ou atualizar um idioma. - **Comunidade**: compartilhe os aplicativos que você criar, ajude outros usuários e divulgue o Dify. ### Contribuidores diff --git a/docs/sl-SI/README.md b/docs/sl-SI/README.md index 680a858634e..38c41317ecf 100644 --- a/docs/sl-SI/README.md +++ b/docs/sl-SI/README.md @@ -141,7 +141,7 @@ Dify pozdravlja vse vrste prispevkov: - **Koda**: preberite [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md), nato pa preglejte [naloge za nove sodelavce](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideje in povratne informacije**: začnite ali se pridružite [razpravi na GitHubu](https://github.com/langgenius/dify/discussions). -- **Prevodi**: sledite [vodniku za internacionalizacijo](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md), da dodate ali posodobite jezik. +- **Prevodi**: sledite [vodniku za internacionalizacijo](https://github.com/langgenius/dify/blob/main/web/i18n/README.md), da dodate ali posodobite jezik. - **Skupnost**: delite ustvarjene aplikacije, pomagajte drugim uporabnikom in razširite glas o Difyju. ### Sodelavci diff --git a/docs/tlh/README.md b/docs/tlh/README.md index df8bfd47b8d..12d639fec38 100644 --- a/docs/tlh/README.md +++ b/docs/tlh/README.md @@ -140,7 +140,7 @@ Dify welcomes contributions of all kinds: - **Code**: Read the [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md), then browse [good first issues](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ideas and feedback**: Start or join a [GitHub Discussion](https://github.com/langgenius/dify/discussions). -- **Translations**: Follow the [internationalization guide](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) to add or update a locale. +- **Translations**: Follow the [internationalization guide](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) to add or update a locale. - **Community**: Share the apps you build, help other users, and spread the word about Dify. ### Contributors diff --git a/docs/tr-TR/README.md b/docs/tr-TR/README.md index 93ef7013235..d99d0f5c02a 100644 --- a/docs/tr-TR/README.md +++ b/docs/tr-TR/README.md @@ -134,7 +134,7 @@ Dify her türlü katkıyı memnuniyetle karşılar: - **Kod**: [Katkı Kılavuzu'nu](./CONTRIBUTING.md) okuyun, ardından [yeni katkıda bulunanlara uygun sorunlara](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) göz atın. - **Fikirler ve geri bildirim**: Bir [GitHub tartışması](https://github.com/langgenius/dify/discussions) başlatın veya mevcut bir tartışmaya katılın. -- **Çeviriler**: Bir dil eklemek veya güncellemek için [uluslararasılaştırma kılavuzunu](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) izleyin. +- **Çeviriler**: Bir dil eklemek veya güncellemek için [uluslararasılaştırma kılavuzunu](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) izleyin. - **Topluluk**: Oluşturduğunuz uygulamaları paylaşın, diğer kullanıcılara yardımcı olun ve Dify'ı duyurun. ### Katkıda bulunanlar diff --git a/docs/vi-VN/README.md b/docs/vi-VN/README.md index db8d5904321..de05098833c 100644 --- a/docs/vi-VN/README.md +++ b/docs/vi-VN/README.md @@ -135,7 +135,7 @@ Dify chào đón mọi hình thức đóng góp: - **Mã nguồn**: đọc [Hướng dẫn đóng góp](./CONTRIBUTING.md), sau đó xem các [issue phù hợp cho người mới](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). - **Ý tưởng và phản hồi**: bắt đầu hoặc tham gia một [thảo luận trên GitHub](https://github.com/langgenius/dify/discussions). -- **Bản dịch**: làm theo [hướng dẫn quốc tế hóa](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md) để thêm hoặc cập nhật một ngôn ngữ. +- **Bản dịch**: làm theo [hướng dẫn quốc tế hóa](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) để thêm hoặc cập nhật một ngôn ngữ. - **Cộng đồng**: chia sẻ ứng dụng bạn xây dựng, hỗ trợ người dùng khác và giới thiệu Dify đến nhiều người hơn. ### Người đóng góp diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 5db1ed94a01..a7ef1a3102a 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -147,7 +147,7 @@ Dify 欢迎各种形式的贡献: - **代码**:阅读[贡献指南](./CONTRIBUTING.md),然后浏览[适合新手的问题](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)。 - **想法与反馈**:发起或加入 [GitHub Discussions](https://github.com/langgenius/dify/discussions)。 -- **翻译**:按照[国际化指南](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md)添加或更新语言。 +- **翻译**:按照[国际化指南](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)添加或更新语言。 - **社区**:分享您构建的应用、帮助其他用户,并向更多人介绍 Dify。 ### 贡献者 diff --git a/docs/zh-TW/README.md b/docs/zh-TW/README.md index b7682b181c6..c1418665269 100644 --- a/docs/zh-TW/README.md +++ b/docs/zh-TW/README.md @@ -145,7 +145,7 @@ Dify 歡迎各種形式的貢獻: - **程式碼**:閱讀[貢獻指南](./CONTRIBUTING.md),再瀏覽[適合新手的議題](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)。 - **想法與意見**:發起或加入 [GitHub Discussions](https://github.com/langgenius/dify/discussions)。 -- **翻譯**:依照[國際化指南](https://github.com/langgenius/dify/blob/main/web/i18n-config/README.md)新增或更新語言。 +- **翻譯**:依照[國際化指南](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)新增或更新語言。 - **社群**:分享您建立的應用程式、協助其他使用者,並向更多人介紹 Dify。 ### 貢獻者 diff --git a/eslint.config.mjs b/eslint.config.mjs index 9b72b129bd9..001db22be9a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -436,7 +436,7 @@ export default defineConfig([ }, }, { - files: ['web/i18n/**/*.json'], + files: ['web/i18n/locales/**/*.json'], plugins: { dify, }, diff --git a/knip.config.ts b/knip.config.ts index 4cf2444d674..d7bbb3a42d7 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -33,7 +33,7 @@ const config: KnipConfig = { '!vitest.setup.ts!', ], ignore: ['public/**'], - ignoreFiles: ['i18n-config/locale-resources/*.ts'], + ignoreFiles: ['i18n/locale-resources/*.ts'], ignoreDependencies: ['@iconify-json/*', '@storybook/addon-onboarding'], }, }, diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 812088ed515..3252fe193fd 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -5163,7 +5163,7 @@ "count": 4 } }, - "web/i18n-config/index.ts": { + "web/i18n/index.ts": { "no-barrel-files/no-barrel-files": { "count": 1 } diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index b9da6f7c6af..e1a1638437d 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -4,7 +4,7 @@ import { ToastHost } from '@langgenius/dify-ui/toast' import { withThemeByDataAttribute } from '@storybook/addon-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { I18nClientProvider as I18N } from '../app/components/provider/i18n' -import commonEnUS from '../i18n/en-US/common.json' +import commonEnUS from '../i18n/locales/en-US/common.json' import '../app/styles/markdown.css' import './storybook.css' diff --git a/web/AGENTS.md b/web/AGENTS.md index dc59e66c9be..c4226a6fdba 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -10,7 +10,7 @@ Web owns application-specific requirements and consumes shared architecture guid - For truncated text disclosure and native `title` decisions, follow [Truncated Text Disclosure]. - When adding or changing page layouts, navigation, or landmark elements, follow [Page landmarks] and inspect the composed page, including parent layouts. -- User-facing strings must use `web/i18n/en-US/` keys. When adding or renaming a key, update every supported locale with the correct localized value. +- User-facing strings must use `web/i18n/locales/en-US/` keys. When adding or renaming a key, update every supported locale with the correct localized value. - For new backend calls and migrated surfaces, use generated `consoleQuery` / `consoleClient` APIs from `@/service/console`. Do not add handwritten REST helpers or DTO mirrors, mock-backed app state, or direct edits to generated contracts. - Prefer `@langgenius/dify-ui/*` primitives, data attributes, and design tokens. Use the [Dify UI package index] to find a primitive; read the relevant contract directly when it is already known. Preserve a visible focus indicator on the final focusable element. - Reuse the Web `SearchInput` composite when its search, clear, and IME contract matches the feature; otherwise follow the canonical [Input Group contract]. diff --git a/web/__tests__/app-star-i18n.test.ts b/web/__tests__/app-star-i18n.test.ts index 31d60ce7f7a..93685b27315 100644 --- a/web/__tests__/app-star-i18n.test.ts +++ b/web/__tests__/app-star-i18n.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -const I18N_DIR = path.join(__dirname, '../i18n') +const I18N_DIR = path.join(__dirname, '../i18n/locales') const REQUIRED_APP_STAR_KEYS = [ 'studio.allApps', diff --git a/web/__tests__/i18n-upload-features.test.ts b/web/__tests__/i18n-upload-features.test.ts index 50aef054f0c..bd42227bb52 100644 --- a/web/__tests__/i18n-upload-features.test.ts +++ b/web/__tests__/i18n-upload-features.test.ts @@ -7,7 +7,7 @@ import fs from 'node:fs' import path from 'node:path' // Get all supported locales from the i18n directory -const I18N_DIR = path.join(__dirname, '../i18n') +const I18N_DIR = path.join(__dirname, '../i18n/locales') const getSupportedLocales = (): string[] => { return fs .readdirSync(I18N_DIR) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx index 6a111ee77d1..92dcff4064a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx @@ -1,4 +1,4 @@ -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import AppAccessConfigPage from '@/app/components/app/access-config' export type AccessConfigPageProps = { diff --git a/web/app/(commonLayout)/datasets/__tests__/document-title.spec.tsx b/web/app/(commonLayout)/datasets/__tests__/document-title.spec.tsx index c410961fc21..dd836c082e3 100644 --- a/web/app/(commonLayout)/datasets/__tests__/document-title.spec.tsx +++ b/web/app/(commonLayout)/datasets/__tests__/document-title.spec.tsx @@ -6,8 +6,8 @@ import { generateMetadata as generateNewKnowledgeMetadata } from '../new/create/ vi.mock('server-only', () => ({})) -vi.mock('@/i18n-config/server', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/server', async (importOriginal) => ({ + ...(await importOriginal()), getLocaleOnServer: async () => 'en-US', })) diff --git a/web/app/(commonLayout)/integrations/[[...slug]]/__tests__/metadata.spec.ts b/web/app/(commonLayout)/integrations/[[...slug]]/__tests__/metadata.spec.ts index de7f7a1db59..9ae932f831b 100644 --- a/web/app/(commonLayout)/integrations/[[...slug]]/__tests__/metadata.spec.ts +++ b/web/app/(commonLayout)/integrations/[[...slug]]/__tests__/metadata.spec.ts @@ -6,8 +6,8 @@ vi.mock('@/app/components/integrations', () => ({ default: () => null, })) -vi.mock('@/i18n-config/server', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/server', async (importOriginal) => ({ + ...(await importOriginal()), getLocaleOnServer: async () => 'en-US', })) diff --git a/web/app/(commonLayout)/integrations/[[...slug]]/layout.tsx b/web/app/(commonLayout)/integrations/[[...slug]]/layout.tsx index b584c3e4a03..bf996fa6214 100644 --- a/web/app/(commonLayout)/integrations/[[...slug]]/layout.tsx +++ b/web/app/(commonLayout)/integrations/[[...slug]]/layout.tsx @@ -2,7 +2,7 @@ import type { PropsWithChildren } from 'react' import type { IntegrationSection } from '@/app/components/integrations/routes' import type { Metadata } from '@/next' import { getIntegrationRouteTargetBySlug } from '@/app/components/integrations/routes' -import { getLocaleOnServer, getTranslation } from '@/i18n-config/server' +import { getLocaleOnServer, getTranslation } from '@/i18n/server' type IntegrationsRouteLayoutProps = PropsWithChildren<{ params: Promise<{ diff --git a/web/app/(commonLayout)/marketplace/creator/[uniqueHandle]/page.tsx b/web/app/(commonLayout)/marketplace/creator/[uniqueHandle]/page.tsx index 1c9c5615aa6..6b64be6d96a 100644 --- a/web/app/(commonLayout)/marketplace/creator/[uniqueHandle]/page.tsx +++ b/web/app/(commonLayout)/marketplace/creator/[uniqueHandle]/page.tsx @@ -1,7 +1,7 @@ import { loadCreatorProfile } from '@/app/components/plugins/marketplace/creator-profile/data.server' import DifyCreatorProfile from '@/app/components/plugins/marketplace/creator-profile/dify-profile' import MarketplaceInstallPermissionProvider from '@/app/components/plugins/marketplace/marketplace-install-permission-provider' -import { getLocaleOnServer } from '@/i18n-config/server' +import { getLocaleOnServer } from '@/i18n/server' import { notFound } from '@/next/navigation' type CreatorPageSearchParams = { diff --git a/web/app/(commonLayout)/skills/__tests__/metadata.spec.ts b/web/app/(commonLayout)/skills/__tests__/metadata.spec.ts index 92e3975587c..5a0c1f5f50d 100644 --- a/web/app/(commonLayout)/skills/__tests__/metadata.spec.ts +++ b/web/app/(commonLayout)/skills/__tests__/metadata.spec.ts @@ -3,8 +3,8 @@ import { generateMetadata } from '../page' vi.mock('server-only', () => ({})) -vi.mock('@/i18n-config/server', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/server', async (importOriginal) => ({ + ...(await importOriginal()), getLocaleOnServer: async () => 'en-US', })) diff --git a/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx b/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx index 2f1c403ebb0..242a96a7d43 100644 --- a/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx +++ b/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx @@ -63,7 +63,7 @@ vi.mock('@/app/components/plugins/marketplace/templates', () => ({ ), })) -vi.mock('@/i18n-config/server', () => ({ +vi.mock('@/i18n/server', () => ({ getLocaleOnServer: () => Promise.resolve('en-US'), })) diff --git a/web/app/(commonLayout)/templates/[[...category]]/page.tsx b/web/app/(commonLayout)/templates/[[...category]]/page.tsx index 3a4f554086c..3a2b1e1e8a3 100644 --- a/web/app/(commonLayout)/templates/[[...category]]/page.tsx +++ b/web/app/(commonLayout)/templates/[[...category]]/page.tsx @@ -2,7 +2,7 @@ import { MARKETPLACE_CONTAINER_ID } from '@/app/components/plugins/marketplace/c import { EmbeddedTemplatesMarketplace } from '@/app/components/plugins/marketplace/templates' import { TemplateDetailRouteProvider } from '@/app/components/plugins/marketplace/templates/template-detail-route' import { parseTemplatesRoute } from '@/app/components/plugins/marketplace/templates/template-links' -import { getLocaleOnServer } from '@/i18n-config/server' +import { getLocaleOnServer } from '@/i18n/server' import { redirect } from '@/next/navigation' type TemplatesPageProps = { diff --git a/web/app/__tests__/layout.spec.tsx b/web/app/__tests__/layout.spec.tsx index cee0bd121ed..9f93408da43 100644 --- a/web/app/__tests__/layout.spec.tsx +++ b/web/app/__tests__/layout.spec.tsx @@ -20,7 +20,7 @@ vi.mock('@/env', async (importOriginal) => { } }) -vi.mock('@/i18n-config/server', () => ({ +vi.mock('@/i18n/server', () => ({ getLocaleOnServer: async () => 'en-US', })) diff --git a/web/app/__tests__/route-metadata.spec.ts b/web/app/__tests__/route-metadata.spec.ts index b9308ee84b8..a85569332f0 100644 --- a/web/app/__tests__/route-metadata.spec.ts +++ b/web/app/__tests__/route-metadata.spec.ts @@ -18,8 +18,8 @@ import { generateMetadata as generateSignupSetPasswordMetadata } from '../signup vi.mock('server-only', () => ({})) -vi.mock('@/i18n-config/server', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/server', async (importOriginal) => ({ + ...(await importOriginal()), getLocaleOnServer: async () => 'en-US', })) diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx index 8474749131e..570b7d850f2 100644 --- a/web/app/components/app/access-config/index.tsx +++ b/web/app/components/app/access-config/index.tsx @@ -11,7 +11,7 @@ import { useStore } from '@/app/components/app/store' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { RESOURCE_ACCESS_SETTINGS_PAGE_SIZE } from '@/service/access-control/constants' import { useAppAccessRules, diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx index 2587af7eca8..9e77cfc47b2 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx @@ -1,9 +1,9 @@ import type { Mock } from 'vite-plus/test' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { render, screen } from '@testing-library/react' import * as React from 'react' import { useLocale } from '#i18n' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import CSVDownload from '../csv-downloader' vi.mock('#i18n', async (importOriginal) => ({ diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx index aa5bdcdb140..6648a6472f7 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { useCSVDownloader } from 'react-papaparse' import { useLocale } from '#i18n' import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' const CSV_TEMPLATE_QA_EN = [ ['question', 'answer'], diff --git a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx index 536fd0379d2..95a731cd7d0 100644 --- a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx @@ -1,11 +1,11 @@ import type { ComponentProps } from 'react' import type { Mock } from 'vite-plus/test' import type { AnnotationItemBasic } from '../../type' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useLocale } from '#i18n' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import HeaderOptions from '../index' diff --git a/web/app/components/app/annotation/header-opts/index.tsx b/web/app/components/app/annotation/header-opts/index.tsx index 6f43e75229e..d725b92c4c7 100644 --- a/web/app/components/app/annotation/header-opts/index.tsx +++ b/web/app/components/app/annotation/header-opts/index.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { jsonToCSV } from 'react-papaparse' import { useLocale } from '#i18n' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation' import { downloadBlob } from '@/utils/download' import AddAnnotationModal from '../add-annotation-modal' diff --git a/web/app/components/app/app-publisher/publish-with-multiple-model.tsx b/web/app/components/app/app-publisher/publish-with-multiple-model.tsx index 84ab304263f..7e3b216573e 100644 --- a/web/app/components/app/app-publisher/publish-with-multiple-model.tsx +++ b/web/app/components/app/app-publisher/publish-with-multiple-model.tsx @@ -17,7 +17,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import ModelIcon from '../../header/account-setting/model-provider-page/model-icon' diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index c3677f3f102..7538e1b68a6 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -28,7 +28,7 @@ import { AuthCategory, PluginAuthInAgent } from '@/app/components/plugins/plugin import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import { CollectionType } from '@/app/components/tools/types' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' import { fetchBuiltInToolList, fetchCustomToolList, diff --git a/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx b/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx index 9227956b34b..24eaca6b69d 100644 --- a/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx +++ b/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx @@ -6,8 +6,8 @@ import GetAutomaticRes from '../get-automatic-res' vi.mock('react-i18next', async () => { const { createReactI18nextMock } = await import('@/test/i18n-mock') - const { default: appDebug } = await import('@/i18n/en-US/app-debug.json') - const { default: common } = await import('@/i18n/en-US/common.json') + const { default: appDebug } = await import('@/i18n/locales/en-US/app-debug.json') + const { default: common } = await import('@/i18n/locales/en-US/common.json') return createReactI18nextMock({ ...appDebug, ...common }) }) diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx index e0278e5dd19..2fcc3bf14fe 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx @@ -8,16 +8,6 @@ import { render } from '@/test/console/render' import { RETRIEVE_METHOD } from '@/types/app' import SelectDataSet from '../index' -vi.mock('@/i18n-config/i18next-config', () => ({ - default: { - changeLanguage: vi.fn(), - addResourceBundle: vi.fn(), - use: vi.fn().mockReturnThis(), - init: vi.fn(), - addResource: vi.fn(), - hasResourceBundle: vi.fn().mockReturnValue(true), - }, -})) const mockUseInfiniteScroll = vi.fn() vi.mock('ahooks', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts index e832efacf6e..b020d8cadcb 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts @@ -1,6 +1,6 @@ -import type { I18nText } from '@/i18n-config/language' +import type { I18nText } from '@/i18n/language' import type { CodeBasedExtensionItem } from '@/models/common' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { withSelectorKey } from '@/test/i18n-mock' import { buildProviders, diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts b/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts index 27557168d13..165554ac3bd 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts +++ b/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts @@ -1,7 +1,7 @@ import type { SelectorTranslate } from '@/app/components/app/configuration/utils' import type { CodeBasedExtensionItem, ExternalDataTool } from '@/models/common' import { getStringSelectorTranslate } from '@/app/components/app/configuration/utils' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' const systemTypes = ['api'] as const diff --git a/web/app/components/app/deploy/index.tsx b/web/app/components/app/deploy/index.tsx index 5ab81fa74dc..d9f03ec1259 100644 --- a/web/app/components/app/deploy/index.tsx +++ b/web/app/components/app/deploy/index.tsx @@ -16,7 +16,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { getEnterpriseDocUrl } from '@/context/i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { getDocLanguage } from '@/i18n-config/language' +import { getDocLanguage } from '@/i18n/language' import dynamic from '@/next/dynamic' import { AppModeEnum } from '@/types/app' import { getAppACLCapabilities } from '@/utils/permission' diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 4f25d6fd20a..00a1b78214c 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -44,7 +44,7 @@ import { pricingQueryParser, } from '@/app/components/billing/pricing/query-params' import { deploymentEditionAtom } from '@/features/system-features/state' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import Link from '@/next/link' import { consoleQuery } from '@/service/console' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx index 12004cb7c28..e4370a88690 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx @@ -24,7 +24,7 @@ vi.mock('@/hooks/use-app-favicon', () => ({ useAppFavicon: vi.fn(), })) -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ changeLanguage: vi.fn().mockResolvedValue(undefined), })) diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index cea9d1b77aa..c1625ef99a9 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -16,7 +16,7 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' -import { changeLanguage } from '@/i18n-config/client' +import { changeLanguage } from '@/i18n/client' import { AppSourceType, delConversation, diff --git a/web/app/components/base/chat/chat/answer/agent-roster-response-content.tsx b/web/app/components/base/chat/chat/answer/agent-roster-response-content.tsx index 895f5868b78..c2ea6e10755 100644 --- a/web/app/components/base/chat/chat/answer/agent-roster-response-content.tsx +++ b/web/app/components/base/chat/chat/answer/agent-roster-response-content.tsx @@ -1,12 +1,12 @@ import type { ThoughtItem } from '@/app/components/base/chat/chat/type' import type { ChatItem } from '@/app/components/base/chat/types' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { FileList } from '@/app/components/base/file-uploader' import { Markdown } from '@/app/components/base/markdown' -import { getPluginLanguage, renderI18nObject } from '@/i18n-config/metadata' +import { getPluginLanguage, renderI18nObject } from '@/i18n/metadata' type AgentRosterResponseContentProps = { item: ChatItem diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts index b41eeec1f0e..bfa327af3e0 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts @@ -1,5 +1,5 @@ import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' -import type { Locale } from '@/i18n-config/locale' +import type { Locale } from '@/i18n/locale' import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types' import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' diff --git a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts index f0746ea8a01..d80fd04ddc2 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts +++ b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts @@ -2,7 +2,7 @@ import type { ButtonProps } from '@langgenius/dify-ui/button' import type { HumanInputFieldValue } from './field-renderer' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import type { HumanInputResolvedValue } from '@/types/workflow' import dayjs from 'dayjs' import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' diff --git a/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx b/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx index 7bed22ea171..32fe04d931a 100644 --- a/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx @@ -27,7 +27,7 @@ type InputForm = { hide?: boolean } -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ changeLanguage: vi.fn().mockResolvedValue(undefined), })) @@ -693,7 +693,7 @@ describe('useEmbeddedChatbot', () => { describe('Language settings', () => { it('should set language from URL parameters', async () => { window.history.replaceState({}, '', '/?locale=zh-Hans') - const { changeLanguage } = await import('@/i18n-config/client') + const { changeLanguage } = await import('@/i18n/client') await renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp)) @@ -703,7 +703,7 @@ describe('useEmbeddedChatbot', () => { it('should set language from system variables when URL param is missing', async () => { mockGetProcessedSystemVariablesFromUrlParams.mockResolvedValue({ locale: 'fr-FR' }) - const { changeLanguage } = await import('@/i18n-config/client') + const { changeLanguage } = await import('@/i18n/client') await renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp)) @@ -719,7 +719,7 @@ describe('useEmbeddedChatbot', () => { default_language: 'ja-JP', }, } as unknown as AppData - const { changeLanguage } = await import('@/i18n-config/client') + const { changeLanguage } = await import('@/i18n/client') await renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp)) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 52ef87d6ac3..9be176dc322 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -1,6 +1,6 @@ import type { ChatConfig, ChatItem, OnFeedback } from '../types' import type { InputValueTypes } from '@/app/components/share/text-generation/types' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import type { AppData, ConversationItem } from '@/models/share' import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' @@ -11,7 +11,7 @@ import { useConversationSelection } from '@/app/components/base/chat/storage' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' -import { changeLanguage } from '@/i18n-config/client' +import { changeLanguage } from '@/i18n/client' import { AppSourceType, updateFeedback } from '@/service/share' import { useInvalidateShareConversations, diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/form-generation.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/form-generation.spec.tsx index 39a61da434c..e2e1b84b824 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/form-generation.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/form-generation.spec.tsx @@ -1,4 +1,4 @@ -import type { I18nText } from '@/i18n-config/language' +import type { I18nText } from '@/i18n/language' import type { CodeBasedExtensionForm } from '@/models/common' import { fireEvent, render, screen } from '@testing-library/react' import FormGeneration from '../form-generation' diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index c42266f16cf..148b1a56d34 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -18,7 +18,7 @@ import { settingsQueryParser, } from '@/app/components/header/account-setting/query-params' import { useDocLink } from '@/context/i18n' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { useCodeBasedExtensions, useModelProviderDetails } from '@/service/use-common' import FormGeneration from './form-generation' import ModerationContent from './moderation-content' diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/index.spec.tsx index 6e4d2961df8..53d0ef02e25 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import { TtsAutoPlay } from '@/types/app' import { FeaturesProvider } from '../../../context' import TextToSpeech from '../index' -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ languages: [ { value: 'en-US', name: 'English', example: 'Hello world' }, { value: 'zh-Hans', name: '中文', example: '你好' }, diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx index ebd7a8b5cf7..36397b7c24b 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx @@ -41,7 +41,7 @@ vi.mock('@/next/navigation', () => ({ useParams: () => ({}), })) -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ get languages() { return mockLanguages }, diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx index 4f7ddfba655..5a923b15ab9 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx @@ -10,7 +10,7 @@ import FeatureCard from '@/app/components/base/features/new-feature-panel/featur import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings' import { FeatureEnum } from '@/app/components/base/features/types' import { TextToAudio } from '@/app/components/base/icons/src/vender/features' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import { TtsAutoPlay } from '@/types/app' type Props = Readonly<{ diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx index 7eb99ac4095..77d6ca9658e 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -20,7 +20,7 @@ import { replace } from 'string-ts' import { AudioBtn } from '@/app/components/base/audio-btn' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import { Infotip } from '@/app/components/base/infotip' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import { usePathname } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { TtsAutoPlay } from '@/types/app' diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 8a1742074a2..d93220ea3d4 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -1,6 +1,6 @@ import type { AnyFormApi, FieldValidators } from '@tanstack/react-form' import type { ForwardedRef, ReactNode } from 'react' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' export type TypeWithI18N = { en_US: T diff --git a/web/app/components/base/new-audio-button/__tests__/index.spec.tsx b/web/app/components/base/new-audio-button/__tests__/index.spec.tsx index 0e8de6b1b1a..d1c87fd4f60 100644 --- a/web/app/components/base/new-audio-button/__tests__/index.spec.tsx +++ b/web/app/components/base/new-audio-button/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event' import { createInstance } from 'i18next' import { I18nextProvider } from 'react-i18next' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import appApi from '@/i18n/en-US/app-api.json' +import appApi from '@/i18n/locales/en-US/app-api.json' import { useParams, usePathname } from '@/next/navigation' import AudioBtn from '../index' diff --git a/web/app/components/base/resize-handle/__tests__/index.spec.tsx b/web/app/components/base/resize-handle/__tests__/index.spec.tsx index 7e232417a6f..9fd4bb3b668 100644 --- a/web/app/components/base/resize-handle/__tests__/index.spec.tsx +++ b/web/app/components/base/resize-handle/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import ResizeHandle from '..' vi.mock('react-i18next', async () => { const { createReactI18nextMock } = await import('@/test/i18n-mock') - const { default: common } = await import('@/i18n/en-US/common.json') + const { default: common } = await import('@/i18n/locales/en-US/common.json') return createReactI18nextMock(common) }) diff --git a/web/app/components/billing/pricing/__tests__/footer.spec.tsx b/web/app/components/billing/pricing/__tests__/footer.spec.tsx index ea564663d04..5a1a2b09910 100644 --- a/web/app/components/billing/pricing/__tests__/footer.spec.tsx +++ b/web/app/components/billing/pricing/__tests__/footer.spec.tsx @@ -1,4 +1,4 @@ -import type { Locale } from '@/i18n-config/locale' +import type { Locale } from '@/i18n/locale' import { render, screen } from '@testing-library/react' import { PricingFooter } from '../footer' diff --git a/web/app/components/billing/pricing/footer.tsx b/web/app/components/billing/pricing/footer.tsx index b1f5f4f5652..f729dbecc90 100644 --- a/web/app/components/billing/pricing/footer.tsx +++ b/web/app/components/billing/pricing/footer.tsx @@ -1,4 +1,4 @@ -import type { Locale } from '@/i18n-config/locale' +import type { Locale } from '@/i18n/locale' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Link from '@/next/link' diff --git a/web/app/components/datasets/access-config/index.tsx b/web/app/components/datasets/access-config/index.tsx index 579c18c038d..b4ae968f9b8 100644 --- a/web/app/components/datasets/access-config/index.tsx +++ b/web/app/components/datasets/access-config/index.tsx @@ -12,7 +12,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { RESOURCE_ACCESS_SETTINGS_PAGE_SIZE } from '@/service/access-control/constants' import { useDatasetAccessRules, diff --git a/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx index 4b7f346d814..1badbede64a 100644 --- a/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useLocale } from '#i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { usePipelineTemplateList } from '@/service/use-pipeline' import CreateCard from './create-card' import TemplateCard from './template-card' diff --git a/web/app/components/datasets/create/file-uploader/__tests__/index.spec.tsx b/web/app/components/datasets/create/file-uploader/__tests__/index.spec.tsx index e2b41f00cb8..c8b503ca8e3 100644 --- a/web/app/components/datasets/create/file-uploader/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/__tests__/index.spec.tsx @@ -31,7 +31,7 @@ vi.mock('@/service/use-common', () => ({ data: { allowed_extensions: ['pdf', 'docx', 'txt'] }, }), })) -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ LanguagesSupported: ['en-US', 'zh-Hans'], })) diff --git a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx index 3d82ed33950..5586bb01ea4 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx @@ -38,7 +38,7 @@ vi.mock('@/service/use-common', () => ({ useFileUploadConfig: () => ({ data: mockFileUploadConfig }), useFileSupportTypes: () => ({ data: mockSupportTypes }), })) -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ LanguagesSupported: ['en-US', 'zh-Hans'], })) diff --git a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts index 9da58edbbf4..2606889e901 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts +++ b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { upload } from '@/service/base' import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common' import { getFileExtension } from '@/utils/format' diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index a3b7381be87..5ed0e40578b 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -10,7 +10,7 @@ import { useLocale } from '#i18n' import Divider from '@/app/components/base/divider' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { DataSourceProvider } from '@/models/common' import { ChunkingMode, ProcessMode } from '@/models/datasets' import { useFetchDefaultProcessRule } from '@/service/knowledge/use-create-dataset' diff --git a/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx b/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx index 64553d139df..d6346db4c0e 100644 --- a/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/step-two/language-select/__tests__/index.spec.tsx @@ -2,7 +2,7 @@ import type { ILanguageSelectProps } from '../index' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import LanguageSelect from '../index' const supportedLanguages = languages.filter((language) => language.supported) diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx index c127906f76e..aef2e4f1305 100644 --- a/web/app/components/datasets/create/step-two/language-select/index.tsx +++ b/web/app/components/datasets/create/step-two/language-select/index.tsx @@ -11,7 +11,7 @@ import { SelectValue, } from '@langgenius/dify-ui/select' import * as React from 'react' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' export type ILanguageSelectProps = { currentLanguage: string diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx index 58da00674f7..d6e9047723f 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx @@ -36,7 +36,7 @@ vi.mock('@/utils/format', () => ({ }, })) // Mock i18n config -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ LanguagesSupported: ['en-US', 'zh-Hans'], })) diff --git a/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-downloader.spec.tsx b/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-downloader.spec.tsx index 0ffc93d4a96..d84a53350ca 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-downloader.spec.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-downloader.spec.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import { render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { ChunkingMode } from '@/models/datasets' import CSVDownload from '../csv-downloader' diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx index 1f18f464437..da1355ea611 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { useCSVDownloader } from 'react-papaparse' import { useLocale } from '#i18n' import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { ChunkingMode } from '@/models/datasets' const CSV_TEMPLATE_QA_EN = [ diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx index 8d7d9ab77ad..777d58a9b2d 100644 --- a/web/app/components/explore/category.tsx +++ b/web/app/components/explore/category.tsx @@ -3,7 +3,7 @@ import type { AppCategory } from '@/models/explore' import { cn } from '@langgenius/dify-ui/cn' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio-group' import { useTranslation } from 'react-i18next' -import exploreI18n from '@/i18n/en-US/explore.json' +import exploreI18n from '@/i18n/locales/en-US/explore.json' type ICategoryProps = { className?: string diff --git a/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts b/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts index 16a0dc946f3..faf634ec905 100644 --- a/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts @@ -6,8 +6,8 @@ vi.mock('@/service/marketplace', () => ({ marketplaceQuery: { searchAdvanced: { queryOptions: serviceMocks.queryOptions } }, })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), renderI18nObject: vi.fn((value: Record | string, locale: string) => typeof value === 'string' ? value : value[locale] || value.en_US || '', ), diff --git a/web/app/components/goto-anything/actions/commands/__tests__/language.spec.ts b/web/app/components/goto-anything/actions/commands/__tests__/language.spec.ts index 670e58c785f..dbf84f2f351 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/language.spec.ts +++ b/web/app/components/goto-anything/actions/commands/__tests__/language.spec.ts @@ -2,7 +2,7 @@ import { registerCommands, unregisterCommands } from '../command-bus' import { languageCommand } from '../language' vi.mock('../command-bus') -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ languages: [ { value: 'en-US', name: 'English', supported: true }, { value: 'zh-Hans', name: '简体中文', supported: true }, diff --git a/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx b/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx index 90471eebb64..dbc1c3993d2 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx +++ b/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx @@ -43,7 +43,7 @@ vi.mock('next-themes', () => ({ setTheme: mockSetTheme, }), })) -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ setLocaleOnClient: mockSetLocale, })) vi.mock('@/features/agent-v2/feature-flag', () => ({ diff --git a/web/app/components/goto-anything/actions/commands/language.tsx b/web/app/components/goto-anything/actions/commands/language.tsx index 0439124f4ea..0d064c60821 100644 --- a/web/app/components/goto-anything/actions/commands/language.tsx +++ b/web/app/components/goto-anything/actions/commands/language.tsx @@ -1,7 +1,7 @@ import type { CommandSearchResult } from '../types' import type { SlashCommandHandler } from './types' import { getI18n } from 'react-i18next' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import { registerCommands, unregisterCommands } from './command-bus' // Language dependency types diff --git a/web/app/components/goto-anything/actions/commands/slash-provider.tsx b/web/app/components/goto-anything/actions/commands/slash-provider.tsx index 914bc1ff253..5589c4f83f9 100644 --- a/web/app/components/goto-anything/actions/commands/slash-provider.tsx +++ b/web/app/components/goto-anything/actions/commands/slash-provider.tsx @@ -6,7 +6,7 @@ import { ENABLE_FEATURE_PREVIEW } from '@/config' import { useDocLink } from '@/context/i18n' import { isCurrentWorkspaceDatasetOperatorAtom } from '@/context/workspace-state' import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag' -import { setLocaleOnClient } from '@/i18n-config/client' +import { setLocaleOnClient } from '@/i18n/client' import { accountCommand } from './account' import { createCommand } from './create' import { discordCommand } from './discord' diff --git a/web/app/components/goto-anything/actions/plugin.tsx b/web/app/components/goto-anything/actions/plugin.tsx index 491afa7b965..5ff0fa7ed4a 100644 --- a/web/app/components/goto-anything/actions/plugin.tsx +++ b/web/app/components/goto-anything/actions/plugin.tsx @@ -1,6 +1,6 @@ import type { Plugin } from '../../plugins/types' import type { ActionItem, PluginSearchResult } from './types' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { marketplaceQuery } from '@/service/marketplace' import Icon from '../../plugins/card/base/card-icon' import { getFormattedPlugin } from '../../plugins/marketplace/utils' diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx index 145bb6f7fb7..29a67e9ba2d 100644 --- a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx @@ -15,7 +15,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { getEnterpriseDocUrl } from '@/context/i18n' -import { getDocLanguage } from '@/i18n-config/language' +import { getDocLanguage } from '@/i18n/language' import PermissionPicker from './permission-picker' export type PermissionSetModalMode = 'create' | 'edit' | 'view' diff --git a/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx index 6285cf5503e..834b15c386a 100644 --- a/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx +++ b/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx @@ -10,7 +10,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' import AccessRuleSection from './access-rule-section' diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index b76c055545c..d808cea5fd6 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -15,7 +15,7 @@ import { workspacePermissionKeysAtom } from '@/context/permission-state' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { getAccessControlTemplateLanguage, LanguagesSupported } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage, LanguagesSupported } from '@/i18n/language' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { consoleQuery } from '@/service/console' import { useMembers } from '@/service/use-common' diff --git a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx index 03d3121c3ec..7450738a363 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx @@ -18,7 +18,7 @@ import { import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' type RoleSelectorProps = { diff --git a/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx b/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx index 93f5dbf6d65..75f455707d9 100644 --- a/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/member-details-modal/index.tsx @@ -9,7 +9,7 @@ import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { useRolesOfMember } from '@/service/access-control/use-member-roles' import AssignRolesModal from '../assign-roles-modal' import PermissionRoleChip from './permission-role-chip' diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index dfdbf8154d4..041dfe1cffd 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -22,7 +22,7 @@ import { } from '@/app/components/plugins/marketplace/hooks' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { useModalContextSelector } from '@/context/modal-context' -import { getModelLanguage } from '@/i18n-config/metadata' +import { getModelLanguage } from '@/i18n/metadata' import { fetchDefaultModal } from '@/service/common' import { consoleQuery } from '@/service/console' import { commonQueryKeys, modelProviderDetailsQueryOptions } from '@/service/use-common' diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx index 51925bfcfe1..150187af5e1 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx @@ -8,7 +8,7 @@ import type { ModelSelectorProvider } from '../model-selector/types' import { cn } from '@langgenius/dify-ui/cn' import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm' import useTheme from '@/hooks/use-theme' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { Theme } from '@/types/app' import { useLanguage } from '../hooks' diff --git a/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx index 82722aaf5ee..9807acdc3ad 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx @@ -1,7 +1,7 @@ import type { FC, PropsWithChildren } from 'react' import type { ModelSelectorModel } from '../model-selector/types' import { cn } from '@langgenius/dify-ui/cn' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { useLanguage } from '../hooks' import ModelBadge from '../model-badge' import FeatureIcon from '../model-selector/feature-icon' diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index c5851f606ab..1dd68ba7ff1 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -18,7 +18,7 @@ import { useCallback, useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { useModalContext } from '@/context/modal-context' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations' import { diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index 95bf0f00191..7712cfbbee3 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -21,7 +21,7 @@ import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/ import useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import { fetchPluginInfoFromMarketPlace } from '@/service/plugins' import { useInstallPackageFromMarketPlace } from '@/service/use-plugins' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx index 2c0405b2c9f..01949571294 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx @@ -12,7 +12,7 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import { commonQueryKeys } from '@/service/use-common' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx index d46ff856715..13dc35e40ca 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx @@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm' import { Openai } from '@/app/components/base/icons/src/vender/other' import useTheme from '@/hooks/use-theme' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { Theme } from '@/types/app' import { useLanguage } from '../hooks' diff --git a/web/app/components/header/account-setting/permissions-page/index.tsx b/web/app/components/header/account-setting/permissions-page/index.tsx index 4bc4c3ed268..dd4b1fdc7c4 100644 --- a/web/app/components/header/account-setting/permissions-page/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/index.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { useCreateWorkspaceRole, useUpdateWorkspaceRole, diff --git a/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx b/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx index 2d66c4b9ccb..740ab51a93c 100644 --- a/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx @@ -15,7 +15,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { getEnterpriseDocUrl } from '@/context/i18n' -import { getDocLanguage } from '@/i18n-config/language' +import { getDocLanguage } from '@/i18n/language' import PermissionField from './permission-field' export type RoleModalMode = 'create' | 'view' | 'edit' diff --git a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx index a4f65eebca9..494c1479bd0 100644 --- a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx @@ -3,7 +3,7 @@ import { ToastHost } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { render } from '@/test/console/render' import { timezones } from '@/utils/timezone' @@ -30,7 +30,7 @@ vi.mock('#i18n', async (importOriginal) => ({ useLocale: () => mockLocale, })) -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ setLocaleOnClient: vi.fn(), })) diff --git a/web/app/components/header/account-setting/preference-page/index.tsx b/web/app/components/header/account-setting/preference-page/index.tsx index 494207b55c3..281c09a4f9f 100644 --- a/web/app/components/header/account-setting/preference-page/index.tsx +++ b/web/app/components/header/account-setting/preference-page/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { Select, SelectContent, @@ -15,8 +15,8 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { setLocaleOnClient } from '@/i18n-config/client' -import { languages } from '@/i18n-config/language' +import { setLocaleOnClient } from '@/i18n/client' +import { languages } from '@/i18n/language' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { timezones } from '@/utils/timezone' diff --git a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx index 52338cb25c2..5445c25d58c 100644 --- a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx +++ b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx @@ -15,7 +15,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { SearchInput } from '@/app/components/base/search-input' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' type WorkspaceRoleCheckboxListProps = { diff --git a/web/app/components/integrations/tool-provider-card.tsx b/web/app/components/integrations/tool-provider-card.tsx index c22c599a289..85c4a804e28 100644 --- a/web/app/components/integrations/tool-provider-card.tsx +++ b/web/app/components/integrations/tool-provider-card.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' import Icon from '@/app/components/plugins/card/base/card-icon' import CornerMark from '@/app/components/plugins/card/base/corner-mark' import { useGetLanguage } from '@/context/i18n' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' const getCollectionPluginIdentity = (collection: Collection) => { const [org, ...nameParts] = collection.plugin_id?.split('/').filter(Boolean) ?? [] diff --git a/web/app/components/plugins/card/__tests__/index.spec.tsx b/web/app/components/plugins/card/__tests__/index.spec.tsx index 669591379d8..b505a6d070d 100644 --- a/web/app/components/plugins/card/__tests__/index.spec.tsx +++ b/web/app/components/plugins/card/__tests__/index.spec.tsx @@ -43,8 +43,8 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), renderI18nObject: (value: Record) => value['en-US'] ?? '', })) diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index cdbb6f4304c..8685e334fe5 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from '#i18n' import { useGetLanguage } from '@/context/i18n' import { currentWorkspaceIdAtom } from '@/context/workspace-state' import useTheme from '@/hooks/use-theme' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { Theme } from '@/types/app' import { formatNumber } from '@/utils/format' import Partner from '../base/badges/partner' diff --git a/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx b/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx index da95eaf2204..5d8a7526b5e 100644 --- a/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx @@ -9,7 +9,7 @@ const { mockFetchPluginBanners, mockGetLocaleOnServer } = vi.hoisted(() => ({ mockGetLocaleOnServer: vi.fn(), })) -vi.mock('@/i18n-config/server', () => ({ +vi.mock('@/i18n/server', () => ({ getLocaleOnServer: mockGetLocaleOnServer, })) diff --git a/web/app/components/plugins/marketplace/home/home-guide.tsx b/web/app/components/plugins/marketplace/home/home-guide.tsx index bceac49c2df..950a56e3a59 100644 --- a/web/app/components/plugins/marketplace/home/home-guide.tsx +++ b/web/app/components/plugins/marketplace/home/home-guide.tsx @@ -7,7 +7,7 @@ import { SubmitRequestDropdownMenu, } from '@/app/components/plugins/plugin-page/nav-operations' import { defaultDocBaseUrl } from '@/context/i18n' -import { getDocLanguage } from '@/i18n-config/language' +import { getDocLanguage } from '@/i18n/language' function MarketplaceGuide() { const { i18n } = useTranslation() diff --git a/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx b/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx index 39c2345f4cc..cd6893e2be0 100644 --- a/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx +++ b/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx @@ -25,7 +25,7 @@ import { useDebounce } from 'ahooks' import { useEffect, useRef, useState } from 'react' import { useTranslation } from '#i18n' import { MARKETPLACE_API_PREFIX } from '@/config' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { marketplaceQuery } from '@/service/marketplace' import { markMarketplaceSiteSearch } from '@/utils/marketplace-site-track' import { diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 92baa6f3380..4b0b8fe08d7 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -1,7 +1,7 @@ import type { PluginBanner } from '@dify/contracts/marketplace' import type { SearchParams } from 'nuqs' import type { MarketplaceViewProps } from './view' -import { getLocaleOnServer } from '@/i18n-config/server' +import { getLocaleOnServer } from '@/i18n/server' import { fetchPluginBanners } from './home/banners' import { HydrateQueryClient } from './hydration-server' import { prefetchMarketplaceDehydratedState } from './prefetch-marketplace-dehydrated-state' diff --git a/web/app/components/plugins/marketplace/list/__tests__/list-with-collection-layout.browser.spec.tsx b/web/app/components/plugins/marketplace/list/__tests__/list-with-collection-layout.browser.spec.tsx index ebc278238a8..aa016f45c10 100644 --- a/web/app/components/plugins/marketplace/list/__tests__/list-with-collection-layout.browser.spec.tsx +++ b/web/app/components/plugins/marketplace/list/__tests__/list-with-collection-layout.browser.spec.tsx @@ -26,8 +26,8 @@ vi.mock('#i18n', async () => { } }) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: (locale: string) => locale, })) diff --git a/web/app/components/plugins/marketplace/list/__tests__/list-with-collection.spec.tsx b/web/app/components/plugins/marketplace/list/__tests__/list-with-collection.spec.tsx index 36cf7dae10e..0073d7aba91 100644 --- a/web/app/components/plugins/marketplace/list/__tests__/list-with-collection.spec.tsx +++ b/web/app/components/plugins/marketplace/list/__tests__/list-with-collection.spec.tsx @@ -9,8 +9,8 @@ vi.mock('../../atoms', () => ({ useMarketplaceMoreClick: () => mockMoreClick, })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: (locale: string) => locale, })) diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 12ac75161fc..7a4db8ffdc3 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -11,7 +11,7 @@ import { useTags } from '@/app/components/plugins/hooks' import { useOptionalPluginInstallPermission } from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import { useGetLanguage } from '@/context/i18n' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import Link from '@/next/link' import { trackMarketplaceSiteCardClick } from '@/utils/marketplace-site-track' import MarketplaceDetailDialog from '../detail-dialog' diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index bc2d75ad941..09588c55751 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -5,7 +5,7 @@ import type { Plugin } from '@/app/components/plugins/types' import { cn } from '@langgenius/dify-ui/cn' import { useEffect, useId, useMemo, useRef, useState } from 'react' import { useLocale, useTranslation } from '#i18n' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' import { trackMarketplaceSiteEvent } from '@/utils/marketplace-site-track' import { useMarketplaceMoreClick } from '../atoms' import { MARKETPLACE_CONTAINER_ID } from '../constants' diff --git a/web/app/components/plugins/marketplace/templates/index.tsx b/web/app/components/plugins/marketplace/templates/index.tsx index da35a40ec91..b7a7f86b5b5 100644 --- a/web/app/components/plugins/marketplace/templates/index.tsx +++ b/web/app/components/plugins/marketplace/templates/index.tsx @@ -1,8 +1,8 @@ import type { MarketplaceTemplate } from '@dify/contracts/marketplace' import type { TemplateCategory } from './categories' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { cn } from '@langgenius/dify-ui/cn' -import { getTranslation } from '@/i18n-config/server' +import { getTranslation } from '@/i18n/server' import { redirect } from '@/next/navigation' import { getMarketplaceTemplateCollectionsAndTemplates, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/tts-params-panel.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/tts-params-panel.spec.tsx index f2be3c2d620..e32d13b3d29 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/tts-params-panel.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/tts-params-panel.spec.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import TTSParamsPanel from '../tts-params-panel' -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ languages: [ { value: 'en-US', name: 'English', supported: true }, { value: 'zh-Hans', name: 'Chinese', supported: true }, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx index 6f0aff384bf..e67a06e8485 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx @@ -10,7 +10,7 @@ import { import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' type Props = Readonly<{ currentModel: any diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx index 8986c4a81f0..4971c3aee09 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx @@ -2,8 +2,8 @@ import type { FC } from 'react' import type { StrategyDetail as StrategyDetailType } from '@/app/components/plugins/types' -import type { Locale } from '@/i18n-config/locale' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { Locale } from '@/i18n/locale' +import type { PluginLanguage } from '@/i18n/metadata' import { cn } from '@langgenius/dify-ui/cn' import { Drawer, diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx index 89f4e56d1ae..8201618dd62 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx @@ -1,8 +1,8 @@ 'use client' import type { StrategyDetail } from '@/app/components/plugins/types' -import type { Locale } from '@/i18n-config/locale' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { Locale } from '@/i18n/locale' +import type { PluginLanguage } from '@/i18n/metadata' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useState } from 'react' diff --git a/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx index ea107adca89..320bc968d4b 100644 --- a/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx @@ -18,8 +18,8 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), renderI18nObject: (obj: Record, locale: string) => { return obj?.[locale] || obj?.['en-US'] || '' }, diff --git a/web/app/components/plugins/plugin-page/__tests__/plugins-panel.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/plugins-panel.spec.tsx index 69af145ed3b..d2afd68aa08 100644 --- a/web/app/components/plugins/plugin-page/__tests__/plugins-panel.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/plugins-panel.spec.tsx @@ -42,8 +42,8 @@ vi.mock('@tanstack/react-query', () => ({ queryOptions: (options: unknown) => options, useSuspenseQuery: () => ({ data: mockSystemFeatures.enableMarketplace }), })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), renderI18nObject: (value: Record, locale: string) => value[locale] || '', })) diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx index f7db5146a8d..db83102ab1f 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import type { Plugin, PluginStatus } from '@/app/components/plugins/types' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { PluginLanguage } from '@/i18n/metadata' import { Button } from '@langgenius/dify-ui/button' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-item.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-item.tsx index 532036044d9..5e0a267d7ff 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-item.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-item.tsx @@ -1,6 +1,6 @@ import type { FC, ReactNode } from 'react' import type { PluginStatus } from '@/app/components/plugins/types' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { PluginLanguage } from '@/i18n/metadata' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import CardIcon from '@/app/components/plugins/card/base/card-icon' diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-section.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-section.tsx index c20c31ccc9a..c76b8c1bf6e 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-section.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/plugin-section.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import type { PluginStatus } from '@/app/components/plugins/types' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { PluginLanguage } from '@/i18n/metadata' import { ScrollArea, ScrollAreaContent, diff --git a/web/app/components/plugins/plugin-page/plugins-panel-utils.ts b/web/app/components/plugins/plugin-page/plugins-panel-utils.ts index b5a3c6b8f4d..a95959939db 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel-utils.ts +++ b/web/app/components/plugins/plugin-page/plugins-panel-utils.ts @@ -1,5 +1,5 @@ import type { Collection } from '@/app/components/tools/types' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' export const EMPTY_BUILTIN_TOOLS: Collection[] = [] diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 88b423f6a83..745f3cb5b69 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -14,7 +14,7 @@ import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/ta import ProviderDetail from '@/app/components/tools/provider/detail' import { useGetLanguage } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { normalizePluginCategoryListLanguage, useInstalledPluginList, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-item.spec.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-item.spec.tsx index 6b60659ba21..3672b220ac3 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-item.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-item.spec.tsx @@ -6,8 +6,8 @@ import ToolItem from '../tool-item' vi.mock('@/config', () => ({ MARKETPLACE_API_PREFIX: 'https://marketplace.example.com', })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), renderI18nObject: (value: Record, language: string) => value[language], })) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx index b78456767e1..b760595a4a1 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import Icon from '@/app/components/plugins/card/base/card-icon' import { MARKETPLACE_API_PREFIX } from '@/config' import { useGetLanguage } from '@/context/i18n' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' type Props = Readonly<{ payload: PluginDetail diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 86888865192..6412894bd9a 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -3,8 +3,8 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types' import type { TypeWithI18N } from '@/app/components/base/form/types' import type { ToolCredential } from '@/app/components/tools/types' -import type { Locale } from '@/i18n-config/locale' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { Locale } from '@/i18n/locale' +import type { PluginLanguage } from '@/i18n/metadata' export enum PluginCategoryEnum { tool = 'tool', diff --git a/web/app/components/provider/i18n-server.tsx b/web/app/components/provider/i18n-server.tsx index 4560a46d0ec..aa6745a2061 100644 --- a/web/app/components/provider/i18n-server.tsx +++ b/web/app/components/provider/i18n-server.tsx @@ -1,4 +1,4 @@ -import { getLocaleOnServer, getResources } from '@/i18n-config/server' +import { getLocaleOnServer, getResources } from '@/i18n/server' import { I18nClientProvider } from './i18n' export async function I18nServerProvider({ children }: { children: React.ReactNode }) { diff --git a/web/app/components/provider/i18n.tsx b/web/app/components/provider/i18n.tsx index 9c9e60a7350..ca8b41e894d 100644 --- a/web/app/components/provider/i18n.tsx +++ b/web/app/components/provider/i18n.tsx @@ -1,9 +1,9 @@ 'use client' import type { Resource } from 'i18next' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { I18nextProvider } from 'react-i18next' -import { createI18nextInstance } from '@/i18n-config/client' +import { createI18nextInstance } from '@/i18n/client' export function I18nClientProvider({ locale, diff --git a/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts b/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts index 6dc76577d0b..0a9dae58cc1 100644 --- a/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts +++ b/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts @@ -48,7 +48,7 @@ vi.mock('@/hooks/use-document-title', () => ({ default: useDocumentTitleMock, })) -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ changeLanguage: changeLanguageMock, })) diff --git a/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts b/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts index 75c853da440..90a989d2273 100644 --- a/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts +++ b/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts @@ -16,7 +16,7 @@ import { useWebAppStore } from '@/context/web-app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useAppFavicon } from '@/hooks/use-app-favicon' import useDocumentTitle from '@/hooks/use-document-title' -import { changeLanguage } from '@/i18n-config/client' +import { changeLanguage } from '@/i18n/client' import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, diff --git a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx index b70cf260e92..d32d6a315d6 100644 --- a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx @@ -24,7 +24,7 @@ import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { AuthType } from '@/app/components/tools/types' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' import { testAPIAvailable } from '@/service/tools' import ConfigCredentials from './config-credentials' diff --git a/web/app/components/tools/mcp/detail/tool-item.tsx b/web/app/components/tools/mcp/detail/tool-item.tsx index a0659a99518..b0878587baa 100644 --- a/web/app/components/tools/mcp/detail/tool-item.tsx +++ b/web/app/components/tools/mcp/detail/tool-item.tsx @@ -5,7 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop import * as React from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' type Props = Readonly<{ tool: Tool diff --git a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx index 4928aa38b4a..e3889cf7d70 100644 --- a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx +++ b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx @@ -29,8 +29,8 @@ vi.mock('@/context/i18n', () => ({ })) // Mock getPluginLanguage -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: () => 'en-US', })) diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index ce7e0c34d6f..a5bbc4f1576 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -48,8 +48,8 @@ const render = (ui: ReactElement) => { return renderWithConsoleQuery(ui, { queryClient }) } -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: () => 'en_US', })) diff --git a/web/app/components/tools/provider/__tests__/tool-item.spec.tsx b/web/app/components/tools/provider/__tests__/tool-item.spec.tsx index 0b11a7fb5f1..459783b646f 100644 --- a/web/app/components/tools/provider/__tests__/tool-item.spec.tsx +++ b/web/app/components/tools/provider/__tests__/tool-item.spec.tsx @@ -4,8 +4,8 @@ import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vite-plus/test' import ToolItem from '../tool-item' -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: () => 'en_US', })) vi.mock( diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index 4dd22f1ea29..6678b7ac43e 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -47,7 +47,7 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool' import { useModalContext } from '@/context/modal-context' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import { deleteWorkflowTool, diff --git a/web/app/components/tools/provider/tool-item.tsx b/web/app/components/tools/provider/tool-item.tsx index a226c1ad003..eb1bd73307b 100644 --- a/web/app/components/tools/provider/tool-item.tsx +++ b/web/app/components/tools/provider/tool-item.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useState } from 'react' import { useLocale } from '#i18n' import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getPluginLanguage } from '@/i18n/metadata' type Props = Readonly<{ disabled?: boolean diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 0c20b74f699..fa96c308dfe 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -4,7 +4,7 @@ import type { TFunction } from 'i18next' import type { ToolWithProvider } from '../types' import type { ToolDefaultValue, ToolValue } from './types' import type { Plugin } from '@/app/components/plugins/types' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { PluginLanguage } from '@/i18n/metadata' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index dd054f391bb..dde08397a54 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -4,7 +4,7 @@ import type { TFunction } from 'i18next' import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item' import type { TriggerDefaultValue, TriggerWithProvider } from './types' import type { Plugin } from '@/app/components/plugins/types' -import type { PluginLanguage } from '@/i18n-config/metadata' +import type { PluginLanguage } from '@/i18n/metadata' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts index 6920678ef33..1e0bf856519 100644 --- a/web/app/components/workflow/nodes/agent/default.ts +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -3,7 +3,7 @@ import type { NodeDefault } from '../../types' import type { AgentNodeType } from './types' import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { BlockEnum } from '../../types' import { genNodeMetaData } from '../../utils' diff --git a/web/app/components/workflow/nodes/document-extractor/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/document-extractor/__tests__/integration.spec.tsx index b3b6470ce7d..a0514acfdf0 100644 --- a/web/app/components/workflow/nodes/document-extractor/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/document-extractor/__tests__/integration.spec.tsx @@ -3,7 +3,7 @@ import type { DocExtractorNodeType } from '../types' import type { PanelProps } from '@/types/workflow' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { BlockEnum } from '../../../types' import Node from '../node' import Panel from '../panel' diff --git a/web/app/components/workflow/nodes/document-extractor/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/document-extractor/__tests__/panel.spec.tsx index 3302087d4b5..0f439ad7b88 100644 --- a/web/app/components/workflow/nodes/document-extractor/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/document-extractor/__tests__/panel.spec.tsx @@ -3,7 +3,7 @@ import type { DocExtractorNodeType } from '../types' import type { PanelProps } from '@/types/workflow' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { BlockEnum } from '../../../types' import Panel from '../panel' import useConfig from '../use-config' diff --git a/web/app/components/workflow/nodes/document-extractor/panel.tsx b/web/app/components/workflow/nodes/document-extractor/panel.tsx index ab684415e1f..fc3952703bb 100644 --- a/web/app/components/workflow/nodes/document-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/document-extractor/panel.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Field from '@/app/components/workflow/nodes/_base/components/field' import { BlockEnum } from '@/app/components/workflow/types' -import { LanguagesSupported } from '@/i18n-config/language' +import { LanguagesSupported } from '@/i18n/language' import { useFileSupportTypes } from '@/service/use-common' import OutputVars, { VarItem } from '../_base/components/output-vars' import Split from '../_base/components/split' diff --git a/web/app/components/workflow/nodes/knowledge-base/node.tsx b/web/app/components/workflow/nodes/knowledge-base/node.tsx index 504b13e4c9c..e51e73e8554 100644 --- a/web/app/components/workflow/nodes/knowledge-base/node.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/node.tsx @@ -9,7 +9,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro import { DERIVED_MODEL_STATUS_BADGE_I18N } from '@/app/components/header/account-setting/model-provider-page/derive-model-status' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils' -import { renderI18nObject } from '@/i18n-config/metadata' +import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' import { useEmbeddingModelStatus } from './hooks/use-embedding-model-status' import { useSettingsDisplay } from './hooks/use-settings-display' diff --git a/web/app/device/_header.tsx b/web/app/device/_header.tsx index 2796f3f005f..ed40bfb6646 100644 --- a/web/app/device/_header.tsx +++ b/web/app/device/_header.tsx @@ -5,8 +5,8 @@ import Divider from '@/app/components/base/divider' import { DifyLogo } from '@/app/components/base/logo/dify-logo' import LocaleMenu from '@/app/signin/_locale-menu' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { setLocaleOnClient } from '@/i18n-config/client' -import { languages } from '@/i18n-config/language' +import { setLocaleOnClient } from '@/i18n/client' +import { languages } from '@/i18n/language' import dynamic from '@/next/dynamic' const ThemeSelector = dynamic(() => import('@/app/components/base/theme-selector'), { diff --git a/web/app/device/utils/error-copy.ts b/web/app/device/utils/error-copy.ts index fee7a08dcfb..cc0c337c342 100644 --- a/web/app/device/utils/error-copy.ts +++ b/web/app/device/utils/error-copy.ts @@ -4,7 +4,7 @@ // this maps a server code to an i18n key, then the key to translated copy. import type { TFunction } from 'i18next' -import type deviceFlowResources from '@/i18n/en-US/device-flow.json' +import type deviceFlowResources from '@/i18n/locales/en-US/device-flow.json' import { DeviceFlowError } from '@/service/device-flow' type DeviceFlowKey = keyof typeof deviceFlowResources diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 50f9e13370a..935875cc1e2 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -13,7 +13,7 @@ import { dehydrateSystemFeatures, getOptionalSystemFeatures, } from '@/features/system-features/server' -import { getLocaleOnServer } from '@/i18n-config/server' +import { getLocaleOnServer } from '@/i18n/server' import { headers } from '@/next/headers' import { getApplicationTitle } from '@/utils/document-title' import { basePath } from '@/utils/var' diff --git a/web/app/route-metadata.ts b/web/app/route-metadata.ts index c0baa53016e..d4b939e3c71 100644 --- a/web/app/route-metadata.ts +++ b/web/app/route-metadata.ts @@ -1,7 +1,7 @@ import type { SelectorParam } from 'i18next' -import type { Namespace } from '@/i18n-config/resources' +import type { Namespace } from '@/i18n/resources' import type { Metadata } from '@/next' -import { getLocaleOnServer, getTranslation } from '@/i18n-config/server' +import { getLocaleOnServer, getTranslation } from '@/i18n/server' import 'server-only' export async function getRouteMetadata( diff --git a/web/app/signin/__tests__/_header.spec.tsx b/web/app/signin/__tests__/_header.spec.tsx index 86778357cd3..e6190795048 100644 --- a/web/app/signin/__tests__/_header.spec.tsx +++ b/web/app/signin/__tests__/_header.spec.tsx @@ -1,9 +1,9 @@ import { fireEvent, screen } from '@testing-library/react' -import { setLocaleOnClient } from '@/i18n-config/client' +import { setLocaleOnClient } from '@/i18n/client' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import Header from '../_header' -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ setLocaleOnClient: vi.fn(), })) diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx index 9e309c5c902..3b6be586226 100644 --- a/web/app/signin/_header.tsx +++ b/web/app/signin/_header.tsx @@ -4,8 +4,8 @@ import { useLocale } from '#i18n' import Divider from '@/app/components/base/divider' import { DifyLogo } from '@/app/components/base/logo/dify-logo' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { setLocaleOnClient } from '@/i18n-config/client' -import { languages } from '@/i18n-config/language' +import { setLocaleOnClient } from '@/i18n/client' +import { languages } from '@/i18n/language' import dynamic from '@/next/dynamic' import LocaleMenu from './_locale-menu' diff --git a/web/app/signin/invite-settings/__tests__/page.spec.tsx b/web/app/signin/invite-settings/__tests__/page.spec.tsx index 52ff5a09e10..ff98926162e 100644 --- a/web/app/signin/invite-settings/__tests__/page.spec.tsx +++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx @@ -47,13 +47,13 @@ vi.mock('#i18n', async (importOriginal) => ({ useLocale: vi.fn(), })) -vi.mock('@/i18n-config', () => ({ +vi.mock('@/i18n', () => ({ i18n: { defaultLocale: 'en-US', }, })) -vi.mock('@/i18n-config/client', () => ({ +vi.mock('@/i18n/client', () => ({ setLocaleOnClient: vi.fn(() => Promise.resolve()), })) diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 1d891a7d8e7..bd00ead67b8 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,5 +1,5 @@ 'use client' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' @@ -23,9 +23,9 @@ import { LICENSE_LINK } from '@/constants/link' import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' -import { i18n } from '@/i18n-config' -import { setLocaleOnClient } from '@/i18n-config/client' -import { languages } from '@/i18n-config/language' +import { i18n } from '@/i18n' +import { setLocaleOnClient } from '@/i18n/client' +import { languages } from '@/i18n/language' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' import { activateMember } from '@/service/common' diff --git a/web/app/signin/one-more-step.tsx b/web/app/signin/one-more-step.tsx index 2e885e486ea..3a553c6037b 100644 --- a/web/app/signin/one-more-step.tsx +++ b/web/app/signin/one-more-step.tsx @@ -20,7 +20,7 @@ import { useReducer } from 'react' import { useTranslation } from 'react-i18next' import { resolvePostLoginRedirect } from '@/app/signin/utils/post-login-redirect' import { LICENSE_LINK } from '@/constants/link' -import { languages } from '@/i18n-config/language' +import { languages } from '@/i18n/language' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' diff --git a/web/context/i18n.spec.ts b/web/context/i18n.spec.ts index ed8b1b1351d..bbcf0b47c70 100644 --- a/web/context/i18n.spec.ts +++ b/web/context/i18n.spec.ts @@ -2,7 +2,7 @@ import type { DocPathMap } from './i18n' import type { DocPathWithoutLang } from '@/types/doc-paths' import { renderHook } from '@testing-library/react' import { useLocale } from '#i18n' -import { getDocLanguage } from '@/i18n-config/language' +import { getDocLanguage } from '@/i18n/language' import { defaultDocBaseUrl, enterpriseDocBaseUrl, useDocLink } from './i18n' const mockDeploymentEdition = vi.hoisted(() => ({ @@ -19,7 +19,7 @@ vi.mock('jotai', async (importOriginal) => ({ useAtomValue: () => mockDeploymentEdition.value, })) -vi.mock('@/i18n-config/language', () => ({ +vi.mock('@/i18n/language', () => ({ getDocLanguage: vi.fn((locale: string) => { const map: Record = { 'zh-Hans': 'zh', @@ -30,8 +30,8 @@ vi.mock('@/i18n-config/language', () => ({ }), })) -vi.mock('@/i18n-config/metadata', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@/i18n/metadata', async (importOriginal) => ({ + ...(await importOriginal()), getPluginLanguage: vi.fn(), })) diff --git a/web/context/i18n.ts b/web/context/i18n.ts index 376731b8558..9f4ee1d1630 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -1,12 +1,12 @@ import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' -import type { Locale } from '@/i18n-config/locale' +import type { Locale } from '@/i18n/locale' import type { DocLanguage, DocPathWithoutLang, DocsProduct } from '@/types/doc-paths' import { useAtomValue } from 'jotai' import { useCallback } from 'react' import { useLocale } from '#i18n' import { deploymentEditionAtom } from '@/features/system-features/state' -import { getDocLanguage } from '@/i18n-config/language' -import { getPluginLanguage } from '@/i18n-config/metadata' +import { getDocLanguage } from '@/i18n/language' +import { getPluginLanguage } from '@/i18n/metadata' import { docPathProductAvailability, isProductlessDocPath } from '@/types/doc-paths' export const useGetLanguage = () => { diff --git a/web/features/agent-v2/agent-detail/access-config/page.tsx b/web/features/agent-v2/agent-detail/access-config/page.tsx index 89864e98863..2a4275f3feb 100644 --- a/web/features/agent-v2/agent-detail/access-config/page.tsx +++ b/web/features/agent-v2/agent-detail/access-config/page.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import AccessRulesEditor from '@/app/components/access-rules-editor' import { useAgentPermissions } from '@/features/agent-v2/permissions' -import { getAccessControlTemplateLanguage } from '@/i18n-config/language' +import { getAccessControlTemplateLanguage } from '@/i18n/language' import { RESOURCE_ACCESS_SETTINGS_PAGE_SIZE } from '@/service/access-control/constants' import { consoleQuery } from '@/service/console' diff --git a/web/features/agent-v2/roster/components/__tests__/delete-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/delete-agent-dialog.spec.tsx index 7ed8e40a8b9..07a46921c97 100644 --- a/web/features/agent-v2/roster/components/__tests__/delete-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/delete-agent-dialog.spec.tsx @@ -24,7 +24,7 @@ vi.mock('@/service/console', () => ({ vi.mock('react-i18next', async () => { const { createReactI18nextMock } = await import('@/test/i18n-mock') - const { default: agentV2 } = await import('@/i18n/en-US/agent-v-2.json') + const { default: agentV2 } = await import('@/i18n/locales/en-US/agent-v-2.json') return createReactI18nextMock({ 'roster.deleteDialog.description': agentV2['roster.deleteDialog.description'], 'roster.deleteDialog.title': agentV2['roster.deleteDialog.title'], diff --git a/web/features/home/page.tsx b/web/features/home/page.tsx index 31b6fb8ac0a..5e734844b20 100644 --- a/web/features/home/page.tsx +++ b/web/features/home/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, noop } from '@tanstack/react-query' import { Suspense } from 'react' import { getQueryClient } from '@/app/get-query-client' import { getOptionalSystemFeatures } from '@/features/system-features/server' -import { getLocaleOnServer } from '@/i18n-config/server' +import { getLocaleOnServer } from '@/i18n/server' import { consoleQuery } from '@/service/console' import { HomeContent } from './home-content/home-content' import { HomeShell } from './home-shell' diff --git a/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx index b6da837d90c..7b2f412be40 100644 --- a/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx +++ b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx @@ -8,8 +8,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createInstance } from 'i18next' -import { getInitOptions } from '@/i18n-config/settings' -import datasetTranslations from '@/i18n/en-US/dataset.json' +import datasetTranslations from '@/i18n/locales/en-US/dataset.json' +import { getInitOptions } from '@/i18n/settings' import { render } from '@/test/console/render' import { CrawlSelectionForm } from '../crawl-selection-form' diff --git a/web/features/new-rag/__tests__/sources-page.spec.tsx b/web/features/new-rag/__tests__/sources-page.spec.tsx index 494a8fbc5df..cd31c3a7d0f 100644 --- a/web/features/new-rag/__tests__/sources-page.spec.tsx +++ b/web/features/new-rag/__tests__/sources-page.spec.tsx @@ -1,7 +1,7 @@ import type { Source } from '@dify/contracts/knowledge-fs/types.gen' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import datasetTranslations from '@/i18n/en-US/dataset.json' +import datasetTranslations from '@/i18n/locales/en-US/dataset.json' import { render } from '@/test/console/render' import { SourcesPage } from '../sources-page' diff --git a/web/hooks/use-format-time-from-now.ts b/web/hooks/use-format-time-from-now.ts index e08418fd32d..a245758eb10 100644 --- a/web/hooks/use-format-time-from-now.ts +++ b/web/hooks/use-format-time-from-now.ts @@ -2,7 +2,7 @@ import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { useCallback } from 'react' import { useLocale } from '#i18n' -import { localeMap } from '@/i18n-config/language' +import { localeMap } from '@/i18n/language' import 'dayjs/locale/az' import 'dayjs/locale/de' import 'dayjs/locale/es' diff --git a/web/hooks/use-i18n.ts b/web/hooks/use-i18n.ts index 85fc7682f83..326750562ef 100644 --- a/web/hooks/use-i18n.ts +++ b/web/hooks/use-i18n.ts @@ -1,5 +1,5 @@ import { useLocale } from '#i18n' -import { getModelLanguage, renderI18nObject } from '@/i18n-config/metadata' +import { getModelLanguage, renderI18nObject } from '@/i18n/metadata' export const useRenderI18nObject = () => { const language = getModelLanguage(useLocale()) diff --git a/web/i18n-config/README.md b/web/i18n/README.md similarity index 80% rename from web/i18n-config/README.md rename to web/i18n/README.md index af063bca2b3..632a41e9e7d 100644 --- a/web/i18n-config/README.md +++ b/web/i18n/README.md @@ -1,6 +1,8 @@ # Internationalization -English JSON files under `web/i18n/en-US/` are the source locale. Other locale directories must keep the same flat keys and placeholders. i18next uses `keySeparator: false`, so dots are part of a key rather than nested-object separators. +English JSON files under `web/i18n/locales/en-US/` are the source locale. Other locale directories must keep the same flat keys and placeholders. i18next uses `keySeparator: false`, so dots are part of a key rather than nested-object separators. + +The module keeps runtime code and bundled translation assets together. `#i18n` remains the conditional client/server entrypoint; `locales/` contains translation JSON only. Per-locale loaders preserve the existing dynamic-import boundaries. This layout is a project ownership convention, not an i18next requirement. ## Owners @@ -21,7 +23,7 @@ Do not copy the language registry into documentation. Read the current source fi ## Add a locale 1. Add the locale metadata to `languages.ts`. -2. Add a matching `web/i18n//` directory with every source namespace. +2. Add a matching `web/i18n/locales//` directory with every source namespace. 3. Add `locale-resources/.ts` and the required mappings in `language.ts`. 4. Keep the backend language and timezone registry in `api/constants/languages.py` aligned when the locale is accepted by backend APIs. 5. Run the complete i18n check before submitting the change. @@ -70,6 +72,6 @@ not load Vite plugins. ## Automated translation -Changes to `web/i18n/en-US/*.json` on `main` trigger the scoped translation workflow. The workflow derives target locales from `languages.ts`, translates only the changed namespaces and keys, verifies them with `i18n:check`, and opens a pull request when translations change. +Changes to `web/i18n/locales/en-US/*.json` on `main` trigger the scoped translation workflow. The workflow derives target locales from `languages.ts`, translates only the changed namespaces and keys, verifies them with `i18n:check`, and opens a pull request when translations change. Use the `Translate i18n Files with Claude Code` workflow dispatch for a manual scoped sync. Full mode requires an explicit file list. diff --git a/web/i18n-config/__tests__/client-locale.spec.ts b/web/i18n/__tests__/client-locale.spec.ts similarity index 100% rename from web/i18n-config/__tests__/client-locale.spec.ts rename to web/i18n/__tests__/client-locale.spec.ts diff --git a/web/i18n-config/__tests__/load-resource.spec.ts b/web/i18n/__tests__/load-resource.spec.ts similarity index 100% rename from web/i18n-config/__tests__/load-resource.spec.ts rename to web/i18n/__tests__/load-resource.spec.ts diff --git a/web/i18n-config/__tests__/metadata.spec.ts b/web/i18n/__tests__/metadata.spec.ts similarity index 100% rename from web/i18n-config/__tests__/metadata.spec.ts rename to web/i18n/__tests__/metadata.spec.ts diff --git a/web/i18n-config/__tests__/plural-selector.spec.ts b/web/i18n/__tests__/plural-selector.spec.ts similarity index 97% rename from web/i18n-config/__tests__/plural-selector.spec.ts rename to web/i18n/__tests__/plural-selector.spec.ts index 313e9e4db62..170e89bc2b2 100644 --- a/web/i18n-config/__tests__/plural-selector.spec.ts +++ b/web/i18n/__tests__/plural-selector.spec.ts @@ -1,7 +1,7 @@ import type { SelectorParam } from 'i18next' import { createInstance } from 'i18next' import { describe, expect, it } from 'vite-plus/test' -import skill from '../../i18n/en-US/skill.json' +import skill from '../locales/en-US/skill.json' import { getInitOptions } from '../settings' describe('i18n selector configuration', () => { diff --git a/web/i18n-config/__tests__/server.spec.ts b/web/i18n/__tests__/server.spec.ts similarity index 100% rename from web/i18n-config/__tests__/server.spec.ts rename to web/i18n/__tests__/server.spec.ts diff --git a/web/i18n-config/client.ts b/web/i18n/client.ts similarity index 100% rename from web/i18n-config/client.ts rename to web/i18n/client.ts diff --git a/web/i18n-config/index.ts b/web/i18n/index.ts similarity index 100% rename from web/i18n-config/index.ts rename to web/i18n/index.ts diff --git a/web/i18n-config/language.ts b/web/i18n/language.ts similarity index 100% rename from web/i18n-config/language.ts rename to web/i18n/language.ts diff --git a/web/i18n-config/languages.ts b/web/i18n/languages.ts similarity index 100% rename from web/i18n-config/languages.ts rename to web/i18n/languages.ts diff --git a/web/i18n-config/lib.client.ts b/web/i18n/lib.client.ts similarity index 100% rename from web/i18n-config/lib.client.ts rename to web/i18n/lib.client.ts diff --git a/web/i18n-config/lib.server.ts b/web/i18n/lib.server.ts similarity index 100% rename from web/i18n-config/lib.server.ts rename to web/i18n/lib.server.ts diff --git a/web/i18n-config/load-resource.ts b/web/i18n/load-resource.ts similarity index 100% rename from web/i18n-config/load-resource.ts rename to web/i18n/load-resource.ts diff --git a/web/i18n-config/locale-resources/ar-TN.ts b/web/i18n/locale-resources/ar-TN.ts similarity index 51% rename from web/i18n-config/locale-resources/ar-TN.ts rename to web/i18n/locale-resources/ar-TN.ts index 0233334f0c4..67115a32d26 100644 --- a/web/i18n-config/locale-resources/ar-TN.ts +++ b/web/i18n/locale-resources/ar-TN.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/ar-TN/${fileNamespace}.json`) + import(`../locales/ar-TN/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/az-AZ.ts b/web/i18n/locale-resources/az-AZ.ts similarity index 51% rename from web/i18n-config/locale-resources/az-AZ.ts rename to web/i18n/locale-resources/az-AZ.ts index 6a515f06370..3db1baeb3d2 100644 --- a/web/i18n-config/locale-resources/az-AZ.ts +++ b/web/i18n/locale-resources/az-AZ.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/az-AZ/${fileNamespace}.json`) + import(`../locales/az-AZ/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/de-DE.ts b/web/i18n/locale-resources/de-DE.ts similarity index 51% rename from web/i18n-config/locale-resources/de-DE.ts rename to web/i18n/locale-resources/de-DE.ts index d518616ea12..a16f3a628ff 100644 --- a/web/i18n-config/locale-resources/de-DE.ts +++ b/web/i18n/locale-resources/de-DE.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/de-DE/${fileNamespace}.json`) + import(`../locales/de-DE/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/en-US.ts b/web/i18n/locale-resources/en-US.ts similarity index 51% rename from web/i18n-config/locale-resources/en-US.ts rename to web/i18n/locale-resources/en-US.ts index f7d33c53c6d..4dc709e11df 100644 --- a/web/i18n-config/locale-resources/en-US.ts +++ b/web/i18n/locale-resources/en-US.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/en-US/${fileNamespace}.json`) + import(`../locales/en-US/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/es-ES.ts b/web/i18n/locale-resources/es-ES.ts similarity index 51% rename from web/i18n-config/locale-resources/es-ES.ts rename to web/i18n/locale-resources/es-ES.ts index ad7d256275a..7a84ef6fef1 100644 --- a/web/i18n-config/locale-resources/es-ES.ts +++ b/web/i18n/locale-resources/es-ES.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/es-ES/${fileNamespace}.json`) + import(`../locales/es-ES/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/fa-IR.ts b/web/i18n/locale-resources/fa-IR.ts similarity index 51% rename from web/i18n-config/locale-resources/fa-IR.ts rename to web/i18n/locale-resources/fa-IR.ts index d95c6e62ca3..f4bfa9a858b 100644 --- a/web/i18n-config/locale-resources/fa-IR.ts +++ b/web/i18n/locale-resources/fa-IR.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/fa-IR/${fileNamespace}.json`) + import(`../locales/fa-IR/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/fr-FR.ts b/web/i18n/locale-resources/fr-FR.ts similarity index 51% rename from web/i18n-config/locale-resources/fr-FR.ts rename to web/i18n/locale-resources/fr-FR.ts index e82371bb143..7da11806152 100644 --- a/web/i18n-config/locale-resources/fr-FR.ts +++ b/web/i18n/locale-resources/fr-FR.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/fr-FR/${fileNamespace}.json`) + import(`../locales/fr-FR/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/hi-IN.ts b/web/i18n/locale-resources/hi-IN.ts similarity index 51% rename from web/i18n-config/locale-resources/hi-IN.ts rename to web/i18n/locale-resources/hi-IN.ts index c81c8c62eb7..65aa799773d 100644 --- a/web/i18n-config/locale-resources/hi-IN.ts +++ b/web/i18n/locale-resources/hi-IN.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/hi-IN/${fileNamespace}.json`) + import(`../locales/hi-IN/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/id-ID.ts b/web/i18n/locale-resources/id-ID.ts similarity index 51% rename from web/i18n-config/locale-resources/id-ID.ts rename to web/i18n/locale-resources/id-ID.ts index 1b8629e15f2..3fee5bdf5d2 100644 --- a/web/i18n-config/locale-resources/id-ID.ts +++ b/web/i18n/locale-resources/id-ID.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/id-ID/${fileNamespace}.json`) + import(`../locales/id-ID/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/it-IT.ts b/web/i18n/locale-resources/it-IT.ts similarity index 51% rename from web/i18n-config/locale-resources/it-IT.ts rename to web/i18n/locale-resources/it-IT.ts index 0920e7b4d20..61edaaa4b9a 100644 --- a/web/i18n-config/locale-resources/it-IT.ts +++ b/web/i18n/locale-resources/it-IT.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/it-IT/${fileNamespace}.json`) + import(`../locales/it-IT/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/ja-JP.ts b/web/i18n/locale-resources/ja-JP.ts similarity index 51% rename from web/i18n-config/locale-resources/ja-JP.ts rename to web/i18n/locale-resources/ja-JP.ts index 50732b3c6e3..eb14b53b55b 100644 --- a/web/i18n-config/locale-resources/ja-JP.ts +++ b/web/i18n/locale-resources/ja-JP.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/ja-JP/${fileNamespace}.json`) + import(`../locales/ja-JP/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/ko-KR.ts b/web/i18n/locale-resources/ko-KR.ts similarity index 51% rename from web/i18n-config/locale-resources/ko-KR.ts rename to web/i18n/locale-resources/ko-KR.ts index 60234fbd033..7a56e3326ef 100644 --- a/web/i18n-config/locale-resources/ko-KR.ts +++ b/web/i18n/locale-resources/ko-KR.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/ko-KR/${fileNamespace}.json`) + import(`../locales/ko-KR/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/lo-LA.ts b/web/i18n/locale-resources/lo-LA.ts similarity index 51% rename from web/i18n-config/locale-resources/lo-LA.ts rename to web/i18n/locale-resources/lo-LA.ts index b835990c5ee..c58cd9a1fc3 100644 --- a/web/i18n-config/locale-resources/lo-LA.ts +++ b/web/i18n/locale-resources/lo-LA.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/lo-LA/${fileNamespace}.json`) + import(`../locales/lo-LA/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/nl-NL.ts b/web/i18n/locale-resources/nl-NL.ts similarity index 51% rename from web/i18n-config/locale-resources/nl-NL.ts rename to web/i18n/locale-resources/nl-NL.ts index 56dda25160e..b10f825e1fe 100644 --- a/web/i18n-config/locale-resources/nl-NL.ts +++ b/web/i18n/locale-resources/nl-NL.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/nl-NL/${fileNamespace}.json`) + import(`../locales/nl-NL/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/pl-PL.ts b/web/i18n/locale-resources/pl-PL.ts similarity index 51% rename from web/i18n-config/locale-resources/pl-PL.ts rename to web/i18n/locale-resources/pl-PL.ts index e0782421886..2f0fd3908d5 100644 --- a/web/i18n-config/locale-resources/pl-PL.ts +++ b/web/i18n/locale-resources/pl-PL.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/pl-PL/${fileNamespace}.json`) + import(`../locales/pl-PL/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/pt-BR.ts b/web/i18n/locale-resources/pt-BR.ts similarity index 51% rename from web/i18n-config/locale-resources/pt-BR.ts rename to web/i18n/locale-resources/pt-BR.ts index d77363b7c67..7aa6f3723f4 100644 --- a/web/i18n-config/locale-resources/pt-BR.ts +++ b/web/i18n/locale-resources/pt-BR.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/pt-BR/${fileNamespace}.json`) + import(`../locales/pt-BR/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/ro-RO.ts b/web/i18n/locale-resources/ro-RO.ts similarity index 51% rename from web/i18n-config/locale-resources/ro-RO.ts rename to web/i18n/locale-resources/ro-RO.ts index db065496d0b..7188eb82896 100644 --- a/web/i18n-config/locale-resources/ro-RO.ts +++ b/web/i18n/locale-resources/ro-RO.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/ro-RO/${fileNamespace}.json`) + import(`../locales/ro-RO/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/ru-RU.ts b/web/i18n/locale-resources/ru-RU.ts similarity index 51% rename from web/i18n-config/locale-resources/ru-RU.ts rename to web/i18n/locale-resources/ru-RU.ts index f5034f141ba..6652e96e8e1 100644 --- a/web/i18n-config/locale-resources/ru-RU.ts +++ b/web/i18n/locale-resources/ru-RU.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/ru-RU/${fileNamespace}.json`) + import(`../locales/ru-RU/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/sl-SI.ts b/web/i18n/locale-resources/sl-SI.ts similarity index 51% rename from web/i18n-config/locale-resources/sl-SI.ts rename to web/i18n/locale-resources/sl-SI.ts index aa4e530b180..a8f138321d9 100644 --- a/web/i18n-config/locale-resources/sl-SI.ts +++ b/web/i18n/locale-resources/sl-SI.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/sl-SI/${fileNamespace}.json`) + import(`../locales/sl-SI/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/th-TH.ts b/web/i18n/locale-resources/th-TH.ts similarity index 51% rename from web/i18n-config/locale-resources/th-TH.ts rename to web/i18n/locale-resources/th-TH.ts index a7f3e322447..91b694ca0f1 100644 --- a/web/i18n-config/locale-resources/th-TH.ts +++ b/web/i18n/locale-resources/th-TH.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/th-TH/${fileNamespace}.json`) + import(`../locales/th-TH/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/tr-TR.ts b/web/i18n/locale-resources/tr-TR.ts similarity index 51% rename from web/i18n-config/locale-resources/tr-TR.ts rename to web/i18n/locale-resources/tr-TR.ts index e4efa9b5a83..18b0314b507 100644 --- a/web/i18n-config/locale-resources/tr-TR.ts +++ b/web/i18n/locale-resources/tr-TR.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/tr-TR/${fileNamespace}.json`) + import(`../locales/tr-TR/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/uk-UA.ts b/web/i18n/locale-resources/uk-UA.ts similarity index 51% rename from web/i18n-config/locale-resources/uk-UA.ts rename to web/i18n/locale-resources/uk-UA.ts index 245696237c4..64b8eba9a9b 100644 --- a/web/i18n-config/locale-resources/uk-UA.ts +++ b/web/i18n/locale-resources/uk-UA.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/uk-UA/${fileNamespace}.json`) + import(`../locales/uk-UA/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/vi-VN.ts b/web/i18n/locale-resources/vi-VN.ts similarity index 51% rename from web/i18n-config/locale-resources/vi-VN.ts rename to web/i18n/locale-resources/vi-VN.ts index dfde38233f8..09fd9637747 100644 --- a/web/i18n-config/locale-resources/vi-VN.ts +++ b/web/i18n/locale-resources/vi-VN.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/vi-VN/${fileNamespace}.json`) + import(`../locales/vi-VN/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/zh-Hans.ts b/web/i18n/locale-resources/zh-Hans.ts similarity index 50% rename from web/i18n-config/locale-resources/zh-Hans.ts rename to web/i18n/locale-resources/zh-Hans.ts index 3fecc27d70a..2404b82dabe 100644 --- a/web/i18n-config/locale-resources/zh-Hans.ts +++ b/web/i18n/locale-resources/zh-Hans.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/zh-Hans/${fileNamespace}.json`) + import(`../locales/zh-Hans/${fileNamespace}.json`) diff --git a/web/i18n-config/locale-resources/zh-Hant.ts b/web/i18n/locale-resources/zh-Hant.ts similarity index 50% rename from web/i18n-config/locale-resources/zh-Hant.ts rename to web/i18n/locale-resources/zh-Hant.ts index 30cd2cb64e1..8608870f8f1 100644 --- a/web/i18n-config/locale-resources/zh-Hant.ts +++ b/web/i18n/locale-resources/zh-Hant.ts @@ -1,2 +1,2 @@ export const loadResource = (fileNamespace: string) => - import(`../../i18n/zh-Hant/${fileNamespace}.json`) + import(`../locales/zh-Hant/${fileNamespace}.json`) diff --git a/web/i18n-config/locale.ts b/web/i18n/locale.ts similarity index 100% rename from web/i18n-config/locale.ts rename to web/i18n/locale.ts diff --git a/web/i18n/ar-TN/agent-v-2.json b/web/i18n/locales/ar-TN/agent-v-2.json similarity index 100% rename from web/i18n/ar-TN/agent-v-2.json rename to web/i18n/locales/ar-TN/agent-v-2.json diff --git a/web/i18n/ar-TN/app-annotation.json b/web/i18n/locales/ar-TN/app-annotation.json similarity index 100% rename from web/i18n/ar-TN/app-annotation.json rename to web/i18n/locales/ar-TN/app-annotation.json diff --git a/web/i18n/ar-TN/app-api.json b/web/i18n/locales/ar-TN/app-api.json similarity index 100% rename from web/i18n/ar-TN/app-api.json rename to web/i18n/locales/ar-TN/app-api.json diff --git a/web/i18n/ar-TN/app-debug.json b/web/i18n/locales/ar-TN/app-debug.json similarity index 100% rename from web/i18n/ar-TN/app-debug.json rename to web/i18n/locales/ar-TN/app-debug.json diff --git a/web/i18n/ar-TN/app-log.json b/web/i18n/locales/ar-TN/app-log.json similarity index 100% rename from web/i18n/ar-TN/app-log.json rename to web/i18n/locales/ar-TN/app-log.json diff --git a/web/i18n/ar-TN/app-overview.json b/web/i18n/locales/ar-TN/app-overview.json similarity index 100% rename from web/i18n/ar-TN/app-overview.json rename to web/i18n/locales/ar-TN/app-overview.json diff --git a/web/i18n/ar-TN/app.json b/web/i18n/locales/ar-TN/app.json similarity index 100% rename from web/i18n/ar-TN/app.json rename to web/i18n/locales/ar-TN/app.json diff --git a/web/i18n/ar-TN/billing.json b/web/i18n/locales/ar-TN/billing.json similarity index 100% rename from web/i18n/ar-TN/billing.json rename to web/i18n/locales/ar-TN/billing.json diff --git a/web/i18n/ar-TN/common.json b/web/i18n/locales/ar-TN/common.json similarity index 100% rename from web/i18n/ar-TN/common.json rename to web/i18n/locales/ar-TN/common.json diff --git a/web/i18n/ar-TN/custom.json b/web/i18n/locales/ar-TN/custom.json similarity index 100% rename from web/i18n/ar-TN/custom.json rename to web/i18n/locales/ar-TN/custom.json diff --git a/web/i18n/ar-TN/dataset-creation.json b/web/i18n/locales/ar-TN/dataset-creation.json similarity index 100% rename from web/i18n/ar-TN/dataset-creation.json rename to web/i18n/locales/ar-TN/dataset-creation.json diff --git a/web/i18n/ar-TN/dataset-documents.json b/web/i18n/locales/ar-TN/dataset-documents.json similarity index 100% rename from web/i18n/ar-TN/dataset-documents.json rename to web/i18n/locales/ar-TN/dataset-documents.json diff --git a/web/i18n/ar-TN/dataset-hit-testing.json b/web/i18n/locales/ar-TN/dataset-hit-testing.json similarity index 100% rename from web/i18n/ar-TN/dataset-hit-testing.json rename to web/i18n/locales/ar-TN/dataset-hit-testing.json diff --git a/web/i18n/ar-TN/dataset-pipeline.json b/web/i18n/locales/ar-TN/dataset-pipeline.json similarity index 100% rename from web/i18n/ar-TN/dataset-pipeline.json rename to web/i18n/locales/ar-TN/dataset-pipeline.json diff --git a/web/i18n/ar-TN/dataset-settings.json b/web/i18n/locales/ar-TN/dataset-settings.json similarity index 100% rename from web/i18n/ar-TN/dataset-settings.json rename to web/i18n/locales/ar-TN/dataset-settings.json diff --git a/web/i18n/ar-TN/dataset.json b/web/i18n/locales/ar-TN/dataset.json similarity index 100% rename from web/i18n/ar-TN/dataset.json rename to web/i18n/locales/ar-TN/dataset.json diff --git a/web/i18n/ar-TN/deployments.json b/web/i18n/locales/ar-TN/deployments.json similarity index 100% rename from web/i18n/ar-TN/deployments.json rename to web/i18n/locales/ar-TN/deployments.json diff --git a/web/i18n/ar-TN/device-flow.json b/web/i18n/locales/ar-TN/device-flow.json similarity index 100% rename from web/i18n/ar-TN/device-flow.json rename to web/i18n/locales/ar-TN/device-flow.json diff --git a/web/i18n/ar-TN/education.json b/web/i18n/locales/ar-TN/education.json similarity index 100% rename from web/i18n/ar-TN/education.json rename to web/i18n/locales/ar-TN/education.json diff --git a/web/i18n/ar-TN/explore.json b/web/i18n/locales/ar-TN/explore.json similarity index 100% rename from web/i18n/ar-TN/explore.json rename to web/i18n/locales/ar-TN/explore.json diff --git a/web/i18n/ar-TN/layout.json b/web/i18n/locales/ar-TN/layout.json similarity index 100% rename from web/i18n/ar-TN/layout.json rename to web/i18n/locales/ar-TN/layout.json diff --git a/web/i18n/ar-TN/login.json b/web/i18n/locales/ar-TN/login.json similarity index 100% rename from web/i18n/ar-TN/login.json rename to web/i18n/locales/ar-TN/login.json diff --git a/web/i18n/ar-TN/oauth.json b/web/i18n/locales/ar-TN/oauth.json similarity index 100% rename from web/i18n/ar-TN/oauth.json rename to web/i18n/locales/ar-TN/oauth.json diff --git a/web/i18n/ar-TN/permission-keys.json b/web/i18n/locales/ar-TN/permission-keys.json similarity index 100% rename from web/i18n/ar-TN/permission-keys.json rename to web/i18n/locales/ar-TN/permission-keys.json diff --git a/web/i18n/ar-TN/permission.json b/web/i18n/locales/ar-TN/permission.json similarity index 100% rename from web/i18n/ar-TN/permission.json rename to web/i18n/locales/ar-TN/permission.json diff --git a/web/i18n/ar-TN/pipeline.json b/web/i18n/locales/ar-TN/pipeline.json similarity index 100% rename from web/i18n/ar-TN/pipeline.json rename to web/i18n/locales/ar-TN/pipeline.json diff --git a/web/i18n/ar-TN/plugin-tags.json b/web/i18n/locales/ar-TN/plugin-tags.json similarity index 100% rename from web/i18n/ar-TN/plugin-tags.json rename to web/i18n/locales/ar-TN/plugin-tags.json diff --git a/web/i18n/ar-TN/plugin-trigger.json b/web/i18n/locales/ar-TN/plugin-trigger.json similarity index 100% rename from web/i18n/ar-TN/plugin-trigger.json rename to web/i18n/locales/ar-TN/plugin-trigger.json diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/locales/ar-TN/plugin.json similarity index 100% rename from web/i18n/ar-TN/plugin.json rename to web/i18n/locales/ar-TN/plugin.json diff --git a/web/i18n/ar-TN/register.json b/web/i18n/locales/ar-TN/register.json similarity index 100% rename from web/i18n/ar-TN/register.json rename to web/i18n/locales/ar-TN/register.json diff --git a/web/i18n/ar-TN/run-log.json b/web/i18n/locales/ar-TN/run-log.json similarity index 100% rename from web/i18n/ar-TN/run-log.json rename to web/i18n/locales/ar-TN/run-log.json diff --git a/web/i18n/ar-TN/share.json b/web/i18n/locales/ar-TN/share.json similarity index 100% rename from web/i18n/ar-TN/share.json rename to web/i18n/locales/ar-TN/share.json diff --git a/web/i18n/ar-TN/skill.json b/web/i18n/locales/ar-TN/skill.json similarity index 100% rename from web/i18n/ar-TN/skill.json rename to web/i18n/locales/ar-TN/skill.json diff --git a/web/i18n/ar-TN/snippet.json b/web/i18n/locales/ar-TN/snippet.json similarity index 100% rename from web/i18n/ar-TN/snippet.json rename to web/i18n/locales/ar-TN/snippet.json diff --git a/web/i18n/ar-TN/time.json b/web/i18n/locales/ar-TN/time.json similarity index 100% rename from web/i18n/ar-TN/time.json rename to web/i18n/locales/ar-TN/time.json diff --git a/web/i18n/ar-TN/tools.json b/web/i18n/locales/ar-TN/tools.json similarity index 100% rename from web/i18n/ar-TN/tools.json rename to web/i18n/locales/ar-TN/tools.json diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/locales/ar-TN/workflow.json similarity index 100% rename from web/i18n/ar-TN/workflow.json rename to web/i18n/locales/ar-TN/workflow.json diff --git a/web/i18n/az-AZ/agent-v-2.json b/web/i18n/locales/az-AZ/agent-v-2.json similarity index 100% rename from web/i18n/az-AZ/agent-v-2.json rename to web/i18n/locales/az-AZ/agent-v-2.json diff --git a/web/i18n/az-AZ/app-annotation.json b/web/i18n/locales/az-AZ/app-annotation.json similarity index 100% rename from web/i18n/az-AZ/app-annotation.json rename to web/i18n/locales/az-AZ/app-annotation.json diff --git a/web/i18n/az-AZ/app-api.json b/web/i18n/locales/az-AZ/app-api.json similarity index 100% rename from web/i18n/az-AZ/app-api.json rename to web/i18n/locales/az-AZ/app-api.json diff --git a/web/i18n/az-AZ/app-debug.json b/web/i18n/locales/az-AZ/app-debug.json similarity index 100% rename from web/i18n/az-AZ/app-debug.json rename to web/i18n/locales/az-AZ/app-debug.json diff --git a/web/i18n/az-AZ/app-log.json b/web/i18n/locales/az-AZ/app-log.json similarity index 100% rename from web/i18n/az-AZ/app-log.json rename to web/i18n/locales/az-AZ/app-log.json diff --git a/web/i18n/az-AZ/app-overview.json b/web/i18n/locales/az-AZ/app-overview.json similarity index 100% rename from web/i18n/az-AZ/app-overview.json rename to web/i18n/locales/az-AZ/app-overview.json diff --git a/web/i18n/az-AZ/app.json b/web/i18n/locales/az-AZ/app.json similarity index 100% rename from web/i18n/az-AZ/app.json rename to web/i18n/locales/az-AZ/app.json diff --git a/web/i18n/az-AZ/billing.json b/web/i18n/locales/az-AZ/billing.json similarity index 100% rename from web/i18n/az-AZ/billing.json rename to web/i18n/locales/az-AZ/billing.json diff --git a/web/i18n/az-AZ/common.json b/web/i18n/locales/az-AZ/common.json similarity index 100% rename from web/i18n/az-AZ/common.json rename to web/i18n/locales/az-AZ/common.json diff --git a/web/i18n/az-AZ/custom.json b/web/i18n/locales/az-AZ/custom.json similarity index 100% rename from web/i18n/az-AZ/custom.json rename to web/i18n/locales/az-AZ/custom.json diff --git a/web/i18n/az-AZ/dataset-creation.json b/web/i18n/locales/az-AZ/dataset-creation.json similarity index 100% rename from web/i18n/az-AZ/dataset-creation.json rename to web/i18n/locales/az-AZ/dataset-creation.json diff --git a/web/i18n/az-AZ/dataset-documents.json b/web/i18n/locales/az-AZ/dataset-documents.json similarity index 100% rename from web/i18n/az-AZ/dataset-documents.json rename to web/i18n/locales/az-AZ/dataset-documents.json diff --git a/web/i18n/az-AZ/dataset-hit-testing.json b/web/i18n/locales/az-AZ/dataset-hit-testing.json similarity index 100% rename from web/i18n/az-AZ/dataset-hit-testing.json rename to web/i18n/locales/az-AZ/dataset-hit-testing.json diff --git a/web/i18n/az-AZ/dataset-pipeline.json b/web/i18n/locales/az-AZ/dataset-pipeline.json similarity index 100% rename from web/i18n/az-AZ/dataset-pipeline.json rename to web/i18n/locales/az-AZ/dataset-pipeline.json diff --git a/web/i18n/az-AZ/dataset-settings.json b/web/i18n/locales/az-AZ/dataset-settings.json similarity index 100% rename from web/i18n/az-AZ/dataset-settings.json rename to web/i18n/locales/az-AZ/dataset-settings.json diff --git a/web/i18n/az-AZ/dataset.json b/web/i18n/locales/az-AZ/dataset.json similarity index 100% rename from web/i18n/az-AZ/dataset.json rename to web/i18n/locales/az-AZ/dataset.json diff --git a/web/i18n/az-AZ/deployments.json b/web/i18n/locales/az-AZ/deployments.json similarity index 100% rename from web/i18n/az-AZ/deployments.json rename to web/i18n/locales/az-AZ/deployments.json diff --git a/web/i18n/az-AZ/device-flow.json b/web/i18n/locales/az-AZ/device-flow.json similarity index 100% rename from web/i18n/az-AZ/device-flow.json rename to web/i18n/locales/az-AZ/device-flow.json diff --git a/web/i18n/az-AZ/education.json b/web/i18n/locales/az-AZ/education.json similarity index 100% rename from web/i18n/az-AZ/education.json rename to web/i18n/locales/az-AZ/education.json diff --git a/web/i18n/az-AZ/explore.json b/web/i18n/locales/az-AZ/explore.json similarity index 100% rename from web/i18n/az-AZ/explore.json rename to web/i18n/locales/az-AZ/explore.json diff --git a/web/i18n/az-AZ/layout.json b/web/i18n/locales/az-AZ/layout.json similarity index 100% rename from web/i18n/az-AZ/layout.json rename to web/i18n/locales/az-AZ/layout.json diff --git a/web/i18n/az-AZ/login.json b/web/i18n/locales/az-AZ/login.json similarity index 100% rename from web/i18n/az-AZ/login.json rename to web/i18n/locales/az-AZ/login.json diff --git a/web/i18n/az-AZ/oauth.json b/web/i18n/locales/az-AZ/oauth.json similarity index 100% rename from web/i18n/az-AZ/oauth.json rename to web/i18n/locales/az-AZ/oauth.json diff --git a/web/i18n/az-AZ/permission-keys.json b/web/i18n/locales/az-AZ/permission-keys.json similarity index 100% rename from web/i18n/az-AZ/permission-keys.json rename to web/i18n/locales/az-AZ/permission-keys.json diff --git a/web/i18n/az-AZ/permission.json b/web/i18n/locales/az-AZ/permission.json similarity index 100% rename from web/i18n/az-AZ/permission.json rename to web/i18n/locales/az-AZ/permission.json diff --git a/web/i18n/az-AZ/pipeline.json b/web/i18n/locales/az-AZ/pipeline.json similarity index 100% rename from web/i18n/az-AZ/pipeline.json rename to web/i18n/locales/az-AZ/pipeline.json diff --git a/web/i18n/az-AZ/plugin-tags.json b/web/i18n/locales/az-AZ/plugin-tags.json similarity index 100% rename from web/i18n/az-AZ/plugin-tags.json rename to web/i18n/locales/az-AZ/plugin-tags.json diff --git a/web/i18n/az-AZ/plugin-trigger.json b/web/i18n/locales/az-AZ/plugin-trigger.json similarity index 100% rename from web/i18n/az-AZ/plugin-trigger.json rename to web/i18n/locales/az-AZ/plugin-trigger.json diff --git a/web/i18n/az-AZ/plugin.json b/web/i18n/locales/az-AZ/plugin.json similarity index 100% rename from web/i18n/az-AZ/plugin.json rename to web/i18n/locales/az-AZ/plugin.json diff --git a/web/i18n/az-AZ/register.json b/web/i18n/locales/az-AZ/register.json similarity index 100% rename from web/i18n/az-AZ/register.json rename to web/i18n/locales/az-AZ/register.json diff --git a/web/i18n/az-AZ/run-log.json b/web/i18n/locales/az-AZ/run-log.json similarity index 100% rename from web/i18n/az-AZ/run-log.json rename to web/i18n/locales/az-AZ/run-log.json diff --git a/web/i18n/az-AZ/share.json b/web/i18n/locales/az-AZ/share.json similarity index 100% rename from web/i18n/az-AZ/share.json rename to web/i18n/locales/az-AZ/share.json diff --git a/web/i18n/az-AZ/skill.json b/web/i18n/locales/az-AZ/skill.json similarity index 100% rename from web/i18n/az-AZ/skill.json rename to web/i18n/locales/az-AZ/skill.json diff --git a/web/i18n/az-AZ/snippet.json b/web/i18n/locales/az-AZ/snippet.json similarity index 100% rename from web/i18n/az-AZ/snippet.json rename to web/i18n/locales/az-AZ/snippet.json diff --git a/web/i18n/az-AZ/time.json b/web/i18n/locales/az-AZ/time.json similarity index 100% rename from web/i18n/az-AZ/time.json rename to web/i18n/locales/az-AZ/time.json diff --git a/web/i18n/az-AZ/tools.json b/web/i18n/locales/az-AZ/tools.json similarity index 100% rename from web/i18n/az-AZ/tools.json rename to web/i18n/locales/az-AZ/tools.json diff --git a/web/i18n/az-AZ/workflow.json b/web/i18n/locales/az-AZ/workflow.json similarity index 100% rename from web/i18n/az-AZ/workflow.json rename to web/i18n/locales/az-AZ/workflow.json diff --git a/web/i18n/de-DE/agent-v-2.json b/web/i18n/locales/de-DE/agent-v-2.json similarity index 100% rename from web/i18n/de-DE/agent-v-2.json rename to web/i18n/locales/de-DE/agent-v-2.json diff --git a/web/i18n/de-DE/app-annotation.json b/web/i18n/locales/de-DE/app-annotation.json similarity index 100% rename from web/i18n/de-DE/app-annotation.json rename to web/i18n/locales/de-DE/app-annotation.json diff --git a/web/i18n/de-DE/app-api.json b/web/i18n/locales/de-DE/app-api.json similarity index 100% rename from web/i18n/de-DE/app-api.json rename to web/i18n/locales/de-DE/app-api.json diff --git a/web/i18n/de-DE/app-debug.json b/web/i18n/locales/de-DE/app-debug.json similarity index 100% rename from web/i18n/de-DE/app-debug.json rename to web/i18n/locales/de-DE/app-debug.json diff --git a/web/i18n/de-DE/app-log.json b/web/i18n/locales/de-DE/app-log.json similarity index 100% rename from web/i18n/de-DE/app-log.json rename to web/i18n/locales/de-DE/app-log.json diff --git a/web/i18n/de-DE/app-overview.json b/web/i18n/locales/de-DE/app-overview.json similarity index 100% rename from web/i18n/de-DE/app-overview.json rename to web/i18n/locales/de-DE/app-overview.json diff --git a/web/i18n/de-DE/app.json b/web/i18n/locales/de-DE/app.json similarity index 100% rename from web/i18n/de-DE/app.json rename to web/i18n/locales/de-DE/app.json diff --git a/web/i18n/de-DE/billing.json b/web/i18n/locales/de-DE/billing.json similarity index 100% rename from web/i18n/de-DE/billing.json rename to web/i18n/locales/de-DE/billing.json diff --git a/web/i18n/de-DE/common.json b/web/i18n/locales/de-DE/common.json similarity index 100% rename from web/i18n/de-DE/common.json rename to web/i18n/locales/de-DE/common.json diff --git a/web/i18n/de-DE/custom.json b/web/i18n/locales/de-DE/custom.json similarity index 100% rename from web/i18n/de-DE/custom.json rename to web/i18n/locales/de-DE/custom.json diff --git a/web/i18n/de-DE/dataset-creation.json b/web/i18n/locales/de-DE/dataset-creation.json similarity index 100% rename from web/i18n/de-DE/dataset-creation.json rename to web/i18n/locales/de-DE/dataset-creation.json diff --git a/web/i18n/de-DE/dataset-documents.json b/web/i18n/locales/de-DE/dataset-documents.json similarity index 100% rename from web/i18n/de-DE/dataset-documents.json rename to web/i18n/locales/de-DE/dataset-documents.json diff --git a/web/i18n/de-DE/dataset-hit-testing.json b/web/i18n/locales/de-DE/dataset-hit-testing.json similarity index 100% rename from web/i18n/de-DE/dataset-hit-testing.json rename to web/i18n/locales/de-DE/dataset-hit-testing.json diff --git a/web/i18n/de-DE/dataset-pipeline.json b/web/i18n/locales/de-DE/dataset-pipeline.json similarity index 100% rename from web/i18n/de-DE/dataset-pipeline.json rename to web/i18n/locales/de-DE/dataset-pipeline.json diff --git a/web/i18n/de-DE/dataset-settings.json b/web/i18n/locales/de-DE/dataset-settings.json similarity index 100% rename from web/i18n/de-DE/dataset-settings.json rename to web/i18n/locales/de-DE/dataset-settings.json diff --git a/web/i18n/de-DE/dataset.json b/web/i18n/locales/de-DE/dataset.json similarity index 100% rename from web/i18n/de-DE/dataset.json rename to web/i18n/locales/de-DE/dataset.json diff --git a/web/i18n/de-DE/deployments.json b/web/i18n/locales/de-DE/deployments.json similarity index 100% rename from web/i18n/de-DE/deployments.json rename to web/i18n/locales/de-DE/deployments.json diff --git a/web/i18n/de-DE/device-flow.json b/web/i18n/locales/de-DE/device-flow.json similarity index 100% rename from web/i18n/de-DE/device-flow.json rename to web/i18n/locales/de-DE/device-flow.json diff --git a/web/i18n/de-DE/education.json b/web/i18n/locales/de-DE/education.json similarity index 100% rename from web/i18n/de-DE/education.json rename to web/i18n/locales/de-DE/education.json diff --git a/web/i18n/de-DE/explore.json b/web/i18n/locales/de-DE/explore.json similarity index 100% rename from web/i18n/de-DE/explore.json rename to web/i18n/locales/de-DE/explore.json diff --git a/web/i18n/de-DE/layout.json b/web/i18n/locales/de-DE/layout.json similarity index 100% rename from web/i18n/de-DE/layout.json rename to web/i18n/locales/de-DE/layout.json diff --git a/web/i18n/de-DE/login.json b/web/i18n/locales/de-DE/login.json similarity index 100% rename from web/i18n/de-DE/login.json rename to web/i18n/locales/de-DE/login.json diff --git a/web/i18n/de-DE/oauth.json b/web/i18n/locales/de-DE/oauth.json similarity index 100% rename from web/i18n/de-DE/oauth.json rename to web/i18n/locales/de-DE/oauth.json diff --git a/web/i18n/de-DE/permission-keys.json b/web/i18n/locales/de-DE/permission-keys.json similarity index 100% rename from web/i18n/de-DE/permission-keys.json rename to web/i18n/locales/de-DE/permission-keys.json diff --git a/web/i18n/de-DE/permission.json b/web/i18n/locales/de-DE/permission.json similarity index 100% rename from web/i18n/de-DE/permission.json rename to web/i18n/locales/de-DE/permission.json diff --git a/web/i18n/de-DE/pipeline.json b/web/i18n/locales/de-DE/pipeline.json similarity index 100% rename from web/i18n/de-DE/pipeline.json rename to web/i18n/locales/de-DE/pipeline.json diff --git a/web/i18n/de-DE/plugin-tags.json b/web/i18n/locales/de-DE/plugin-tags.json similarity index 100% rename from web/i18n/de-DE/plugin-tags.json rename to web/i18n/locales/de-DE/plugin-tags.json diff --git a/web/i18n/de-DE/plugin-trigger.json b/web/i18n/locales/de-DE/plugin-trigger.json similarity index 100% rename from web/i18n/de-DE/plugin-trigger.json rename to web/i18n/locales/de-DE/plugin-trigger.json diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/locales/de-DE/plugin.json similarity index 100% rename from web/i18n/de-DE/plugin.json rename to web/i18n/locales/de-DE/plugin.json diff --git a/web/i18n/de-DE/register.json b/web/i18n/locales/de-DE/register.json similarity index 100% rename from web/i18n/de-DE/register.json rename to web/i18n/locales/de-DE/register.json diff --git a/web/i18n/de-DE/run-log.json b/web/i18n/locales/de-DE/run-log.json similarity index 100% rename from web/i18n/de-DE/run-log.json rename to web/i18n/locales/de-DE/run-log.json diff --git a/web/i18n/de-DE/share.json b/web/i18n/locales/de-DE/share.json similarity index 100% rename from web/i18n/de-DE/share.json rename to web/i18n/locales/de-DE/share.json diff --git a/web/i18n/de-DE/skill.json b/web/i18n/locales/de-DE/skill.json similarity index 100% rename from web/i18n/de-DE/skill.json rename to web/i18n/locales/de-DE/skill.json diff --git a/web/i18n/de-DE/snippet.json b/web/i18n/locales/de-DE/snippet.json similarity index 100% rename from web/i18n/de-DE/snippet.json rename to web/i18n/locales/de-DE/snippet.json diff --git a/web/i18n/de-DE/time.json b/web/i18n/locales/de-DE/time.json similarity index 100% rename from web/i18n/de-DE/time.json rename to web/i18n/locales/de-DE/time.json diff --git a/web/i18n/de-DE/tools.json b/web/i18n/locales/de-DE/tools.json similarity index 100% rename from web/i18n/de-DE/tools.json rename to web/i18n/locales/de-DE/tools.json diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/locales/de-DE/workflow.json similarity index 100% rename from web/i18n/de-DE/workflow.json rename to web/i18n/locales/de-DE/workflow.json diff --git a/web/i18n/en-US/agent-v-2.json b/web/i18n/locales/en-US/agent-v-2.json similarity index 100% rename from web/i18n/en-US/agent-v-2.json rename to web/i18n/locales/en-US/agent-v-2.json diff --git a/web/i18n/en-US/app-annotation.json b/web/i18n/locales/en-US/app-annotation.json similarity index 100% rename from web/i18n/en-US/app-annotation.json rename to web/i18n/locales/en-US/app-annotation.json diff --git a/web/i18n/en-US/app-api.json b/web/i18n/locales/en-US/app-api.json similarity index 100% rename from web/i18n/en-US/app-api.json rename to web/i18n/locales/en-US/app-api.json diff --git a/web/i18n/en-US/app-debug.json b/web/i18n/locales/en-US/app-debug.json similarity index 100% rename from web/i18n/en-US/app-debug.json rename to web/i18n/locales/en-US/app-debug.json diff --git a/web/i18n/en-US/app-log.json b/web/i18n/locales/en-US/app-log.json similarity index 100% rename from web/i18n/en-US/app-log.json rename to web/i18n/locales/en-US/app-log.json diff --git a/web/i18n/en-US/app-overview.json b/web/i18n/locales/en-US/app-overview.json similarity index 100% rename from web/i18n/en-US/app-overview.json rename to web/i18n/locales/en-US/app-overview.json diff --git a/web/i18n/en-US/app.json b/web/i18n/locales/en-US/app.json similarity index 100% rename from web/i18n/en-US/app.json rename to web/i18n/locales/en-US/app.json diff --git a/web/i18n/en-US/billing.json b/web/i18n/locales/en-US/billing.json similarity index 100% rename from web/i18n/en-US/billing.json rename to web/i18n/locales/en-US/billing.json diff --git a/web/i18n/en-US/common.json b/web/i18n/locales/en-US/common.json similarity index 100% rename from web/i18n/en-US/common.json rename to web/i18n/locales/en-US/common.json diff --git a/web/i18n/en-US/custom.json b/web/i18n/locales/en-US/custom.json similarity index 100% rename from web/i18n/en-US/custom.json rename to web/i18n/locales/en-US/custom.json diff --git a/web/i18n/en-US/dataset-creation.json b/web/i18n/locales/en-US/dataset-creation.json similarity index 100% rename from web/i18n/en-US/dataset-creation.json rename to web/i18n/locales/en-US/dataset-creation.json diff --git a/web/i18n/en-US/dataset-documents.json b/web/i18n/locales/en-US/dataset-documents.json similarity index 100% rename from web/i18n/en-US/dataset-documents.json rename to web/i18n/locales/en-US/dataset-documents.json diff --git a/web/i18n/en-US/dataset-hit-testing.json b/web/i18n/locales/en-US/dataset-hit-testing.json similarity index 100% rename from web/i18n/en-US/dataset-hit-testing.json rename to web/i18n/locales/en-US/dataset-hit-testing.json diff --git a/web/i18n/en-US/dataset-pipeline.json b/web/i18n/locales/en-US/dataset-pipeline.json similarity index 100% rename from web/i18n/en-US/dataset-pipeline.json rename to web/i18n/locales/en-US/dataset-pipeline.json diff --git a/web/i18n/en-US/dataset-settings.json b/web/i18n/locales/en-US/dataset-settings.json similarity index 100% rename from web/i18n/en-US/dataset-settings.json rename to web/i18n/locales/en-US/dataset-settings.json diff --git a/web/i18n/en-US/dataset.json b/web/i18n/locales/en-US/dataset.json similarity index 100% rename from web/i18n/en-US/dataset.json rename to web/i18n/locales/en-US/dataset.json diff --git a/web/i18n/en-US/deployments.json b/web/i18n/locales/en-US/deployments.json similarity index 100% rename from web/i18n/en-US/deployments.json rename to web/i18n/locales/en-US/deployments.json diff --git a/web/i18n/en-US/device-flow.json b/web/i18n/locales/en-US/device-flow.json similarity index 100% rename from web/i18n/en-US/device-flow.json rename to web/i18n/locales/en-US/device-flow.json diff --git a/web/i18n/en-US/education.json b/web/i18n/locales/en-US/education.json similarity index 100% rename from web/i18n/en-US/education.json rename to web/i18n/locales/en-US/education.json diff --git a/web/i18n/en-US/explore.json b/web/i18n/locales/en-US/explore.json similarity index 100% rename from web/i18n/en-US/explore.json rename to web/i18n/locales/en-US/explore.json diff --git a/web/i18n/en-US/layout.json b/web/i18n/locales/en-US/layout.json similarity index 100% rename from web/i18n/en-US/layout.json rename to web/i18n/locales/en-US/layout.json diff --git a/web/i18n/en-US/login.json b/web/i18n/locales/en-US/login.json similarity index 100% rename from web/i18n/en-US/login.json rename to web/i18n/locales/en-US/login.json diff --git a/web/i18n/en-US/oauth.json b/web/i18n/locales/en-US/oauth.json similarity index 100% rename from web/i18n/en-US/oauth.json rename to web/i18n/locales/en-US/oauth.json diff --git a/web/i18n/en-US/permission-keys.json b/web/i18n/locales/en-US/permission-keys.json similarity index 100% rename from web/i18n/en-US/permission-keys.json rename to web/i18n/locales/en-US/permission-keys.json diff --git a/web/i18n/en-US/permission.json b/web/i18n/locales/en-US/permission.json similarity index 100% rename from web/i18n/en-US/permission.json rename to web/i18n/locales/en-US/permission.json diff --git a/web/i18n/en-US/pipeline.json b/web/i18n/locales/en-US/pipeline.json similarity index 100% rename from web/i18n/en-US/pipeline.json rename to web/i18n/locales/en-US/pipeline.json diff --git a/web/i18n/en-US/plugin-tags.json b/web/i18n/locales/en-US/plugin-tags.json similarity index 100% rename from web/i18n/en-US/plugin-tags.json rename to web/i18n/locales/en-US/plugin-tags.json diff --git a/web/i18n/en-US/plugin-trigger.json b/web/i18n/locales/en-US/plugin-trigger.json similarity index 100% rename from web/i18n/en-US/plugin-trigger.json rename to web/i18n/locales/en-US/plugin-trigger.json diff --git a/web/i18n/en-US/plugin.json b/web/i18n/locales/en-US/plugin.json similarity index 100% rename from web/i18n/en-US/plugin.json rename to web/i18n/locales/en-US/plugin.json diff --git a/web/i18n/en-US/register.json b/web/i18n/locales/en-US/register.json similarity index 100% rename from web/i18n/en-US/register.json rename to web/i18n/locales/en-US/register.json diff --git a/web/i18n/en-US/run-log.json b/web/i18n/locales/en-US/run-log.json similarity index 100% rename from web/i18n/en-US/run-log.json rename to web/i18n/locales/en-US/run-log.json diff --git a/web/i18n/en-US/share.json b/web/i18n/locales/en-US/share.json similarity index 100% rename from web/i18n/en-US/share.json rename to web/i18n/locales/en-US/share.json diff --git a/web/i18n/en-US/skill.json b/web/i18n/locales/en-US/skill.json similarity index 100% rename from web/i18n/en-US/skill.json rename to web/i18n/locales/en-US/skill.json diff --git a/web/i18n/en-US/snippet.json b/web/i18n/locales/en-US/snippet.json similarity index 100% rename from web/i18n/en-US/snippet.json rename to web/i18n/locales/en-US/snippet.json diff --git a/web/i18n/en-US/time.json b/web/i18n/locales/en-US/time.json similarity index 100% rename from web/i18n/en-US/time.json rename to web/i18n/locales/en-US/time.json diff --git a/web/i18n/en-US/tools.json b/web/i18n/locales/en-US/tools.json similarity index 100% rename from web/i18n/en-US/tools.json rename to web/i18n/locales/en-US/tools.json diff --git a/web/i18n/en-US/workflow.json b/web/i18n/locales/en-US/workflow.json similarity index 100% rename from web/i18n/en-US/workflow.json rename to web/i18n/locales/en-US/workflow.json diff --git a/web/i18n/es-ES/agent-v-2.json b/web/i18n/locales/es-ES/agent-v-2.json similarity index 100% rename from web/i18n/es-ES/agent-v-2.json rename to web/i18n/locales/es-ES/agent-v-2.json diff --git a/web/i18n/es-ES/app-annotation.json b/web/i18n/locales/es-ES/app-annotation.json similarity index 100% rename from web/i18n/es-ES/app-annotation.json rename to web/i18n/locales/es-ES/app-annotation.json diff --git a/web/i18n/es-ES/app-api.json b/web/i18n/locales/es-ES/app-api.json similarity index 100% rename from web/i18n/es-ES/app-api.json rename to web/i18n/locales/es-ES/app-api.json diff --git a/web/i18n/es-ES/app-debug.json b/web/i18n/locales/es-ES/app-debug.json similarity index 100% rename from web/i18n/es-ES/app-debug.json rename to web/i18n/locales/es-ES/app-debug.json diff --git a/web/i18n/es-ES/app-log.json b/web/i18n/locales/es-ES/app-log.json similarity index 100% rename from web/i18n/es-ES/app-log.json rename to web/i18n/locales/es-ES/app-log.json diff --git a/web/i18n/es-ES/app-overview.json b/web/i18n/locales/es-ES/app-overview.json similarity index 100% rename from web/i18n/es-ES/app-overview.json rename to web/i18n/locales/es-ES/app-overview.json diff --git a/web/i18n/es-ES/app.json b/web/i18n/locales/es-ES/app.json similarity index 100% rename from web/i18n/es-ES/app.json rename to web/i18n/locales/es-ES/app.json diff --git a/web/i18n/es-ES/billing.json b/web/i18n/locales/es-ES/billing.json similarity index 100% rename from web/i18n/es-ES/billing.json rename to web/i18n/locales/es-ES/billing.json diff --git a/web/i18n/es-ES/common.json b/web/i18n/locales/es-ES/common.json similarity index 100% rename from web/i18n/es-ES/common.json rename to web/i18n/locales/es-ES/common.json diff --git a/web/i18n/es-ES/custom.json b/web/i18n/locales/es-ES/custom.json similarity index 100% rename from web/i18n/es-ES/custom.json rename to web/i18n/locales/es-ES/custom.json diff --git a/web/i18n/es-ES/dataset-creation.json b/web/i18n/locales/es-ES/dataset-creation.json similarity index 100% rename from web/i18n/es-ES/dataset-creation.json rename to web/i18n/locales/es-ES/dataset-creation.json diff --git a/web/i18n/es-ES/dataset-documents.json b/web/i18n/locales/es-ES/dataset-documents.json similarity index 100% rename from web/i18n/es-ES/dataset-documents.json rename to web/i18n/locales/es-ES/dataset-documents.json diff --git a/web/i18n/es-ES/dataset-hit-testing.json b/web/i18n/locales/es-ES/dataset-hit-testing.json similarity index 100% rename from web/i18n/es-ES/dataset-hit-testing.json rename to web/i18n/locales/es-ES/dataset-hit-testing.json diff --git a/web/i18n/es-ES/dataset-pipeline.json b/web/i18n/locales/es-ES/dataset-pipeline.json similarity index 100% rename from web/i18n/es-ES/dataset-pipeline.json rename to web/i18n/locales/es-ES/dataset-pipeline.json diff --git a/web/i18n/es-ES/dataset-settings.json b/web/i18n/locales/es-ES/dataset-settings.json similarity index 100% rename from web/i18n/es-ES/dataset-settings.json rename to web/i18n/locales/es-ES/dataset-settings.json diff --git a/web/i18n/es-ES/dataset.json b/web/i18n/locales/es-ES/dataset.json similarity index 100% rename from web/i18n/es-ES/dataset.json rename to web/i18n/locales/es-ES/dataset.json diff --git a/web/i18n/es-ES/deployments.json b/web/i18n/locales/es-ES/deployments.json similarity index 100% rename from web/i18n/es-ES/deployments.json rename to web/i18n/locales/es-ES/deployments.json diff --git a/web/i18n/es-ES/device-flow.json b/web/i18n/locales/es-ES/device-flow.json similarity index 100% rename from web/i18n/es-ES/device-flow.json rename to web/i18n/locales/es-ES/device-flow.json diff --git a/web/i18n/es-ES/education.json b/web/i18n/locales/es-ES/education.json similarity index 100% rename from web/i18n/es-ES/education.json rename to web/i18n/locales/es-ES/education.json diff --git a/web/i18n/es-ES/explore.json b/web/i18n/locales/es-ES/explore.json similarity index 100% rename from web/i18n/es-ES/explore.json rename to web/i18n/locales/es-ES/explore.json diff --git a/web/i18n/es-ES/layout.json b/web/i18n/locales/es-ES/layout.json similarity index 100% rename from web/i18n/es-ES/layout.json rename to web/i18n/locales/es-ES/layout.json diff --git a/web/i18n/es-ES/login.json b/web/i18n/locales/es-ES/login.json similarity index 100% rename from web/i18n/es-ES/login.json rename to web/i18n/locales/es-ES/login.json diff --git a/web/i18n/es-ES/oauth.json b/web/i18n/locales/es-ES/oauth.json similarity index 100% rename from web/i18n/es-ES/oauth.json rename to web/i18n/locales/es-ES/oauth.json diff --git a/web/i18n/es-ES/permission-keys.json b/web/i18n/locales/es-ES/permission-keys.json similarity index 100% rename from web/i18n/es-ES/permission-keys.json rename to web/i18n/locales/es-ES/permission-keys.json diff --git a/web/i18n/es-ES/permission.json b/web/i18n/locales/es-ES/permission.json similarity index 100% rename from web/i18n/es-ES/permission.json rename to web/i18n/locales/es-ES/permission.json diff --git a/web/i18n/es-ES/pipeline.json b/web/i18n/locales/es-ES/pipeline.json similarity index 100% rename from web/i18n/es-ES/pipeline.json rename to web/i18n/locales/es-ES/pipeline.json diff --git a/web/i18n/es-ES/plugin-tags.json b/web/i18n/locales/es-ES/plugin-tags.json similarity index 100% rename from web/i18n/es-ES/plugin-tags.json rename to web/i18n/locales/es-ES/plugin-tags.json diff --git a/web/i18n/es-ES/plugin-trigger.json b/web/i18n/locales/es-ES/plugin-trigger.json similarity index 100% rename from web/i18n/es-ES/plugin-trigger.json rename to web/i18n/locales/es-ES/plugin-trigger.json diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/locales/es-ES/plugin.json similarity index 100% rename from web/i18n/es-ES/plugin.json rename to web/i18n/locales/es-ES/plugin.json diff --git a/web/i18n/es-ES/register.json b/web/i18n/locales/es-ES/register.json similarity index 100% rename from web/i18n/es-ES/register.json rename to web/i18n/locales/es-ES/register.json diff --git a/web/i18n/es-ES/run-log.json b/web/i18n/locales/es-ES/run-log.json similarity index 100% rename from web/i18n/es-ES/run-log.json rename to web/i18n/locales/es-ES/run-log.json diff --git a/web/i18n/es-ES/share.json b/web/i18n/locales/es-ES/share.json similarity index 100% rename from web/i18n/es-ES/share.json rename to web/i18n/locales/es-ES/share.json diff --git a/web/i18n/es-ES/skill.json b/web/i18n/locales/es-ES/skill.json similarity index 100% rename from web/i18n/es-ES/skill.json rename to web/i18n/locales/es-ES/skill.json diff --git a/web/i18n/es-ES/snippet.json b/web/i18n/locales/es-ES/snippet.json similarity index 100% rename from web/i18n/es-ES/snippet.json rename to web/i18n/locales/es-ES/snippet.json diff --git a/web/i18n/es-ES/time.json b/web/i18n/locales/es-ES/time.json similarity index 100% rename from web/i18n/es-ES/time.json rename to web/i18n/locales/es-ES/time.json diff --git a/web/i18n/es-ES/tools.json b/web/i18n/locales/es-ES/tools.json similarity index 100% rename from web/i18n/es-ES/tools.json rename to web/i18n/locales/es-ES/tools.json diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/locales/es-ES/workflow.json similarity index 100% rename from web/i18n/es-ES/workflow.json rename to web/i18n/locales/es-ES/workflow.json diff --git a/web/i18n/fa-IR/agent-v-2.json b/web/i18n/locales/fa-IR/agent-v-2.json similarity index 100% rename from web/i18n/fa-IR/agent-v-2.json rename to web/i18n/locales/fa-IR/agent-v-2.json diff --git a/web/i18n/fa-IR/app-annotation.json b/web/i18n/locales/fa-IR/app-annotation.json similarity index 100% rename from web/i18n/fa-IR/app-annotation.json rename to web/i18n/locales/fa-IR/app-annotation.json diff --git a/web/i18n/fa-IR/app-api.json b/web/i18n/locales/fa-IR/app-api.json similarity index 100% rename from web/i18n/fa-IR/app-api.json rename to web/i18n/locales/fa-IR/app-api.json diff --git a/web/i18n/fa-IR/app-debug.json b/web/i18n/locales/fa-IR/app-debug.json similarity index 100% rename from web/i18n/fa-IR/app-debug.json rename to web/i18n/locales/fa-IR/app-debug.json diff --git a/web/i18n/fa-IR/app-log.json b/web/i18n/locales/fa-IR/app-log.json similarity index 100% rename from web/i18n/fa-IR/app-log.json rename to web/i18n/locales/fa-IR/app-log.json diff --git a/web/i18n/fa-IR/app-overview.json b/web/i18n/locales/fa-IR/app-overview.json similarity index 100% rename from web/i18n/fa-IR/app-overview.json rename to web/i18n/locales/fa-IR/app-overview.json diff --git a/web/i18n/fa-IR/app.json b/web/i18n/locales/fa-IR/app.json similarity index 100% rename from web/i18n/fa-IR/app.json rename to web/i18n/locales/fa-IR/app.json diff --git a/web/i18n/fa-IR/billing.json b/web/i18n/locales/fa-IR/billing.json similarity index 100% rename from web/i18n/fa-IR/billing.json rename to web/i18n/locales/fa-IR/billing.json diff --git a/web/i18n/fa-IR/common.json b/web/i18n/locales/fa-IR/common.json similarity index 100% rename from web/i18n/fa-IR/common.json rename to web/i18n/locales/fa-IR/common.json diff --git a/web/i18n/fa-IR/custom.json b/web/i18n/locales/fa-IR/custom.json similarity index 100% rename from web/i18n/fa-IR/custom.json rename to web/i18n/locales/fa-IR/custom.json diff --git a/web/i18n/fa-IR/dataset-creation.json b/web/i18n/locales/fa-IR/dataset-creation.json similarity index 100% rename from web/i18n/fa-IR/dataset-creation.json rename to web/i18n/locales/fa-IR/dataset-creation.json diff --git a/web/i18n/fa-IR/dataset-documents.json b/web/i18n/locales/fa-IR/dataset-documents.json similarity index 100% rename from web/i18n/fa-IR/dataset-documents.json rename to web/i18n/locales/fa-IR/dataset-documents.json diff --git a/web/i18n/fa-IR/dataset-hit-testing.json b/web/i18n/locales/fa-IR/dataset-hit-testing.json similarity index 100% rename from web/i18n/fa-IR/dataset-hit-testing.json rename to web/i18n/locales/fa-IR/dataset-hit-testing.json diff --git a/web/i18n/fa-IR/dataset-pipeline.json b/web/i18n/locales/fa-IR/dataset-pipeline.json similarity index 100% rename from web/i18n/fa-IR/dataset-pipeline.json rename to web/i18n/locales/fa-IR/dataset-pipeline.json diff --git a/web/i18n/fa-IR/dataset-settings.json b/web/i18n/locales/fa-IR/dataset-settings.json similarity index 100% rename from web/i18n/fa-IR/dataset-settings.json rename to web/i18n/locales/fa-IR/dataset-settings.json diff --git a/web/i18n/fa-IR/dataset.json b/web/i18n/locales/fa-IR/dataset.json similarity index 100% rename from web/i18n/fa-IR/dataset.json rename to web/i18n/locales/fa-IR/dataset.json diff --git a/web/i18n/fa-IR/deployments.json b/web/i18n/locales/fa-IR/deployments.json similarity index 100% rename from web/i18n/fa-IR/deployments.json rename to web/i18n/locales/fa-IR/deployments.json diff --git a/web/i18n/fa-IR/device-flow.json b/web/i18n/locales/fa-IR/device-flow.json similarity index 100% rename from web/i18n/fa-IR/device-flow.json rename to web/i18n/locales/fa-IR/device-flow.json diff --git a/web/i18n/fa-IR/education.json b/web/i18n/locales/fa-IR/education.json similarity index 100% rename from web/i18n/fa-IR/education.json rename to web/i18n/locales/fa-IR/education.json diff --git a/web/i18n/fa-IR/explore.json b/web/i18n/locales/fa-IR/explore.json similarity index 100% rename from web/i18n/fa-IR/explore.json rename to web/i18n/locales/fa-IR/explore.json diff --git a/web/i18n/fa-IR/layout.json b/web/i18n/locales/fa-IR/layout.json similarity index 100% rename from web/i18n/fa-IR/layout.json rename to web/i18n/locales/fa-IR/layout.json diff --git a/web/i18n/fa-IR/login.json b/web/i18n/locales/fa-IR/login.json similarity index 100% rename from web/i18n/fa-IR/login.json rename to web/i18n/locales/fa-IR/login.json diff --git a/web/i18n/fa-IR/oauth.json b/web/i18n/locales/fa-IR/oauth.json similarity index 100% rename from web/i18n/fa-IR/oauth.json rename to web/i18n/locales/fa-IR/oauth.json diff --git a/web/i18n/fa-IR/permission-keys.json b/web/i18n/locales/fa-IR/permission-keys.json similarity index 100% rename from web/i18n/fa-IR/permission-keys.json rename to web/i18n/locales/fa-IR/permission-keys.json diff --git a/web/i18n/fa-IR/permission.json b/web/i18n/locales/fa-IR/permission.json similarity index 100% rename from web/i18n/fa-IR/permission.json rename to web/i18n/locales/fa-IR/permission.json diff --git a/web/i18n/fa-IR/pipeline.json b/web/i18n/locales/fa-IR/pipeline.json similarity index 100% rename from web/i18n/fa-IR/pipeline.json rename to web/i18n/locales/fa-IR/pipeline.json diff --git a/web/i18n/fa-IR/plugin-tags.json b/web/i18n/locales/fa-IR/plugin-tags.json similarity index 100% rename from web/i18n/fa-IR/plugin-tags.json rename to web/i18n/locales/fa-IR/plugin-tags.json diff --git a/web/i18n/fa-IR/plugin-trigger.json b/web/i18n/locales/fa-IR/plugin-trigger.json similarity index 100% rename from web/i18n/fa-IR/plugin-trigger.json rename to web/i18n/locales/fa-IR/plugin-trigger.json diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/locales/fa-IR/plugin.json similarity index 100% rename from web/i18n/fa-IR/plugin.json rename to web/i18n/locales/fa-IR/plugin.json diff --git a/web/i18n/fa-IR/register.json b/web/i18n/locales/fa-IR/register.json similarity index 100% rename from web/i18n/fa-IR/register.json rename to web/i18n/locales/fa-IR/register.json diff --git a/web/i18n/fa-IR/run-log.json b/web/i18n/locales/fa-IR/run-log.json similarity index 100% rename from web/i18n/fa-IR/run-log.json rename to web/i18n/locales/fa-IR/run-log.json diff --git a/web/i18n/fa-IR/share.json b/web/i18n/locales/fa-IR/share.json similarity index 100% rename from web/i18n/fa-IR/share.json rename to web/i18n/locales/fa-IR/share.json diff --git a/web/i18n/fa-IR/skill.json b/web/i18n/locales/fa-IR/skill.json similarity index 100% rename from web/i18n/fa-IR/skill.json rename to web/i18n/locales/fa-IR/skill.json diff --git a/web/i18n/fa-IR/snippet.json b/web/i18n/locales/fa-IR/snippet.json similarity index 100% rename from web/i18n/fa-IR/snippet.json rename to web/i18n/locales/fa-IR/snippet.json diff --git a/web/i18n/fa-IR/time.json b/web/i18n/locales/fa-IR/time.json similarity index 100% rename from web/i18n/fa-IR/time.json rename to web/i18n/locales/fa-IR/time.json diff --git a/web/i18n/fa-IR/tools.json b/web/i18n/locales/fa-IR/tools.json similarity index 100% rename from web/i18n/fa-IR/tools.json rename to web/i18n/locales/fa-IR/tools.json diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/locales/fa-IR/workflow.json similarity index 100% rename from web/i18n/fa-IR/workflow.json rename to web/i18n/locales/fa-IR/workflow.json diff --git a/web/i18n/fr-FR/agent-v-2.json b/web/i18n/locales/fr-FR/agent-v-2.json similarity index 100% rename from web/i18n/fr-FR/agent-v-2.json rename to web/i18n/locales/fr-FR/agent-v-2.json diff --git a/web/i18n/fr-FR/app-annotation.json b/web/i18n/locales/fr-FR/app-annotation.json similarity index 100% rename from web/i18n/fr-FR/app-annotation.json rename to web/i18n/locales/fr-FR/app-annotation.json diff --git a/web/i18n/fr-FR/app-api.json b/web/i18n/locales/fr-FR/app-api.json similarity index 100% rename from web/i18n/fr-FR/app-api.json rename to web/i18n/locales/fr-FR/app-api.json diff --git a/web/i18n/fr-FR/app-debug.json b/web/i18n/locales/fr-FR/app-debug.json similarity index 100% rename from web/i18n/fr-FR/app-debug.json rename to web/i18n/locales/fr-FR/app-debug.json diff --git a/web/i18n/fr-FR/app-log.json b/web/i18n/locales/fr-FR/app-log.json similarity index 100% rename from web/i18n/fr-FR/app-log.json rename to web/i18n/locales/fr-FR/app-log.json diff --git a/web/i18n/fr-FR/app-overview.json b/web/i18n/locales/fr-FR/app-overview.json similarity index 100% rename from web/i18n/fr-FR/app-overview.json rename to web/i18n/locales/fr-FR/app-overview.json diff --git a/web/i18n/fr-FR/app.json b/web/i18n/locales/fr-FR/app.json similarity index 100% rename from web/i18n/fr-FR/app.json rename to web/i18n/locales/fr-FR/app.json diff --git a/web/i18n/fr-FR/billing.json b/web/i18n/locales/fr-FR/billing.json similarity index 100% rename from web/i18n/fr-FR/billing.json rename to web/i18n/locales/fr-FR/billing.json diff --git a/web/i18n/fr-FR/common.json b/web/i18n/locales/fr-FR/common.json similarity index 100% rename from web/i18n/fr-FR/common.json rename to web/i18n/locales/fr-FR/common.json diff --git a/web/i18n/fr-FR/custom.json b/web/i18n/locales/fr-FR/custom.json similarity index 100% rename from web/i18n/fr-FR/custom.json rename to web/i18n/locales/fr-FR/custom.json diff --git a/web/i18n/fr-FR/dataset-creation.json b/web/i18n/locales/fr-FR/dataset-creation.json similarity index 100% rename from web/i18n/fr-FR/dataset-creation.json rename to web/i18n/locales/fr-FR/dataset-creation.json diff --git a/web/i18n/fr-FR/dataset-documents.json b/web/i18n/locales/fr-FR/dataset-documents.json similarity index 100% rename from web/i18n/fr-FR/dataset-documents.json rename to web/i18n/locales/fr-FR/dataset-documents.json diff --git a/web/i18n/fr-FR/dataset-hit-testing.json b/web/i18n/locales/fr-FR/dataset-hit-testing.json similarity index 100% rename from web/i18n/fr-FR/dataset-hit-testing.json rename to web/i18n/locales/fr-FR/dataset-hit-testing.json diff --git a/web/i18n/fr-FR/dataset-pipeline.json b/web/i18n/locales/fr-FR/dataset-pipeline.json similarity index 100% rename from web/i18n/fr-FR/dataset-pipeline.json rename to web/i18n/locales/fr-FR/dataset-pipeline.json diff --git a/web/i18n/fr-FR/dataset-settings.json b/web/i18n/locales/fr-FR/dataset-settings.json similarity index 100% rename from web/i18n/fr-FR/dataset-settings.json rename to web/i18n/locales/fr-FR/dataset-settings.json diff --git a/web/i18n/fr-FR/dataset.json b/web/i18n/locales/fr-FR/dataset.json similarity index 100% rename from web/i18n/fr-FR/dataset.json rename to web/i18n/locales/fr-FR/dataset.json diff --git a/web/i18n/fr-FR/deployments.json b/web/i18n/locales/fr-FR/deployments.json similarity index 100% rename from web/i18n/fr-FR/deployments.json rename to web/i18n/locales/fr-FR/deployments.json diff --git a/web/i18n/fr-FR/device-flow.json b/web/i18n/locales/fr-FR/device-flow.json similarity index 100% rename from web/i18n/fr-FR/device-flow.json rename to web/i18n/locales/fr-FR/device-flow.json diff --git a/web/i18n/fr-FR/education.json b/web/i18n/locales/fr-FR/education.json similarity index 100% rename from web/i18n/fr-FR/education.json rename to web/i18n/locales/fr-FR/education.json diff --git a/web/i18n/fr-FR/explore.json b/web/i18n/locales/fr-FR/explore.json similarity index 100% rename from web/i18n/fr-FR/explore.json rename to web/i18n/locales/fr-FR/explore.json diff --git a/web/i18n/fr-FR/layout.json b/web/i18n/locales/fr-FR/layout.json similarity index 100% rename from web/i18n/fr-FR/layout.json rename to web/i18n/locales/fr-FR/layout.json diff --git a/web/i18n/fr-FR/login.json b/web/i18n/locales/fr-FR/login.json similarity index 100% rename from web/i18n/fr-FR/login.json rename to web/i18n/locales/fr-FR/login.json diff --git a/web/i18n/fr-FR/oauth.json b/web/i18n/locales/fr-FR/oauth.json similarity index 100% rename from web/i18n/fr-FR/oauth.json rename to web/i18n/locales/fr-FR/oauth.json diff --git a/web/i18n/fr-FR/permission-keys.json b/web/i18n/locales/fr-FR/permission-keys.json similarity index 100% rename from web/i18n/fr-FR/permission-keys.json rename to web/i18n/locales/fr-FR/permission-keys.json diff --git a/web/i18n/fr-FR/permission.json b/web/i18n/locales/fr-FR/permission.json similarity index 100% rename from web/i18n/fr-FR/permission.json rename to web/i18n/locales/fr-FR/permission.json diff --git a/web/i18n/fr-FR/pipeline.json b/web/i18n/locales/fr-FR/pipeline.json similarity index 100% rename from web/i18n/fr-FR/pipeline.json rename to web/i18n/locales/fr-FR/pipeline.json diff --git a/web/i18n/fr-FR/plugin-tags.json b/web/i18n/locales/fr-FR/plugin-tags.json similarity index 100% rename from web/i18n/fr-FR/plugin-tags.json rename to web/i18n/locales/fr-FR/plugin-tags.json diff --git a/web/i18n/fr-FR/plugin-trigger.json b/web/i18n/locales/fr-FR/plugin-trigger.json similarity index 100% rename from web/i18n/fr-FR/plugin-trigger.json rename to web/i18n/locales/fr-FR/plugin-trigger.json diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/locales/fr-FR/plugin.json similarity index 100% rename from web/i18n/fr-FR/plugin.json rename to web/i18n/locales/fr-FR/plugin.json diff --git a/web/i18n/fr-FR/register.json b/web/i18n/locales/fr-FR/register.json similarity index 100% rename from web/i18n/fr-FR/register.json rename to web/i18n/locales/fr-FR/register.json diff --git a/web/i18n/fr-FR/run-log.json b/web/i18n/locales/fr-FR/run-log.json similarity index 100% rename from web/i18n/fr-FR/run-log.json rename to web/i18n/locales/fr-FR/run-log.json diff --git a/web/i18n/fr-FR/share.json b/web/i18n/locales/fr-FR/share.json similarity index 100% rename from web/i18n/fr-FR/share.json rename to web/i18n/locales/fr-FR/share.json diff --git a/web/i18n/fr-FR/skill.json b/web/i18n/locales/fr-FR/skill.json similarity index 100% rename from web/i18n/fr-FR/skill.json rename to web/i18n/locales/fr-FR/skill.json diff --git a/web/i18n/fr-FR/snippet.json b/web/i18n/locales/fr-FR/snippet.json similarity index 100% rename from web/i18n/fr-FR/snippet.json rename to web/i18n/locales/fr-FR/snippet.json diff --git a/web/i18n/fr-FR/time.json b/web/i18n/locales/fr-FR/time.json similarity index 100% rename from web/i18n/fr-FR/time.json rename to web/i18n/locales/fr-FR/time.json diff --git a/web/i18n/fr-FR/tools.json b/web/i18n/locales/fr-FR/tools.json similarity index 100% rename from web/i18n/fr-FR/tools.json rename to web/i18n/locales/fr-FR/tools.json diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/locales/fr-FR/workflow.json similarity index 100% rename from web/i18n/fr-FR/workflow.json rename to web/i18n/locales/fr-FR/workflow.json diff --git a/web/i18n/hi-IN/agent-v-2.json b/web/i18n/locales/hi-IN/agent-v-2.json similarity index 100% rename from web/i18n/hi-IN/agent-v-2.json rename to web/i18n/locales/hi-IN/agent-v-2.json diff --git a/web/i18n/hi-IN/app-annotation.json b/web/i18n/locales/hi-IN/app-annotation.json similarity index 100% rename from web/i18n/hi-IN/app-annotation.json rename to web/i18n/locales/hi-IN/app-annotation.json diff --git a/web/i18n/hi-IN/app-api.json b/web/i18n/locales/hi-IN/app-api.json similarity index 100% rename from web/i18n/hi-IN/app-api.json rename to web/i18n/locales/hi-IN/app-api.json diff --git a/web/i18n/hi-IN/app-debug.json b/web/i18n/locales/hi-IN/app-debug.json similarity index 100% rename from web/i18n/hi-IN/app-debug.json rename to web/i18n/locales/hi-IN/app-debug.json diff --git a/web/i18n/hi-IN/app-log.json b/web/i18n/locales/hi-IN/app-log.json similarity index 100% rename from web/i18n/hi-IN/app-log.json rename to web/i18n/locales/hi-IN/app-log.json diff --git a/web/i18n/hi-IN/app-overview.json b/web/i18n/locales/hi-IN/app-overview.json similarity index 100% rename from web/i18n/hi-IN/app-overview.json rename to web/i18n/locales/hi-IN/app-overview.json diff --git a/web/i18n/hi-IN/app.json b/web/i18n/locales/hi-IN/app.json similarity index 100% rename from web/i18n/hi-IN/app.json rename to web/i18n/locales/hi-IN/app.json diff --git a/web/i18n/hi-IN/billing.json b/web/i18n/locales/hi-IN/billing.json similarity index 100% rename from web/i18n/hi-IN/billing.json rename to web/i18n/locales/hi-IN/billing.json diff --git a/web/i18n/hi-IN/common.json b/web/i18n/locales/hi-IN/common.json similarity index 100% rename from web/i18n/hi-IN/common.json rename to web/i18n/locales/hi-IN/common.json diff --git a/web/i18n/hi-IN/custom.json b/web/i18n/locales/hi-IN/custom.json similarity index 100% rename from web/i18n/hi-IN/custom.json rename to web/i18n/locales/hi-IN/custom.json diff --git a/web/i18n/hi-IN/dataset-creation.json b/web/i18n/locales/hi-IN/dataset-creation.json similarity index 100% rename from web/i18n/hi-IN/dataset-creation.json rename to web/i18n/locales/hi-IN/dataset-creation.json diff --git a/web/i18n/hi-IN/dataset-documents.json b/web/i18n/locales/hi-IN/dataset-documents.json similarity index 100% rename from web/i18n/hi-IN/dataset-documents.json rename to web/i18n/locales/hi-IN/dataset-documents.json diff --git a/web/i18n/hi-IN/dataset-hit-testing.json b/web/i18n/locales/hi-IN/dataset-hit-testing.json similarity index 100% rename from web/i18n/hi-IN/dataset-hit-testing.json rename to web/i18n/locales/hi-IN/dataset-hit-testing.json diff --git a/web/i18n/hi-IN/dataset-pipeline.json b/web/i18n/locales/hi-IN/dataset-pipeline.json similarity index 100% rename from web/i18n/hi-IN/dataset-pipeline.json rename to web/i18n/locales/hi-IN/dataset-pipeline.json diff --git a/web/i18n/hi-IN/dataset-settings.json b/web/i18n/locales/hi-IN/dataset-settings.json similarity index 100% rename from web/i18n/hi-IN/dataset-settings.json rename to web/i18n/locales/hi-IN/dataset-settings.json diff --git a/web/i18n/hi-IN/dataset.json b/web/i18n/locales/hi-IN/dataset.json similarity index 100% rename from web/i18n/hi-IN/dataset.json rename to web/i18n/locales/hi-IN/dataset.json diff --git a/web/i18n/hi-IN/deployments.json b/web/i18n/locales/hi-IN/deployments.json similarity index 100% rename from web/i18n/hi-IN/deployments.json rename to web/i18n/locales/hi-IN/deployments.json diff --git a/web/i18n/hi-IN/device-flow.json b/web/i18n/locales/hi-IN/device-flow.json similarity index 100% rename from web/i18n/hi-IN/device-flow.json rename to web/i18n/locales/hi-IN/device-flow.json diff --git a/web/i18n/hi-IN/education.json b/web/i18n/locales/hi-IN/education.json similarity index 100% rename from web/i18n/hi-IN/education.json rename to web/i18n/locales/hi-IN/education.json diff --git a/web/i18n/hi-IN/explore.json b/web/i18n/locales/hi-IN/explore.json similarity index 100% rename from web/i18n/hi-IN/explore.json rename to web/i18n/locales/hi-IN/explore.json diff --git a/web/i18n/hi-IN/layout.json b/web/i18n/locales/hi-IN/layout.json similarity index 100% rename from web/i18n/hi-IN/layout.json rename to web/i18n/locales/hi-IN/layout.json diff --git a/web/i18n/hi-IN/login.json b/web/i18n/locales/hi-IN/login.json similarity index 100% rename from web/i18n/hi-IN/login.json rename to web/i18n/locales/hi-IN/login.json diff --git a/web/i18n/hi-IN/oauth.json b/web/i18n/locales/hi-IN/oauth.json similarity index 100% rename from web/i18n/hi-IN/oauth.json rename to web/i18n/locales/hi-IN/oauth.json diff --git a/web/i18n/hi-IN/permission-keys.json b/web/i18n/locales/hi-IN/permission-keys.json similarity index 100% rename from web/i18n/hi-IN/permission-keys.json rename to web/i18n/locales/hi-IN/permission-keys.json diff --git a/web/i18n/hi-IN/permission.json b/web/i18n/locales/hi-IN/permission.json similarity index 100% rename from web/i18n/hi-IN/permission.json rename to web/i18n/locales/hi-IN/permission.json diff --git a/web/i18n/hi-IN/pipeline.json b/web/i18n/locales/hi-IN/pipeline.json similarity index 100% rename from web/i18n/hi-IN/pipeline.json rename to web/i18n/locales/hi-IN/pipeline.json diff --git a/web/i18n/hi-IN/plugin-tags.json b/web/i18n/locales/hi-IN/plugin-tags.json similarity index 100% rename from web/i18n/hi-IN/plugin-tags.json rename to web/i18n/locales/hi-IN/plugin-tags.json diff --git a/web/i18n/hi-IN/plugin-trigger.json b/web/i18n/locales/hi-IN/plugin-trigger.json similarity index 100% rename from web/i18n/hi-IN/plugin-trigger.json rename to web/i18n/locales/hi-IN/plugin-trigger.json diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/locales/hi-IN/plugin.json similarity index 100% rename from web/i18n/hi-IN/plugin.json rename to web/i18n/locales/hi-IN/plugin.json diff --git a/web/i18n/hi-IN/register.json b/web/i18n/locales/hi-IN/register.json similarity index 100% rename from web/i18n/hi-IN/register.json rename to web/i18n/locales/hi-IN/register.json diff --git a/web/i18n/hi-IN/run-log.json b/web/i18n/locales/hi-IN/run-log.json similarity index 100% rename from web/i18n/hi-IN/run-log.json rename to web/i18n/locales/hi-IN/run-log.json diff --git a/web/i18n/hi-IN/share.json b/web/i18n/locales/hi-IN/share.json similarity index 100% rename from web/i18n/hi-IN/share.json rename to web/i18n/locales/hi-IN/share.json diff --git a/web/i18n/hi-IN/skill.json b/web/i18n/locales/hi-IN/skill.json similarity index 100% rename from web/i18n/hi-IN/skill.json rename to web/i18n/locales/hi-IN/skill.json diff --git a/web/i18n/hi-IN/snippet.json b/web/i18n/locales/hi-IN/snippet.json similarity index 100% rename from web/i18n/hi-IN/snippet.json rename to web/i18n/locales/hi-IN/snippet.json diff --git a/web/i18n/hi-IN/time.json b/web/i18n/locales/hi-IN/time.json similarity index 100% rename from web/i18n/hi-IN/time.json rename to web/i18n/locales/hi-IN/time.json diff --git a/web/i18n/hi-IN/tools.json b/web/i18n/locales/hi-IN/tools.json similarity index 100% rename from web/i18n/hi-IN/tools.json rename to web/i18n/locales/hi-IN/tools.json diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/locales/hi-IN/workflow.json similarity index 100% rename from web/i18n/hi-IN/workflow.json rename to web/i18n/locales/hi-IN/workflow.json diff --git a/web/i18n/id-ID/agent-v-2.json b/web/i18n/locales/id-ID/agent-v-2.json similarity index 100% rename from web/i18n/id-ID/agent-v-2.json rename to web/i18n/locales/id-ID/agent-v-2.json diff --git a/web/i18n/id-ID/app-annotation.json b/web/i18n/locales/id-ID/app-annotation.json similarity index 100% rename from web/i18n/id-ID/app-annotation.json rename to web/i18n/locales/id-ID/app-annotation.json diff --git a/web/i18n/id-ID/app-api.json b/web/i18n/locales/id-ID/app-api.json similarity index 100% rename from web/i18n/id-ID/app-api.json rename to web/i18n/locales/id-ID/app-api.json diff --git a/web/i18n/id-ID/app-debug.json b/web/i18n/locales/id-ID/app-debug.json similarity index 100% rename from web/i18n/id-ID/app-debug.json rename to web/i18n/locales/id-ID/app-debug.json diff --git a/web/i18n/id-ID/app-log.json b/web/i18n/locales/id-ID/app-log.json similarity index 100% rename from web/i18n/id-ID/app-log.json rename to web/i18n/locales/id-ID/app-log.json diff --git a/web/i18n/id-ID/app-overview.json b/web/i18n/locales/id-ID/app-overview.json similarity index 100% rename from web/i18n/id-ID/app-overview.json rename to web/i18n/locales/id-ID/app-overview.json diff --git a/web/i18n/id-ID/app.json b/web/i18n/locales/id-ID/app.json similarity index 100% rename from web/i18n/id-ID/app.json rename to web/i18n/locales/id-ID/app.json diff --git a/web/i18n/id-ID/billing.json b/web/i18n/locales/id-ID/billing.json similarity index 100% rename from web/i18n/id-ID/billing.json rename to web/i18n/locales/id-ID/billing.json diff --git a/web/i18n/id-ID/common.json b/web/i18n/locales/id-ID/common.json similarity index 100% rename from web/i18n/id-ID/common.json rename to web/i18n/locales/id-ID/common.json diff --git a/web/i18n/id-ID/custom.json b/web/i18n/locales/id-ID/custom.json similarity index 100% rename from web/i18n/id-ID/custom.json rename to web/i18n/locales/id-ID/custom.json diff --git a/web/i18n/id-ID/dataset-creation.json b/web/i18n/locales/id-ID/dataset-creation.json similarity index 100% rename from web/i18n/id-ID/dataset-creation.json rename to web/i18n/locales/id-ID/dataset-creation.json diff --git a/web/i18n/id-ID/dataset-documents.json b/web/i18n/locales/id-ID/dataset-documents.json similarity index 100% rename from web/i18n/id-ID/dataset-documents.json rename to web/i18n/locales/id-ID/dataset-documents.json diff --git a/web/i18n/id-ID/dataset-hit-testing.json b/web/i18n/locales/id-ID/dataset-hit-testing.json similarity index 100% rename from web/i18n/id-ID/dataset-hit-testing.json rename to web/i18n/locales/id-ID/dataset-hit-testing.json diff --git a/web/i18n/id-ID/dataset-pipeline.json b/web/i18n/locales/id-ID/dataset-pipeline.json similarity index 100% rename from web/i18n/id-ID/dataset-pipeline.json rename to web/i18n/locales/id-ID/dataset-pipeline.json diff --git a/web/i18n/id-ID/dataset-settings.json b/web/i18n/locales/id-ID/dataset-settings.json similarity index 100% rename from web/i18n/id-ID/dataset-settings.json rename to web/i18n/locales/id-ID/dataset-settings.json diff --git a/web/i18n/id-ID/dataset.json b/web/i18n/locales/id-ID/dataset.json similarity index 100% rename from web/i18n/id-ID/dataset.json rename to web/i18n/locales/id-ID/dataset.json diff --git a/web/i18n/id-ID/deployments.json b/web/i18n/locales/id-ID/deployments.json similarity index 100% rename from web/i18n/id-ID/deployments.json rename to web/i18n/locales/id-ID/deployments.json diff --git a/web/i18n/id-ID/device-flow.json b/web/i18n/locales/id-ID/device-flow.json similarity index 100% rename from web/i18n/id-ID/device-flow.json rename to web/i18n/locales/id-ID/device-flow.json diff --git a/web/i18n/id-ID/education.json b/web/i18n/locales/id-ID/education.json similarity index 100% rename from web/i18n/id-ID/education.json rename to web/i18n/locales/id-ID/education.json diff --git a/web/i18n/id-ID/explore.json b/web/i18n/locales/id-ID/explore.json similarity index 100% rename from web/i18n/id-ID/explore.json rename to web/i18n/locales/id-ID/explore.json diff --git a/web/i18n/id-ID/layout.json b/web/i18n/locales/id-ID/layout.json similarity index 100% rename from web/i18n/id-ID/layout.json rename to web/i18n/locales/id-ID/layout.json diff --git a/web/i18n/id-ID/login.json b/web/i18n/locales/id-ID/login.json similarity index 100% rename from web/i18n/id-ID/login.json rename to web/i18n/locales/id-ID/login.json diff --git a/web/i18n/id-ID/oauth.json b/web/i18n/locales/id-ID/oauth.json similarity index 100% rename from web/i18n/id-ID/oauth.json rename to web/i18n/locales/id-ID/oauth.json diff --git a/web/i18n/id-ID/permission-keys.json b/web/i18n/locales/id-ID/permission-keys.json similarity index 100% rename from web/i18n/id-ID/permission-keys.json rename to web/i18n/locales/id-ID/permission-keys.json diff --git a/web/i18n/id-ID/permission.json b/web/i18n/locales/id-ID/permission.json similarity index 100% rename from web/i18n/id-ID/permission.json rename to web/i18n/locales/id-ID/permission.json diff --git a/web/i18n/id-ID/pipeline.json b/web/i18n/locales/id-ID/pipeline.json similarity index 100% rename from web/i18n/id-ID/pipeline.json rename to web/i18n/locales/id-ID/pipeline.json diff --git a/web/i18n/id-ID/plugin-tags.json b/web/i18n/locales/id-ID/plugin-tags.json similarity index 100% rename from web/i18n/id-ID/plugin-tags.json rename to web/i18n/locales/id-ID/plugin-tags.json diff --git a/web/i18n/id-ID/plugin-trigger.json b/web/i18n/locales/id-ID/plugin-trigger.json similarity index 100% rename from web/i18n/id-ID/plugin-trigger.json rename to web/i18n/locales/id-ID/plugin-trigger.json diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/locales/id-ID/plugin.json similarity index 100% rename from web/i18n/id-ID/plugin.json rename to web/i18n/locales/id-ID/plugin.json diff --git a/web/i18n/id-ID/register.json b/web/i18n/locales/id-ID/register.json similarity index 100% rename from web/i18n/id-ID/register.json rename to web/i18n/locales/id-ID/register.json diff --git a/web/i18n/id-ID/run-log.json b/web/i18n/locales/id-ID/run-log.json similarity index 100% rename from web/i18n/id-ID/run-log.json rename to web/i18n/locales/id-ID/run-log.json diff --git a/web/i18n/id-ID/share.json b/web/i18n/locales/id-ID/share.json similarity index 100% rename from web/i18n/id-ID/share.json rename to web/i18n/locales/id-ID/share.json diff --git a/web/i18n/id-ID/skill.json b/web/i18n/locales/id-ID/skill.json similarity index 100% rename from web/i18n/id-ID/skill.json rename to web/i18n/locales/id-ID/skill.json diff --git a/web/i18n/id-ID/snippet.json b/web/i18n/locales/id-ID/snippet.json similarity index 100% rename from web/i18n/id-ID/snippet.json rename to web/i18n/locales/id-ID/snippet.json diff --git a/web/i18n/id-ID/time.json b/web/i18n/locales/id-ID/time.json similarity index 100% rename from web/i18n/id-ID/time.json rename to web/i18n/locales/id-ID/time.json diff --git a/web/i18n/id-ID/tools.json b/web/i18n/locales/id-ID/tools.json similarity index 100% rename from web/i18n/id-ID/tools.json rename to web/i18n/locales/id-ID/tools.json diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/locales/id-ID/workflow.json similarity index 100% rename from web/i18n/id-ID/workflow.json rename to web/i18n/locales/id-ID/workflow.json diff --git a/web/i18n/it-IT/agent-v-2.json b/web/i18n/locales/it-IT/agent-v-2.json similarity index 100% rename from web/i18n/it-IT/agent-v-2.json rename to web/i18n/locales/it-IT/agent-v-2.json diff --git a/web/i18n/it-IT/app-annotation.json b/web/i18n/locales/it-IT/app-annotation.json similarity index 100% rename from web/i18n/it-IT/app-annotation.json rename to web/i18n/locales/it-IT/app-annotation.json diff --git a/web/i18n/it-IT/app-api.json b/web/i18n/locales/it-IT/app-api.json similarity index 100% rename from web/i18n/it-IT/app-api.json rename to web/i18n/locales/it-IT/app-api.json diff --git a/web/i18n/it-IT/app-debug.json b/web/i18n/locales/it-IT/app-debug.json similarity index 100% rename from web/i18n/it-IT/app-debug.json rename to web/i18n/locales/it-IT/app-debug.json diff --git a/web/i18n/it-IT/app-log.json b/web/i18n/locales/it-IT/app-log.json similarity index 100% rename from web/i18n/it-IT/app-log.json rename to web/i18n/locales/it-IT/app-log.json diff --git a/web/i18n/it-IT/app-overview.json b/web/i18n/locales/it-IT/app-overview.json similarity index 100% rename from web/i18n/it-IT/app-overview.json rename to web/i18n/locales/it-IT/app-overview.json diff --git a/web/i18n/it-IT/app.json b/web/i18n/locales/it-IT/app.json similarity index 100% rename from web/i18n/it-IT/app.json rename to web/i18n/locales/it-IT/app.json diff --git a/web/i18n/it-IT/billing.json b/web/i18n/locales/it-IT/billing.json similarity index 100% rename from web/i18n/it-IT/billing.json rename to web/i18n/locales/it-IT/billing.json diff --git a/web/i18n/it-IT/common.json b/web/i18n/locales/it-IT/common.json similarity index 100% rename from web/i18n/it-IT/common.json rename to web/i18n/locales/it-IT/common.json diff --git a/web/i18n/it-IT/custom.json b/web/i18n/locales/it-IT/custom.json similarity index 100% rename from web/i18n/it-IT/custom.json rename to web/i18n/locales/it-IT/custom.json diff --git a/web/i18n/it-IT/dataset-creation.json b/web/i18n/locales/it-IT/dataset-creation.json similarity index 100% rename from web/i18n/it-IT/dataset-creation.json rename to web/i18n/locales/it-IT/dataset-creation.json diff --git a/web/i18n/it-IT/dataset-documents.json b/web/i18n/locales/it-IT/dataset-documents.json similarity index 100% rename from web/i18n/it-IT/dataset-documents.json rename to web/i18n/locales/it-IT/dataset-documents.json diff --git a/web/i18n/it-IT/dataset-hit-testing.json b/web/i18n/locales/it-IT/dataset-hit-testing.json similarity index 100% rename from web/i18n/it-IT/dataset-hit-testing.json rename to web/i18n/locales/it-IT/dataset-hit-testing.json diff --git a/web/i18n/it-IT/dataset-pipeline.json b/web/i18n/locales/it-IT/dataset-pipeline.json similarity index 100% rename from web/i18n/it-IT/dataset-pipeline.json rename to web/i18n/locales/it-IT/dataset-pipeline.json diff --git a/web/i18n/it-IT/dataset-settings.json b/web/i18n/locales/it-IT/dataset-settings.json similarity index 100% rename from web/i18n/it-IT/dataset-settings.json rename to web/i18n/locales/it-IT/dataset-settings.json diff --git a/web/i18n/it-IT/dataset.json b/web/i18n/locales/it-IT/dataset.json similarity index 100% rename from web/i18n/it-IT/dataset.json rename to web/i18n/locales/it-IT/dataset.json diff --git a/web/i18n/it-IT/deployments.json b/web/i18n/locales/it-IT/deployments.json similarity index 100% rename from web/i18n/it-IT/deployments.json rename to web/i18n/locales/it-IT/deployments.json diff --git a/web/i18n/it-IT/device-flow.json b/web/i18n/locales/it-IT/device-flow.json similarity index 100% rename from web/i18n/it-IT/device-flow.json rename to web/i18n/locales/it-IT/device-flow.json diff --git a/web/i18n/it-IT/education.json b/web/i18n/locales/it-IT/education.json similarity index 100% rename from web/i18n/it-IT/education.json rename to web/i18n/locales/it-IT/education.json diff --git a/web/i18n/it-IT/explore.json b/web/i18n/locales/it-IT/explore.json similarity index 100% rename from web/i18n/it-IT/explore.json rename to web/i18n/locales/it-IT/explore.json diff --git a/web/i18n/it-IT/layout.json b/web/i18n/locales/it-IT/layout.json similarity index 100% rename from web/i18n/it-IT/layout.json rename to web/i18n/locales/it-IT/layout.json diff --git a/web/i18n/it-IT/login.json b/web/i18n/locales/it-IT/login.json similarity index 100% rename from web/i18n/it-IT/login.json rename to web/i18n/locales/it-IT/login.json diff --git a/web/i18n/it-IT/oauth.json b/web/i18n/locales/it-IT/oauth.json similarity index 100% rename from web/i18n/it-IT/oauth.json rename to web/i18n/locales/it-IT/oauth.json diff --git a/web/i18n/it-IT/permission-keys.json b/web/i18n/locales/it-IT/permission-keys.json similarity index 100% rename from web/i18n/it-IT/permission-keys.json rename to web/i18n/locales/it-IT/permission-keys.json diff --git a/web/i18n/it-IT/permission.json b/web/i18n/locales/it-IT/permission.json similarity index 100% rename from web/i18n/it-IT/permission.json rename to web/i18n/locales/it-IT/permission.json diff --git a/web/i18n/it-IT/pipeline.json b/web/i18n/locales/it-IT/pipeline.json similarity index 100% rename from web/i18n/it-IT/pipeline.json rename to web/i18n/locales/it-IT/pipeline.json diff --git a/web/i18n/it-IT/plugin-tags.json b/web/i18n/locales/it-IT/plugin-tags.json similarity index 100% rename from web/i18n/it-IT/plugin-tags.json rename to web/i18n/locales/it-IT/plugin-tags.json diff --git a/web/i18n/it-IT/plugin-trigger.json b/web/i18n/locales/it-IT/plugin-trigger.json similarity index 100% rename from web/i18n/it-IT/plugin-trigger.json rename to web/i18n/locales/it-IT/plugin-trigger.json diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/locales/it-IT/plugin.json similarity index 100% rename from web/i18n/it-IT/plugin.json rename to web/i18n/locales/it-IT/plugin.json diff --git a/web/i18n/it-IT/register.json b/web/i18n/locales/it-IT/register.json similarity index 100% rename from web/i18n/it-IT/register.json rename to web/i18n/locales/it-IT/register.json diff --git a/web/i18n/it-IT/run-log.json b/web/i18n/locales/it-IT/run-log.json similarity index 100% rename from web/i18n/it-IT/run-log.json rename to web/i18n/locales/it-IT/run-log.json diff --git a/web/i18n/it-IT/share.json b/web/i18n/locales/it-IT/share.json similarity index 100% rename from web/i18n/it-IT/share.json rename to web/i18n/locales/it-IT/share.json diff --git a/web/i18n/it-IT/skill.json b/web/i18n/locales/it-IT/skill.json similarity index 100% rename from web/i18n/it-IT/skill.json rename to web/i18n/locales/it-IT/skill.json diff --git a/web/i18n/it-IT/snippet.json b/web/i18n/locales/it-IT/snippet.json similarity index 100% rename from web/i18n/it-IT/snippet.json rename to web/i18n/locales/it-IT/snippet.json diff --git a/web/i18n/it-IT/time.json b/web/i18n/locales/it-IT/time.json similarity index 100% rename from web/i18n/it-IT/time.json rename to web/i18n/locales/it-IT/time.json diff --git a/web/i18n/it-IT/tools.json b/web/i18n/locales/it-IT/tools.json similarity index 100% rename from web/i18n/it-IT/tools.json rename to web/i18n/locales/it-IT/tools.json diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/locales/it-IT/workflow.json similarity index 100% rename from web/i18n/it-IT/workflow.json rename to web/i18n/locales/it-IT/workflow.json diff --git a/web/i18n/ja-JP/agent-v-2.json b/web/i18n/locales/ja-JP/agent-v-2.json similarity index 100% rename from web/i18n/ja-JP/agent-v-2.json rename to web/i18n/locales/ja-JP/agent-v-2.json diff --git a/web/i18n/ja-JP/app-annotation.json b/web/i18n/locales/ja-JP/app-annotation.json similarity index 100% rename from web/i18n/ja-JP/app-annotation.json rename to web/i18n/locales/ja-JP/app-annotation.json diff --git a/web/i18n/ja-JP/app-api.json b/web/i18n/locales/ja-JP/app-api.json similarity index 100% rename from web/i18n/ja-JP/app-api.json rename to web/i18n/locales/ja-JP/app-api.json diff --git a/web/i18n/ja-JP/app-debug.json b/web/i18n/locales/ja-JP/app-debug.json similarity index 100% rename from web/i18n/ja-JP/app-debug.json rename to web/i18n/locales/ja-JP/app-debug.json diff --git a/web/i18n/ja-JP/app-log.json b/web/i18n/locales/ja-JP/app-log.json similarity index 100% rename from web/i18n/ja-JP/app-log.json rename to web/i18n/locales/ja-JP/app-log.json diff --git a/web/i18n/ja-JP/app-overview.json b/web/i18n/locales/ja-JP/app-overview.json similarity index 100% rename from web/i18n/ja-JP/app-overview.json rename to web/i18n/locales/ja-JP/app-overview.json diff --git a/web/i18n/ja-JP/app.json b/web/i18n/locales/ja-JP/app.json similarity index 100% rename from web/i18n/ja-JP/app.json rename to web/i18n/locales/ja-JP/app.json diff --git a/web/i18n/ja-JP/billing.json b/web/i18n/locales/ja-JP/billing.json similarity index 100% rename from web/i18n/ja-JP/billing.json rename to web/i18n/locales/ja-JP/billing.json diff --git a/web/i18n/ja-JP/common.json b/web/i18n/locales/ja-JP/common.json similarity index 100% rename from web/i18n/ja-JP/common.json rename to web/i18n/locales/ja-JP/common.json diff --git a/web/i18n/ja-JP/custom.json b/web/i18n/locales/ja-JP/custom.json similarity index 100% rename from web/i18n/ja-JP/custom.json rename to web/i18n/locales/ja-JP/custom.json diff --git a/web/i18n/ja-JP/dataset-creation.json b/web/i18n/locales/ja-JP/dataset-creation.json similarity index 100% rename from web/i18n/ja-JP/dataset-creation.json rename to web/i18n/locales/ja-JP/dataset-creation.json diff --git a/web/i18n/ja-JP/dataset-documents.json b/web/i18n/locales/ja-JP/dataset-documents.json similarity index 100% rename from web/i18n/ja-JP/dataset-documents.json rename to web/i18n/locales/ja-JP/dataset-documents.json diff --git a/web/i18n/ja-JP/dataset-hit-testing.json b/web/i18n/locales/ja-JP/dataset-hit-testing.json similarity index 100% rename from web/i18n/ja-JP/dataset-hit-testing.json rename to web/i18n/locales/ja-JP/dataset-hit-testing.json diff --git a/web/i18n/ja-JP/dataset-pipeline.json b/web/i18n/locales/ja-JP/dataset-pipeline.json similarity index 100% rename from web/i18n/ja-JP/dataset-pipeline.json rename to web/i18n/locales/ja-JP/dataset-pipeline.json diff --git a/web/i18n/ja-JP/dataset-settings.json b/web/i18n/locales/ja-JP/dataset-settings.json similarity index 100% rename from web/i18n/ja-JP/dataset-settings.json rename to web/i18n/locales/ja-JP/dataset-settings.json diff --git a/web/i18n/ja-JP/dataset.json b/web/i18n/locales/ja-JP/dataset.json similarity index 100% rename from web/i18n/ja-JP/dataset.json rename to web/i18n/locales/ja-JP/dataset.json diff --git a/web/i18n/ja-JP/deployments.json b/web/i18n/locales/ja-JP/deployments.json similarity index 100% rename from web/i18n/ja-JP/deployments.json rename to web/i18n/locales/ja-JP/deployments.json diff --git a/web/i18n/ja-JP/device-flow.json b/web/i18n/locales/ja-JP/device-flow.json similarity index 100% rename from web/i18n/ja-JP/device-flow.json rename to web/i18n/locales/ja-JP/device-flow.json diff --git a/web/i18n/ja-JP/education.json b/web/i18n/locales/ja-JP/education.json similarity index 100% rename from web/i18n/ja-JP/education.json rename to web/i18n/locales/ja-JP/education.json diff --git a/web/i18n/ja-JP/explore.json b/web/i18n/locales/ja-JP/explore.json similarity index 100% rename from web/i18n/ja-JP/explore.json rename to web/i18n/locales/ja-JP/explore.json diff --git a/web/i18n/ja-JP/layout.json b/web/i18n/locales/ja-JP/layout.json similarity index 100% rename from web/i18n/ja-JP/layout.json rename to web/i18n/locales/ja-JP/layout.json diff --git a/web/i18n/ja-JP/login.json b/web/i18n/locales/ja-JP/login.json similarity index 100% rename from web/i18n/ja-JP/login.json rename to web/i18n/locales/ja-JP/login.json diff --git a/web/i18n/ja-JP/oauth.json b/web/i18n/locales/ja-JP/oauth.json similarity index 100% rename from web/i18n/ja-JP/oauth.json rename to web/i18n/locales/ja-JP/oauth.json diff --git a/web/i18n/ja-JP/permission-keys.json b/web/i18n/locales/ja-JP/permission-keys.json similarity index 100% rename from web/i18n/ja-JP/permission-keys.json rename to web/i18n/locales/ja-JP/permission-keys.json diff --git a/web/i18n/ja-JP/permission.json b/web/i18n/locales/ja-JP/permission.json similarity index 100% rename from web/i18n/ja-JP/permission.json rename to web/i18n/locales/ja-JP/permission.json diff --git a/web/i18n/ja-JP/pipeline.json b/web/i18n/locales/ja-JP/pipeline.json similarity index 100% rename from web/i18n/ja-JP/pipeline.json rename to web/i18n/locales/ja-JP/pipeline.json diff --git a/web/i18n/ja-JP/plugin-tags.json b/web/i18n/locales/ja-JP/plugin-tags.json similarity index 100% rename from web/i18n/ja-JP/plugin-tags.json rename to web/i18n/locales/ja-JP/plugin-tags.json diff --git a/web/i18n/ja-JP/plugin-trigger.json b/web/i18n/locales/ja-JP/plugin-trigger.json similarity index 100% rename from web/i18n/ja-JP/plugin-trigger.json rename to web/i18n/locales/ja-JP/plugin-trigger.json diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/locales/ja-JP/plugin.json similarity index 100% rename from web/i18n/ja-JP/plugin.json rename to web/i18n/locales/ja-JP/plugin.json diff --git a/web/i18n/ja-JP/register.json b/web/i18n/locales/ja-JP/register.json similarity index 100% rename from web/i18n/ja-JP/register.json rename to web/i18n/locales/ja-JP/register.json diff --git a/web/i18n/ja-JP/run-log.json b/web/i18n/locales/ja-JP/run-log.json similarity index 100% rename from web/i18n/ja-JP/run-log.json rename to web/i18n/locales/ja-JP/run-log.json diff --git a/web/i18n/ja-JP/share.json b/web/i18n/locales/ja-JP/share.json similarity index 100% rename from web/i18n/ja-JP/share.json rename to web/i18n/locales/ja-JP/share.json diff --git a/web/i18n/ja-JP/skill.json b/web/i18n/locales/ja-JP/skill.json similarity index 100% rename from web/i18n/ja-JP/skill.json rename to web/i18n/locales/ja-JP/skill.json diff --git a/web/i18n/ja-JP/snippet.json b/web/i18n/locales/ja-JP/snippet.json similarity index 100% rename from web/i18n/ja-JP/snippet.json rename to web/i18n/locales/ja-JP/snippet.json diff --git a/web/i18n/ja-JP/time.json b/web/i18n/locales/ja-JP/time.json similarity index 100% rename from web/i18n/ja-JP/time.json rename to web/i18n/locales/ja-JP/time.json diff --git a/web/i18n/ja-JP/tools.json b/web/i18n/locales/ja-JP/tools.json similarity index 100% rename from web/i18n/ja-JP/tools.json rename to web/i18n/locales/ja-JP/tools.json diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/locales/ja-JP/workflow.json similarity index 100% rename from web/i18n/ja-JP/workflow.json rename to web/i18n/locales/ja-JP/workflow.json diff --git a/web/i18n/ko-KR/agent-v-2.json b/web/i18n/locales/ko-KR/agent-v-2.json similarity index 100% rename from web/i18n/ko-KR/agent-v-2.json rename to web/i18n/locales/ko-KR/agent-v-2.json diff --git a/web/i18n/ko-KR/app-annotation.json b/web/i18n/locales/ko-KR/app-annotation.json similarity index 100% rename from web/i18n/ko-KR/app-annotation.json rename to web/i18n/locales/ko-KR/app-annotation.json diff --git a/web/i18n/ko-KR/app-api.json b/web/i18n/locales/ko-KR/app-api.json similarity index 100% rename from web/i18n/ko-KR/app-api.json rename to web/i18n/locales/ko-KR/app-api.json diff --git a/web/i18n/ko-KR/app-debug.json b/web/i18n/locales/ko-KR/app-debug.json similarity index 100% rename from web/i18n/ko-KR/app-debug.json rename to web/i18n/locales/ko-KR/app-debug.json diff --git a/web/i18n/ko-KR/app-log.json b/web/i18n/locales/ko-KR/app-log.json similarity index 100% rename from web/i18n/ko-KR/app-log.json rename to web/i18n/locales/ko-KR/app-log.json diff --git a/web/i18n/ko-KR/app-overview.json b/web/i18n/locales/ko-KR/app-overview.json similarity index 100% rename from web/i18n/ko-KR/app-overview.json rename to web/i18n/locales/ko-KR/app-overview.json diff --git a/web/i18n/ko-KR/app.json b/web/i18n/locales/ko-KR/app.json similarity index 100% rename from web/i18n/ko-KR/app.json rename to web/i18n/locales/ko-KR/app.json diff --git a/web/i18n/ko-KR/billing.json b/web/i18n/locales/ko-KR/billing.json similarity index 100% rename from web/i18n/ko-KR/billing.json rename to web/i18n/locales/ko-KR/billing.json diff --git a/web/i18n/ko-KR/common.json b/web/i18n/locales/ko-KR/common.json similarity index 100% rename from web/i18n/ko-KR/common.json rename to web/i18n/locales/ko-KR/common.json diff --git a/web/i18n/ko-KR/custom.json b/web/i18n/locales/ko-KR/custom.json similarity index 100% rename from web/i18n/ko-KR/custom.json rename to web/i18n/locales/ko-KR/custom.json diff --git a/web/i18n/ko-KR/dataset-creation.json b/web/i18n/locales/ko-KR/dataset-creation.json similarity index 100% rename from web/i18n/ko-KR/dataset-creation.json rename to web/i18n/locales/ko-KR/dataset-creation.json diff --git a/web/i18n/ko-KR/dataset-documents.json b/web/i18n/locales/ko-KR/dataset-documents.json similarity index 100% rename from web/i18n/ko-KR/dataset-documents.json rename to web/i18n/locales/ko-KR/dataset-documents.json diff --git a/web/i18n/ko-KR/dataset-hit-testing.json b/web/i18n/locales/ko-KR/dataset-hit-testing.json similarity index 100% rename from web/i18n/ko-KR/dataset-hit-testing.json rename to web/i18n/locales/ko-KR/dataset-hit-testing.json diff --git a/web/i18n/ko-KR/dataset-pipeline.json b/web/i18n/locales/ko-KR/dataset-pipeline.json similarity index 100% rename from web/i18n/ko-KR/dataset-pipeline.json rename to web/i18n/locales/ko-KR/dataset-pipeline.json diff --git a/web/i18n/ko-KR/dataset-settings.json b/web/i18n/locales/ko-KR/dataset-settings.json similarity index 100% rename from web/i18n/ko-KR/dataset-settings.json rename to web/i18n/locales/ko-KR/dataset-settings.json diff --git a/web/i18n/ko-KR/dataset.json b/web/i18n/locales/ko-KR/dataset.json similarity index 100% rename from web/i18n/ko-KR/dataset.json rename to web/i18n/locales/ko-KR/dataset.json diff --git a/web/i18n/ko-KR/deployments.json b/web/i18n/locales/ko-KR/deployments.json similarity index 100% rename from web/i18n/ko-KR/deployments.json rename to web/i18n/locales/ko-KR/deployments.json diff --git a/web/i18n/ko-KR/device-flow.json b/web/i18n/locales/ko-KR/device-flow.json similarity index 100% rename from web/i18n/ko-KR/device-flow.json rename to web/i18n/locales/ko-KR/device-flow.json diff --git a/web/i18n/ko-KR/education.json b/web/i18n/locales/ko-KR/education.json similarity index 100% rename from web/i18n/ko-KR/education.json rename to web/i18n/locales/ko-KR/education.json diff --git a/web/i18n/ko-KR/explore.json b/web/i18n/locales/ko-KR/explore.json similarity index 100% rename from web/i18n/ko-KR/explore.json rename to web/i18n/locales/ko-KR/explore.json diff --git a/web/i18n/ko-KR/layout.json b/web/i18n/locales/ko-KR/layout.json similarity index 100% rename from web/i18n/ko-KR/layout.json rename to web/i18n/locales/ko-KR/layout.json diff --git a/web/i18n/ko-KR/login.json b/web/i18n/locales/ko-KR/login.json similarity index 100% rename from web/i18n/ko-KR/login.json rename to web/i18n/locales/ko-KR/login.json diff --git a/web/i18n/ko-KR/oauth.json b/web/i18n/locales/ko-KR/oauth.json similarity index 100% rename from web/i18n/ko-KR/oauth.json rename to web/i18n/locales/ko-KR/oauth.json diff --git a/web/i18n/ko-KR/permission-keys.json b/web/i18n/locales/ko-KR/permission-keys.json similarity index 100% rename from web/i18n/ko-KR/permission-keys.json rename to web/i18n/locales/ko-KR/permission-keys.json diff --git a/web/i18n/ko-KR/permission.json b/web/i18n/locales/ko-KR/permission.json similarity index 100% rename from web/i18n/ko-KR/permission.json rename to web/i18n/locales/ko-KR/permission.json diff --git a/web/i18n/ko-KR/pipeline.json b/web/i18n/locales/ko-KR/pipeline.json similarity index 100% rename from web/i18n/ko-KR/pipeline.json rename to web/i18n/locales/ko-KR/pipeline.json diff --git a/web/i18n/ko-KR/plugin-tags.json b/web/i18n/locales/ko-KR/plugin-tags.json similarity index 100% rename from web/i18n/ko-KR/plugin-tags.json rename to web/i18n/locales/ko-KR/plugin-tags.json diff --git a/web/i18n/ko-KR/plugin-trigger.json b/web/i18n/locales/ko-KR/plugin-trigger.json similarity index 100% rename from web/i18n/ko-KR/plugin-trigger.json rename to web/i18n/locales/ko-KR/plugin-trigger.json diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/locales/ko-KR/plugin.json similarity index 100% rename from web/i18n/ko-KR/plugin.json rename to web/i18n/locales/ko-KR/plugin.json diff --git a/web/i18n/ko-KR/register.json b/web/i18n/locales/ko-KR/register.json similarity index 100% rename from web/i18n/ko-KR/register.json rename to web/i18n/locales/ko-KR/register.json diff --git a/web/i18n/ko-KR/run-log.json b/web/i18n/locales/ko-KR/run-log.json similarity index 100% rename from web/i18n/ko-KR/run-log.json rename to web/i18n/locales/ko-KR/run-log.json diff --git a/web/i18n/ko-KR/share.json b/web/i18n/locales/ko-KR/share.json similarity index 100% rename from web/i18n/ko-KR/share.json rename to web/i18n/locales/ko-KR/share.json diff --git a/web/i18n/ko-KR/skill.json b/web/i18n/locales/ko-KR/skill.json similarity index 100% rename from web/i18n/ko-KR/skill.json rename to web/i18n/locales/ko-KR/skill.json diff --git a/web/i18n/ko-KR/snippet.json b/web/i18n/locales/ko-KR/snippet.json similarity index 100% rename from web/i18n/ko-KR/snippet.json rename to web/i18n/locales/ko-KR/snippet.json diff --git a/web/i18n/ko-KR/time.json b/web/i18n/locales/ko-KR/time.json similarity index 100% rename from web/i18n/ko-KR/time.json rename to web/i18n/locales/ko-KR/time.json diff --git a/web/i18n/ko-KR/tools.json b/web/i18n/locales/ko-KR/tools.json similarity index 100% rename from web/i18n/ko-KR/tools.json rename to web/i18n/locales/ko-KR/tools.json diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/locales/ko-KR/workflow.json similarity index 100% rename from web/i18n/ko-KR/workflow.json rename to web/i18n/locales/ko-KR/workflow.json diff --git a/web/i18n/lo-LA/agent-v-2.json b/web/i18n/locales/lo-LA/agent-v-2.json similarity index 100% rename from web/i18n/lo-LA/agent-v-2.json rename to web/i18n/locales/lo-LA/agent-v-2.json diff --git a/web/i18n/lo-LA/app-annotation.json b/web/i18n/locales/lo-LA/app-annotation.json similarity index 100% rename from web/i18n/lo-LA/app-annotation.json rename to web/i18n/locales/lo-LA/app-annotation.json diff --git a/web/i18n/lo-LA/app-api.json b/web/i18n/locales/lo-LA/app-api.json similarity index 100% rename from web/i18n/lo-LA/app-api.json rename to web/i18n/locales/lo-LA/app-api.json diff --git a/web/i18n/lo-LA/app-debug.json b/web/i18n/locales/lo-LA/app-debug.json similarity index 100% rename from web/i18n/lo-LA/app-debug.json rename to web/i18n/locales/lo-LA/app-debug.json diff --git a/web/i18n/lo-LA/app-log.json b/web/i18n/locales/lo-LA/app-log.json similarity index 100% rename from web/i18n/lo-LA/app-log.json rename to web/i18n/locales/lo-LA/app-log.json diff --git a/web/i18n/lo-LA/app-overview.json b/web/i18n/locales/lo-LA/app-overview.json similarity index 100% rename from web/i18n/lo-LA/app-overview.json rename to web/i18n/locales/lo-LA/app-overview.json diff --git a/web/i18n/lo-LA/app.json b/web/i18n/locales/lo-LA/app.json similarity index 100% rename from web/i18n/lo-LA/app.json rename to web/i18n/locales/lo-LA/app.json diff --git a/web/i18n/lo-LA/billing.json b/web/i18n/locales/lo-LA/billing.json similarity index 100% rename from web/i18n/lo-LA/billing.json rename to web/i18n/locales/lo-LA/billing.json diff --git a/web/i18n/lo-LA/common.json b/web/i18n/locales/lo-LA/common.json similarity index 100% rename from web/i18n/lo-LA/common.json rename to web/i18n/locales/lo-LA/common.json diff --git a/web/i18n/lo-LA/custom.json b/web/i18n/locales/lo-LA/custom.json similarity index 100% rename from web/i18n/lo-LA/custom.json rename to web/i18n/locales/lo-LA/custom.json diff --git a/web/i18n/lo-LA/dataset-creation.json b/web/i18n/locales/lo-LA/dataset-creation.json similarity index 100% rename from web/i18n/lo-LA/dataset-creation.json rename to web/i18n/locales/lo-LA/dataset-creation.json diff --git a/web/i18n/lo-LA/dataset-documents.json b/web/i18n/locales/lo-LA/dataset-documents.json similarity index 100% rename from web/i18n/lo-LA/dataset-documents.json rename to web/i18n/locales/lo-LA/dataset-documents.json diff --git a/web/i18n/lo-LA/dataset-hit-testing.json b/web/i18n/locales/lo-LA/dataset-hit-testing.json similarity index 100% rename from web/i18n/lo-LA/dataset-hit-testing.json rename to web/i18n/locales/lo-LA/dataset-hit-testing.json diff --git a/web/i18n/lo-LA/dataset-pipeline.json b/web/i18n/locales/lo-LA/dataset-pipeline.json similarity index 100% rename from web/i18n/lo-LA/dataset-pipeline.json rename to web/i18n/locales/lo-LA/dataset-pipeline.json diff --git a/web/i18n/lo-LA/dataset-settings.json b/web/i18n/locales/lo-LA/dataset-settings.json similarity index 100% rename from web/i18n/lo-LA/dataset-settings.json rename to web/i18n/locales/lo-LA/dataset-settings.json diff --git a/web/i18n/lo-LA/dataset.json b/web/i18n/locales/lo-LA/dataset.json similarity index 100% rename from web/i18n/lo-LA/dataset.json rename to web/i18n/locales/lo-LA/dataset.json diff --git a/web/i18n/lo-LA/deployments.json b/web/i18n/locales/lo-LA/deployments.json similarity index 100% rename from web/i18n/lo-LA/deployments.json rename to web/i18n/locales/lo-LA/deployments.json diff --git a/web/i18n/lo-LA/device-flow.json b/web/i18n/locales/lo-LA/device-flow.json similarity index 100% rename from web/i18n/lo-LA/device-flow.json rename to web/i18n/locales/lo-LA/device-flow.json diff --git a/web/i18n/lo-LA/education.json b/web/i18n/locales/lo-LA/education.json similarity index 100% rename from web/i18n/lo-LA/education.json rename to web/i18n/locales/lo-LA/education.json diff --git a/web/i18n/lo-LA/explore.json b/web/i18n/locales/lo-LA/explore.json similarity index 100% rename from web/i18n/lo-LA/explore.json rename to web/i18n/locales/lo-LA/explore.json diff --git a/web/i18n/lo-LA/layout.json b/web/i18n/locales/lo-LA/layout.json similarity index 100% rename from web/i18n/lo-LA/layout.json rename to web/i18n/locales/lo-LA/layout.json diff --git a/web/i18n/lo-LA/login.json b/web/i18n/locales/lo-LA/login.json similarity index 100% rename from web/i18n/lo-LA/login.json rename to web/i18n/locales/lo-LA/login.json diff --git a/web/i18n/lo-LA/oauth.json b/web/i18n/locales/lo-LA/oauth.json similarity index 100% rename from web/i18n/lo-LA/oauth.json rename to web/i18n/locales/lo-LA/oauth.json diff --git a/web/i18n/lo-LA/permission-keys.json b/web/i18n/locales/lo-LA/permission-keys.json similarity index 100% rename from web/i18n/lo-LA/permission-keys.json rename to web/i18n/locales/lo-LA/permission-keys.json diff --git a/web/i18n/lo-LA/permission.json b/web/i18n/locales/lo-LA/permission.json similarity index 100% rename from web/i18n/lo-LA/permission.json rename to web/i18n/locales/lo-LA/permission.json diff --git a/web/i18n/lo-LA/pipeline.json b/web/i18n/locales/lo-LA/pipeline.json similarity index 100% rename from web/i18n/lo-LA/pipeline.json rename to web/i18n/locales/lo-LA/pipeline.json diff --git a/web/i18n/lo-LA/plugin-tags.json b/web/i18n/locales/lo-LA/plugin-tags.json similarity index 100% rename from web/i18n/lo-LA/plugin-tags.json rename to web/i18n/locales/lo-LA/plugin-tags.json diff --git a/web/i18n/lo-LA/plugin-trigger.json b/web/i18n/locales/lo-LA/plugin-trigger.json similarity index 100% rename from web/i18n/lo-LA/plugin-trigger.json rename to web/i18n/locales/lo-LA/plugin-trigger.json diff --git a/web/i18n/lo-LA/plugin.json b/web/i18n/locales/lo-LA/plugin.json similarity index 100% rename from web/i18n/lo-LA/plugin.json rename to web/i18n/locales/lo-LA/plugin.json diff --git a/web/i18n/lo-LA/register.json b/web/i18n/locales/lo-LA/register.json similarity index 100% rename from web/i18n/lo-LA/register.json rename to web/i18n/locales/lo-LA/register.json diff --git a/web/i18n/lo-LA/run-log.json b/web/i18n/locales/lo-LA/run-log.json similarity index 100% rename from web/i18n/lo-LA/run-log.json rename to web/i18n/locales/lo-LA/run-log.json diff --git a/web/i18n/lo-LA/share.json b/web/i18n/locales/lo-LA/share.json similarity index 100% rename from web/i18n/lo-LA/share.json rename to web/i18n/locales/lo-LA/share.json diff --git a/web/i18n/lo-LA/skill.json b/web/i18n/locales/lo-LA/skill.json similarity index 100% rename from web/i18n/lo-LA/skill.json rename to web/i18n/locales/lo-LA/skill.json diff --git a/web/i18n/lo-LA/snippet.json b/web/i18n/locales/lo-LA/snippet.json similarity index 100% rename from web/i18n/lo-LA/snippet.json rename to web/i18n/locales/lo-LA/snippet.json diff --git a/web/i18n/lo-LA/time.json b/web/i18n/locales/lo-LA/time.json similarity index 100% rename from web/i18n/lo-LA/time.json rename to web/i18n/locales/lo-LA/time.json diff --git a/web/i18n/lo-LA/tools.json b/web/i18n/locales/lo-LA/tools.json similarity index 100% rename from web/i18n/lo-LA/tools.json rename to web/i18n/locales/lo-LA/tools.json diff --git a/web/i18n/lo-LA/workflow.json b/web/i18n/locales/lo-LA/workflow.json similarity index 100% rename from web/i18n/lo-LA/workflow.json rename to web/i18n/locales/lo-LA/workflow.json diff --git a/web/i18n/nl-NL/agent-v-2.json b/web/i18n/locales/nl-NL/agent-v-2.json similarity index 100% rename from web/i18n/nl-NL/agent-v-2.json rename to web/i18n/locales/nl-NL/agent-v-2.json diff --git a/web/i18n/nl-NL/app-annotation.json b/web/i18n/locales/nl-NL/app-annotation.json similarity index 100% rename from web/i18n/nl-NL/app-annotation.json rename to web/i18n/locales/nl-NL/app-annotation.json diff --git a/web/i18n/nl-NL/app-api.json b/web/i18n/locales/nl-NL/app-api.json similarity index 100% rename from web/i18n/nl-NL/app-api.json rename to web/i18n/locales/nl-NL/app-api.json diff --git a/web/i18n/nl-NL/app-debug.json b/web/i18n/locales/nl-NL/app-debug.json similarity index 100% rename from web/i18n/nl-NL/app-debug.json rename to web/i18n/locales/nl-NL/app-debug.json diff --git a/web/i18n/nl-NL/app-log.json b/web/i18n/locales/nl-NL/app-log.json similarity index 100% rename from web/i18n/nl-NL/app-log.json rename to web/i18n/locales/nl-NL/app-log.json diff --git a/web/i18n/nl-NL/app-overview.json b/web/i18n/locales/nl-NL/app-overview.json similarity index 100% rename from web/i18n/nl-NL/app-overview.json rename to web/i18n/locales/nl-NL/app-overview.json diff --git a/web/i18n/nl-NL/app.json b/web/i18n/locales/nl-NL/app.json similarity index 100% rename from web/i18n/nl-NL/app.json rename to web/i18n/locales/nl-NL/app.json diff --git a/web/i18n/nl-NL/billing.json b/web/i18n/locales/nl-NL/billing.json similarity index 100% rename from web/i18n/nl-NL/billing.json rename to web/i18n/locales/nl-NL/billing.json diff --git a/web/i18n/nl-NL/common.json b/web/i18n/locales/nl-NL/common.json similarity index 100% rename from web/i18n/nl-NL/common.json rename to web/i18n/locales/nl-NL/common.json diff --git a/web/i18n/nl-NL/custom.json b/web/i18n/locales/nl-NL/custom.json similarity index 100% rename from web/i18n/nl-NL/custom.json rename to web/i18n/locales/nl-NL/custom.json diff --git a/web/i18n/nl-NL/dataset-creation.json b/web/i18n/locales/nl-NL/dataset-creation.json similarity index 100% rename from web/i18n/nl-NL/dataset-creation.json rename to web/i18n/locales/nl-NL/dataset-creation.json diff --git a/web/i18n/nl-NL/dataset-documents.json b/web/i18n/locales/nl-NL/dataset-documents.json similarity index 100% rename from web/i18n/nl-NL/dataset-documents.json rename to web/i18n/locales/nl-NL/dataset-documents.json diff --git a/web/i18n/nl-NL/dataset-hit-testing.json b/web/i18n/locales/nl-NL/dataset-hit-testing.json similarity index 100% rename from web/i18n/nl-NL/dataset-hit-testing.json rename to web/i18n/locales/nl-NL/dataset-hit-testing.json diff --git a/web/i18n/nl-NL/dataset-pipeline.json b/web/i18n/locales/nl-NL/dataset-pipeline.json similarity index 100% rename from web/i18n/nl-NL/dataset-pipeline.json rename to web/i18n/locales/nl-NL/dataset-pipeline.json diff --git a/web/i18n/nl-NL/dataset-settings.json b/web/i18n/locales/nl-NL/dataset-settings.json similarity index 100% rename from web/i18n/nl-NL/dataset-settings.json rename to web/i18n/locales/nl-NL/dataset-settings.json diff --git a/web/i18n/nl-NL/dataset.json b/web/i18n/locales/nl-NL/dataset.json similarity index 100% rename from web/i18n/nl-NL/dataset.json rename to web/i18n/locales/nl-NL/dataset.json diff --git a/web/i18n/nl-NL/deployments.json b/web/i18n/locales/nl-NL/deployments.json similarity index 100% rename from web/i18n/nl-NL/deployments.json rename to web/i18n/locales/nl-NL/deployments.json diff --git a/web/i18n/nl-NL/device-flow.json b/web/i18n/locales/nl-NL/device-flow.json similarity index 100% rename from web/i18n/nl-NL/device-flow.json rename to web/i18n/locales/nl-NL/device-flow.json diff --git a/web/i18n/nl-NL/education.json b/web/i18n/locales/nl-NL/education.json similarity index 100% rename from web/i18n/nl-NL/education.json rename to web/i18n/locales/nl-NL/education.json diff --git a/web/i18n/nl-NL/explore.json b/web/i18n/locales/nl-NL/explore.json similarity index 100% rename from web/i18n/nl-NL/explore.json rename to web/i18n/locales/nl-NL/explore.json diff --git a/web/i18n/nl-NL/layout.json b/web/i18n/locales/nl-NL/layout.json similarity index 100% rename from web/i18n/nl-NL/layout.json rename to web/i18n/locales/nl-NL/layout.json diff --git a/web/i18n/nl-NL/login.json b/web/i18n/locales/nl-NL/login.json similarity index 100% rename from web/i18n/nl-NL/login.json rename to web/i18n/locales/nl-NL/login.json diff --git a/web/i18n/nl-NL/oauth.json b/web/i18n/locales/nl-NL/oauth.json similarity index 100% rename from web/i18n/nl-NL/oauth.json rename to web/i18n/locales/nl-NL/oauth.json diff --git a/web/i18n/nl-NL/permission-keys.json b/web/i18n/locales/nl-NL/permission-keys.json similarity index 100% rename from web/i18n/nl-NL/permission-keys.json rename to web/i18n/locales/nl-NL/permission-keys.json diff --git a/web/i18n/nl-NL/permission.json b/web/i18n/locales/nl-NL/permission.json similarity index 100% rename from web/i18n/nl-NL/permission.json rename to web/i18n/locales/nl-NL/permission.json diff --git a/web/i18n/nl-NL/pipeline.json b/web/i18n/locales/nl-NL/pipeline.json similarity index 100% rename from web/i18n/nl-NL/pipeline.json rename to web/i18n/locales/nl-NL/pipeline.json diff --git a/web/i18n/nl-NL/plugin-tags.json b/web/i18n/locales/nl-NL/plugin-tags.json similarity index 100% rename from web/i18n/nl-NL/plugin-tags.json rename to web/i18n/locales/nl-NL/plugin-tags.json diff --git a/web/i18n/nl-NL/plugin-trigger.json b/web/i18n/locales/nl-NL/plugin-trigger.json similarity index 100% rename from web/i18n/nl-NL/plugin-trigger.json rename to web/i18n/locales/nl-NL/plugin-trigger.json diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/locales/nl-NL/plugin.json similarity index 100% rename from web/i18n/nl-NL/plugin.json rename to web/i18n/locales/nl-NL/plugin.json diff --git a/web/i18n/nl-NL/register.json b/web/i18n/locales/nl-NL/register.json similarity index 100% rename from web/i18n/nl-NL/register.json rename to web/i18n/locales/nl-NL/register.json diff --git a/web/i18n/nl-NL/run-log.json b/web/i18n/locales/nl-NL/run-log.json similarity index 100% rename from web/i18n/nl-NL/run-log.json rename to web/i18n/locales/nl-NL/run-log.json diff --git a/web/i18n/nl-NL/share.json b/web/i18n/locales/nl-NL/share.json similarity index 100% rename from web/i18n/nl-NL/share.json rename to web/i18n/locales/nl-NL/share.json diff --git a/web/i18n/nl-NL/skill.json b/web/i18n/locales/nl-NL/skill.json similarity index 100% rename from web/i18n/nl-NL/skill.json rename to web/i18n/locales/nl-NL/skill.json diff --git a/web/i18n/nl-NL/snippet.json b/web/i18n/locales/nl-NL/snippet.json similarity index 100% rename from web/i18n/nl-NL/snippet.json rename to web/i18n/locales/nl-NL/snippet.json diff --git a/web/i18n/nl-NL/time.json b/web/i18n/locales/nl-NL/time.json similarity index 100% rename from web/i18n/nl-NL/time.json rename to web/i18n/locales/nl-NL/time.json diff --git a/web/i18n/nl-NL/tools.json b/web/i18n/locales/nl-NL/tools.json similarity index 100% rename from web/i18n/nl-NL/tools.json rename to web/i18n/locales/nl-NL/tools.json diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/locales/nl-NL/workflow.json similarity index 100% rename from web/i18n/nl-NL/workflow.json rename to web/i18n/locales/nl-NL/workflow.json diff --git a/web/i18n/pl-PL/agent-v-2.json b/web/i18n/locales/pl-PL/agent-v-2.json similarity index 100% rename from web/i18n/pl-PL/agent-v-2.json rename to web/i18n/locales/pl-PL/agent-v-2.json diff --git a/web/i18n/pl-PL/app-annotation.json b/web/i18n/locales/pl-PL/app-annotation.json similarity index 100% rename from web/i18n/pl-PL/app-annotation.json rename to web/i18n/locales/pl-PL/app-annotation.json diff --git a/web/i18n/pl-PL/app-api.json b/web/i18n/locales/pl-PL/app-api.json similarity index 100% rename from web/i18n/pl-PL/app-api.json rename to web/i18n/locales/pl-PL/app-api.json diff --git a/web/i18n/pl-PL/app-debug.json b/web/i18n/locales/pl-PL/app-debug.json similarity index 100% rename from web/i18n/pl-PL/app-debug.json rename to web/i18n/locales/pl-PL/app-debug.json diff --git a/web/i18n/pl-PL/app-log.json b/web/i18n/locales/pl-PL/app-log.json similarity index 100% rename from web/i18n/pl-PL/app-log.json rename to web/i18n/locales/pl-PL/app-log.json diff --git a/web/i18n/pl-PL/app-overview.json b/web/i18n/locales/pl-PL/app-overview.json similarity index 100% rename from web/i18n/pl-PL/app-overview.json rename to web/i18n/locales/pl-PL/app-overview.json diff --git a/web/i18n/pl-PL/app.json b/web/i18n/locales/pl-PL/app.json similarity index 100% rename from web/i18n/pl-PL/app.json rename to web/i18n/locales/pl-PL/app.json diff --git a/web/i18n/pl-PL/billing.json b/web/i18n/locales/pl-PL/billing.json similarity index 100% rename from web/i18n/pl-PL/billing.json rename to web/i18n/locales/pl-PL/billing.json diff --git a/web/i18n/pl-PL/common.json b/web/i18n/locales/pl-PL/common.json similarity index 100% rename from web/i18n/pl-PL/common.json rename to web/i18n/locales/pl-PL/common.json diff --git a/web/i18n/pl-PL/custom.json b/web/i18n/locales/pl-PL/custom.json similarity index 100% rename from web/i18n/pl-PL/custom.json rename to web/i18n/locales/pl-PL/custom.json diff --git a/web/i18n/pl-PL/dataset-creation.json b/web/i18n/locales/pl-PL/dataset-creation.json similarity index 100% rename from web/i18n/pl-PL/dataset-creation.json rename to web/i18n/locales/pl-PL/dataset-creation.json diff --git a/web/i18n/pl-PL/dataset-documents.json b/web/i18n/locales/pl-PL/dataset-documents.json similarity index 100% rename from web/i18n/pl-PL/dataset-documents.json rename to web/i18n/locales/pl-PL/dataset-documents.json diff --git a/web/i18n/pl-PL/dataset-hit-testing.json b/web/i18n/locales/pl-PL/dataset-hit-testing.json similarity index 100% rename from web/i18n/pl-PL/dataset-hit-testing.json rename to web/i18n/locales/pl-PL/dataset-hit-testing.json diff --git a/web/i18n/pl-PL/dataset-pipeline.json b/web/i18n/locales/pl-PL/dataset-pipeline.json similarity index 100% rename from web/i18n/pl-PL/dataset-pipeline.json rename to web/i18n/locales/pl-PL/dataset-pipeline.json diff --git a/web/i18n/pl-PL/dataset-settings.json b/web/i18n/locales/pl-PL/dataset-settings.json similarity index 100% rename from web/i18n/pl-PL/dataset-settings.json rename to web/i18n/locales/pl-PL/dataset-settings.json diff --git a/web/i18n/pl-PL/dataset.json b/web/i18n/locales/pl-PL/dataset.json similarity index 100% rename from web/i18n/pl-PL/dataset.json rename to web/i18n/locales/pl-PL/dataset.json diff --git a/web/i18n/pl-PL/deployments.json b/web/i18n/locales/pl-PL/deployments.json similarity index 100% rename from web/i18n/pl-PL/deployments.json rename to web/i18n/locales/pl-PL/deployments.json diff --git a/web/i18n/pl-PL/device-flow.json b/web/i18n/locales/pl-PL/device-flow.json similarity index 100% rename from web/i18n/pl-PL/device-flow.json rename to web/i18n/locales/pl-PL/device-flow.json diff --git a/web/i18n/pl-PL/education.json b/web/i18n/locales/pl-PL/education.json similarity index 100% rename from web/i18n/pl-PL/education.json rename to web/i18n/locales/pl-PL/education.json diff --git a/web/i18n/pl-PL/explore.json b/web/i18n/locales/pl-PL/explore.json similarity index 100% rename from web/i18n/pl-PL/explore.json rename to web/i18n/locales/pl-PL/explore.json diff --git a/web/i18n/pl-PL/layout.json b/web/i18n/locales/pl-PL/layout.json similarity index 100% rename from web/i18n/pl-PL/layout.json rename to web/i18n/locales/pl-PL/layout.json diff --git a/web/i18n/pl-PL/login.json b/web/i18n/locales/pl-PL/login.json similarity index 100% rename from web/i18n/pl-PL/login.json rename to web/i18n/locales/pl-PL/login.json diff --git a/web/i18n/pl-PL/oauth.json b/web/i18n/locales/pl-PL/oauth.json similarity index 100% rename from web/i18n/pl-PL/oauth.json rename to web/i18n/locales/pl-PL/oauth.json diff --git a/web/i18n/pl-PL/permission-keys.json b/web/i18n/locales/pl-PL/permission-keys.json similarity index 100% rename from web/i18n/pl-PL/permission-keys.json rename to web/i18n/locales/pl-PL/permission-keys.json diff --git a/web/i18n/pl-PL/permission.json b/web/i18n/locales/pl-PL/permission.json similarity index 100% rename from web/i18n/pl-PL/permission.json rename to web/i18n/locales/pl-PL/permission.json diff --git a/web/i18n/pl-PL/pipeline.json b/web/i18n/locales/pl-PL/pipeline.json similarity index 100% rename from web/i18n/pl-PL/pipeline.json rename to web/i18n/locales/pl-PL/pipeline.json diff --git a/web/i18n/pl-PL/plugin-tags.json b/web/i18n/locales/pl-PL/plugin-tags.json similarity index 100% rename from web/i18n/pl-PL/plugin-tags.json rename to web/i18n/locales/pl-PL/plugin-tags.json diff --git a/web/i18n/pl-PL/plugin-trigger.json b/web/i18n/locales/pl-PL/plugin-trigger.json similarity index 100% rename from web/i18n/pl-PL/plugin-trigger.json rename to web/i18n/locales/pl-PL/plugin-trigger.json diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/locales/pl-PL/plugin.json similarity index 100% rename from web/i18n/pl-PL/plugin.json rename to web/i18n/locales/pl-PL/plugin.json diff --git a/web/i18n/pl-PL/register.json b/web/i18n/locales/pl-PL/register.json similarity index 100% rename from web/i18n/pl-PL/register.json rename to web/i18n/locales/pl-PL/register.json diff --git a/web/i18n/pl-PL/run-log.json b/web/i18n/locales/pl-PL/run-log.json similarity index 100% rename from web/i18n/pl-PL/run-log.json rename to web/i18n/locales/pl-PL/run-log.json diff --git a/web/i18n/pl-PL/share.json b/web/i18n/locales/pl-PL/share.json similarity index 100% rename from web/i18n/pl-PL/share.json rename to web/i18n/locales/pl-PL/share.json diff --git a/web/i18n/pl-PL/skill.json b/web/i18n/locales/pl-PL/skill.json similarity index 100% rename from web/i18n/pl-PL/skill.json rename to web/i18n/locales/pl-PL/skill.json diff --git a/web/i18n/pl-PL/snippet.json b/web/i18n/locales/pl-PL/snippet.json similarity index 100% rename from web/i18n/pl-PL/snippet.json rename to web/i18n/locales/pl-PL/snippet.json diff --git a/web/i18n/pl-PL/time.json b/web/i18n/locales/pl-PL/time.json similarity index 100% rename from web/i18n/pl-PL/time.json rename to web/i18n/locales/pl-PL/time.json diff --git a/web/i18n/pl-PL/tools.json b/web/i18n/locales/pl-PL/tools.json similarity index 100% rename from web/i18n/pl-PL/tools.json rename to web/i18n/locales/pl-PL/tools.json diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/locales/pl-PL/workflow.json similarity index 100% rename from web/i18n/pl-PL/workflow.json rename to web/i18n/locales/pl-PL/workflow.json diff --git a/web/i18n/pt-BR/agent-v-2.json b/web/i18n/locales/pt-BR/agent-v-2.json similarity index 100% rename from web/i18n/pt-BR/agent-v-2.json rename to web/i18n/locales/pt-BR/agent-v-2.json diff --git a/web/i18n/pt-BR/app-annotation.json b/web/i18n/locales/pt-BR/app-annotation.json similarity index 100% rename from web/i18n/pt-BR/app-annotation.json rename to web/i18n/locales/pt-BR/app-annotation.json diff --git a/web/i18n/pt-BR/app-api.json b/web/i18n/locales/pt-BR/app-api.json similarity index 100% rename from web/i18n/pt-BR/app-api.json rename to web/i18n/locales/pt-BR/app-api.json diff --git a/web/i18n/pt-BR/app-debug.json b/web/i18n/locales/pt-BR/app-debug.json similarity index 100% rename from web/i18n/pt-BR/app-debug.json rename to web/i18n/locales/pt-BR/app-debug.json diff --git a/web/i18n/pt-BR/app-log.json b/web/i18n/locales/pt-BR/app-log.json similarity index 100% rename from web/i18n/pt-BR/app-log.json rename to web/i18n/locales/pt-BR/app-log.json diff --git a/web/i18n/pt-BR/app-overview.json b/web/i18n/locales/pt-BR/app-overview.json similarity index 100% rename from web/i18n/pt-BR/app-overview.json rename to web/i18n/locales/pt-BR/app-overview.json diff --git a/web/i18n/pt-BR/app.json b/web/i18n/locales/pt-BR/app.json similarity index 100% rename from web/i18n/pt-BR/app.json rename to web/i18n/locales/pt-BR/app.json diff --git a/web/i18n/pt-BR/billing.json b/web/i18n/locales/pt-BR/billing.json similarity index 100% rename from web/i18n/pt-BR/billing.json rename to web/i18n/locales/pt-BR/billing.json diff --git a/web/i18n/pt-BR/common.json b/web/i18n/locales/pt-BR/common.json similarity index 100% rename from web/i18n/pt-BR/common.json rename to web/i18n/locales/pt-BR/common.json diff --git a/web/i18n/pt-BR/custom.json b/web/i18n/locales/pt-BR/custom.json similarity index 100% rename from web/i18n/pt-BR/custom.json rename to web/i18n/locales/pt-BR/custom.json diff --git a/web/i18n/pt-BR/dataset-creation.json b/web/i18n/locales/pt-BR/dataset-creation.json similarity index 100% rename from web/i18n/pt-BR/dataset-creation.json rename to web/i18n/locales/pt-BR/dataset-creation.json diff --git a/web/i18n/pt-BR/dataset-documents.json b/web/i18n/locales/pt-BR/dataset-documents.json similarity index 100% rename from web/i18n/pt-BR/dataset-documents.json rename to web/i18n/locales/pt-BR/dataset-documents.json diff --git a/web/i18n/pt-BR/dataset-hit-testing.json b/web/i18n/locales/pt-BR/dataset-hit-testing.json similarity index 100% rename from web/i18n/pt-BR/dataset-hit-testing.json rename to web/i18n/locales/pt-BR/dataset-hit-testing.json diff --git a/web/i18n/pt-BR/dataset-pipeline.json b/web/i18n/locales/pt-BR/dataset-pipeline.json similarity index 100% rename from web/i18n/pt-BR/dataset-pipeline.json rename to web/i18n/locales/pt-BR/dataset-pipeline.json diff --git a/web/i18n/pt-BR/dataset-settings.json b/web/i18n/locales/pt-BR/dataset-settings.json similarity index 100% rename from web/i18n/pt-BR/dataset-settings.json rename to web/i18n/locales/pt-BR/dataset-settings.json diff --git a/web/i18n/pt-BR/dataset.json b/web/i18n/locales/pt-BR/dataset.json similarity index 100% rename from web/i18n/pt-BR/dataset.json rename to web/i18n/locales/pt-BR/dataset.json diff --git a/web/i18n/pt-BR/deployments.json b/web/i18n/locales/pt-BR/deployments.json similarity index 100% rename from web/i18n/pt-BR/deployments.json rename to web/i18n/locales/pt-BR/deployments.json diff --git a/web/i18n/pt-BR/device-flow.json b/web/i18n/locales/pt-BR/device-flow.json similarity index 100% rename from web/i18n/pt-BR/device-flow.json rename to web/i18n/locales/pt-BR/device-flow.json diff --git a/web/i18n/pt-BR/education.json b/web/i18n/locales/pt-BR/education.json similarity index 100% rename from web/i18n/pt-BR/education.json rename to web/i18n/locales/pt-BR/education.json diff --git a/web/i18n/pt-BR/explore.json b/web/i18n/locales/pt-BR/explore.json similarity index 100% rename from web/i18n/pt-BR/explore.json rename to web/i18n/locales/pt-BR/explore.json diff --git a/web/i18n/pt-BR/layout.json b/web/i18n/locales/pt-BR/layout.json similarity index 100% rename from web/i18n/pt-BR/layout.json rename to web/i18n/locales/pt-BR/layout.json diff --git a/web/i18n/pt-BR/login.json b/web/i18n/locales/pt-BR/login.json similarity index 100% rename from web/i18n/pt-BR/login.json rename to web/i18n/locales/pt-BR/login.json diff --git a/web/i18n/pt-BR/oauth.json b/web/i18n/locales/pt-BR/oauth.json similarity index 100% rename from web/i18n/pt-BR/oauth.json rename to web/i18n/locales/pt-BR/oauth.json diff --git a/web/i18n/pt-BR/permission-keys.json b/web/i18n/locales/pt-BR/permission-keys.json similarity index 100% rename from web/i18n/pt-BR/permission-keys.json rename to web/i18n/locales/pt-BR/permission-keys.json diff --git a/web/i18n/pt-BR/permission.json b/web/i18n/locales/pt-BR/permission.json similarity index 100% rename from web/i18n/pt-BR/permission.json rename to web/i18n/locales/pt-BR/permission.json diff --git a/web/i18n/pt-BR/pipeline.json b/web/i18n/locales/pt-BR/pipeline.json similarity index 100% rename from web/i18n/pt-BR/pipeline.json rename to web/i18n/locales/pt-BR/pipeline.json diff --git a/web/i18n/pt-BR/plugin-tags.json b/web/i18n/locales/pt-BR/plugin-tags.json similarity index 100% rename from web/i18n/pt-BR/plugin-tags.json rename to web/i18n/locales/pt-BR/plugin-tags.json diff --git a/web/i18n/pt-BR/plugin-trigger.json b/web/i18n/locales/pt-BR/plugin-trigger.json similarity index 100% rename from web/i18n/pt-BR/plugin-trigger.json rename to web/i18n/locales/pt-BR/plugin-trigger.json diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/locales/pt-BR/plugin.json similarity index 100% rename from web/i18n/pt-BR/plugin.json rename to web/i18n/locales/pt-BR/plugin.json diff --git a/web/i18n/pt-BR/register.json b/web/i18n/locales/pt-BR/register.json similarity index 100% rename from web/i18n/pt-BR/register.json rename to web/i18n/locales/pt-BR/register.json diff --git a/web/i18n/pt-BR/run-log.json b/web/i18n/locales/pt-BR/run-log.json similarity index 100% rename from web/i18n/pt-BR/run-log.json rename to web/i18n/locales/pt-BR/run-log.json diff --git a/web/i18n/pt-BR/share.json b/web/i18n/locales/pt-BR/share.json similarity index 100% rename from web/i18n/pt-BR/share.json rename to web/i18n/locales/pt-BR/share.json diff --git a/web/i18n/pt-BR/skill.json b/web/i18n/locales/pt-BR/skill.json similarity index 100% rename from web/i18n/pt-BR/skill.json rename to web/i18n/locales/pt-BR/skill.json diff --git a/web/i18n/pt-BR/snippet.json b/web/i18n/locales/pt-BR/snippet.json similarity index 100% rename from web/i18n/pt-BR/snippet.json rename to web/i18n/locales/pt-BR/snippet.json diff --git a/web/i18n/pt-BR/time.json b/web/i18n/locales/pt-BR/time.json similarity index 100% rename from web/i18n/pt-BR/time.json rename to web/i18n/locales/pt-BR/time.json diff --git a/web/i18n/pt-BR/tools.json b/web/i18n/locales/pt-BR/tools.json similarity index 100% rename from web/i18n/pt-BR/tools.json rename to web/i18n/locales/pt-BR/tools.json diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/locales/pt-BR/workflow.json similarity index 100% rename from web/i18n/pt-BR/workflow.json rename to web/i18n/locales/pt-BR/workflow.json diff --git a/web/i18n/ro-RO/agent-v-2.json b/web/i18n/locales/ro-RO/agent-v-2.json similarity index 100% rename from web/i18n/ro-RO/agent-v-2.json rename to web/i18n/locales/ro-RO/agent-v-2.json diff --git a/web/i18n/ro-RO/app-annotation.json b/web/i18n/locales/ro-RO/app-annotation.json similarity index 100% rename from web/i18n/ro-RO/app-annotation.json rename to web/i18n/locales/ro-RO/app-annotation.json diff --git a/web/i18n/ro-RO/app-api.json b/web/i18n/locales/ro-RO/app-api.json similarity index 100% rename from web/i18n/ro-RO/app-api.json rename to web/i18n/locales/ro-RO/app-api.json diff --git a/web/i18n/ro-RO/app-debug.json b/web/i18n/locales/ro-RO/app-debug.json similarity index 100% rename from web/i18n/ro-RO/app-debug.json rename to web/i18n/locales/ro-RO/app-debug.json diff --git a/web/i18n/ro-RO/app-log.json b/web/i18n/locales/ro-RO/app-log.json similarity index 100% rename from web/i18n/ro-RO/app-log.json rename to web/i18n/locales/ro-RO/app-log.json diff --git a/web/i18n/ro-RO/app-overview.json b/web/i18n/locales/ro-RO/app-overview.json similarity index 100% rename from web/i18n/ro-RO/app-overview.json rename to web/i18n/locales/ro-RO/app-overview.json diff --git a/web/i18n/ro-RO/app.json b/web/i18n/locales/ro-RO/app.json similarity index 100% rename from web/i18n/ro-RO/app.json rename to web/i18n/locales/ro-RO/app.json diff --git a/web/i18n/ro-RO/billing.json b/web/i18n/locales/ro-RO/billing.json similarity index 100% rename from web/i18n/ro-RO/billing.json rename to web/i18n/locales/ro-RO/billing.json diff --git a/web/i18n/ro-RO/common.json b/web/i18n/locales/ro-RO/common.json similarity index 100% rename from web/i18n/ro-RO/common.json rename to web/i18n/locales/ro-RO/common.json diff --git a/web/i18n/ro-RO/custom.json b/web/i18n/locales/ro-RO/custom.json similarity index 100% rename from web/i18n/ro-RO/custom.json rename to web/i18n/locales/ro-RO/custom.json diff --git a/web/i18n/ro-RO/dataset-creation.json b/web/i18n/locales/ro-RO/dataset-creation.json similarity index 100% rename from web/i18n/ro-RO/dataset-creation.json rename to web/i18n/locales/ro-RO/dataset-creation.json diff --git a/web/i18n/ro-RO/dataset-documents.json b/web/i18n/locales/ro-RO/dataset-documents.json similarity index 100% rename from web/i18n/ro-RO/dataset-documents.json rename to web/i18n/locales/ro-RO/dataset-documents.json diff --git a/web/i18n/ro-RO/dataset-hit-testing.json b/web/i18n/locales/ro-RO/dataset-hit-testing.json similarity index 100% rename from web/i18n/ro-RO/dataset-hit-testing.json rename to web/i18n/locales/ro-RO/dataset-hit-testing.json diff --git a/web/i18n/ro-RO/dataset-pipeline.json b/web/i18n/locales/ro-RO/dataset-pipeline.json similarity index 100% rename from web/i18n/ro-RO/dataset-pipeline.json rename to web/i18n/locales/ro-RO/dataset-pipeline.json diff --git a/web/i18n/ro-RO/dataset-settings.json b/web/i18n/locales/ro-RO/dataset-settings.json similarity index 100% rename from web/i18n/ro-RO/dataset-settings.json rename to web/i18n/locales/ro-RO/dataset-settings.json diff --git a/web/i18n/ro-RO/dataset.json b/web/i18n/locales/ro-RO/dataset.json similarity index 100% rename from web/i18n/ro-RO/dataset.json rename to web/i18n/locales/ro-RO/dataset.json diff --git a/web/i18n/ro-RO/deployments.json b/web/i18n/locales/ro-RO/deployments.json similarity index 100% rename from web/i18n/ro-RO/deployments.json rename to web/i18n/locales/ro-RO/deployments.json diff --git a/web/i18n/ro-RO/device-flow.json b/web/i18n/locales/ro-RO/device-flow.json similarity index 100% rename from web/i18n/ro-RO/device-flow.json rename to web/i18n/locales/ro-RO/device-flow.json diff --git a/web/i18n/ro-RO/education.json b/web/i18n/locales/ro-RO/education.json similarity index 100% rename from web/i18n/ro-RO/education.json rename to web/i18n/locales/ro-RO/education.json diff --git a/web/i18n/ro-RO/explore.json b/web/i18n/locales/ro-RO/explore.json similarity index 100% rename from web/i18n/ro-RO/explore.json rename to web/i18n/locales/ro-RO/explore.json diff --git a/web/i18n/ro-RO/layout.json b/web/i18n/locales/ro-RO/layout.json similarity index 100% rename from web/i18n/ro-RO/layout.json rename to web/i18n/locales/ro-RO/layout.json diff --git a/web/i18n/ro-RO/login.json b/web/i18n/locales/ro-RO/login.json similarity index 100% rename from web/i18n/ro-RO/login.json rename to web/i18n/locales/ro-RO/login.json diff --git a/web/i18n/ro-RO/oauth.json b/web/i18n/locales/ro-RO/oauth.json similarity index 100% rename from web/i18n/ro-RO/oauth.json rename to web/i18n/locales/ro-RO/oauth.json diff --git a/web/i18n/ro-RO/permission-keys.json b/web/i18n/locales/ro-RO/permission-keys.json similarity index 100% rename from web/i18n/ro-RO/permission-keys.json rename to web/i18n/locales/ro-RO/permission-keys.json diff --git a/web/i18n/ro-RO/permission.json b/web/i18n/locales/ro-RO/permission.json similarity index 100% rename from web/i18n/ro-RO/permission.json rename to web/i18n/locales/ro-RO/permission.json diff --git a/web/i18n/ro-RO/pipeline.json b/web/i18n/locales/ro-RO/pipeline.json similarity index 100% rename from web/i18n/ro-RO/pipeline.json rename to web/i18n/locales/ro-RO/pipeline.json diff --git a/web/i18n/ro-RO/plugin-tags.json b/web/i18n/locales/ro-RO/plugin-tags.json similarity index 100% rename from web/i18n/ro-RO/plugin-tags.json rename to web/i18n/locales/ro-RO/plugin-tags.json diff --git a/web/i18n/ro-RO/plugin-trigger.json b/web/i18n/locales/ro-RO/plugin-trigger.json similarity index 100% rename from web/i18n/ro-RO/plugin-trigger.json rename to web/i18n/locales/ro-RO/plugin-trigger.json diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/locales/ro-RO/plugin.json similarity index 100% rename from web/i18n/ro-RO/plugin.json rename to web/i18n/locales/ro-RO/plugin.json diff --git a/web/i18n/ro-RO/register.json b/web/i18n/locales/ro-RO/register.json similarity index 100% rename from web/i18n/ro-RO/register.json rename to web/i18n/locales/ro-RO/register.json diff --git a/web/i18n/ro-RO/run-log.json b/web/i18n/locales/ro-RO/run-log.json similarity index 100% rename from web/i18n/ro-RO/run-log.json rename to web/i18n/locales/ro-RO/run-log.json diff --git a/web/i18n/ro-RO/share.json b/web/i18n/locales/ro-RO/share.json similarity index 100% rename from web/i18n/ro-RO/share.json rename to web/i18n/locales/ro-RO/share.json diff --git a/web/i18n/ro-RO/skill.json b/web/i18n/locales/ro-RO/skill.json similarity index 100% rename from web/i18n/ro-RO/skill.json rename to web/i18n/locales/ro-RO/skill.json diff --git a/web/i18n/ro-RO/snippet.json b/web/i18n/locales/ro-RO/snippet.json similarity index 100% rename from web/i18n/ro-RO/snippet.json rename to web/i18n/locales/ro-RO/snippet.json diff --git a/web/i18n/ro-RO/time.json b/web/i18n/locales/ro-RO/time.json similarity index 100% rename from web/i18n/ro-RO/time.json rename to web/i18n/locales/ro-RO/time.json diff --git a/web/i18n/ro-RO/tools.json b/web/i18n/locales/ro-RO/tools.json similarity index 100% rename from web/i18n/ro-RO/tools.json rename to web/i18n/locales/ro-RO/tools.json diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/locales/ro-RO/workflow.json similarity index 100% rename from web/i18n/ro-RO/workflow.json rename to web/i18n/locales/ro-RO/workflow.json diff --git a/web/i18n/ru-RU/agent-v-2.json b/web/i18n/locales/ru-RU/agent-v-2.json similarity index 100% rename from web/i18n/ru-RU/agent-v-2.json rename to web/i18n/locales/ru-RU/agent-v-2.json diff --git a/web/i18n/ru-RU/app-annotation.json b/web/i18n/locales/ru-RU/app-annotation.json similarity index 100% rename from web/i18n/ru-RU/app-annotation.json rename to web/i18n/locales/ru-RU/app-annotation.json diff --git a/web/i18n/ru-RU/app-api.json b/web/i18n/locales/ru-RU/app-api.json similarity index 100% rename from web/i18n/ru-RU/app-api.json rename to web/i18n/locales/ru-RU/app-api.json diff --git a/web/i18n/ru-RU/app-debug.json b/web/i18n/locales/ru-RU/app-debug.json similarity index 100% rename from web/i18n/ru-RU/app-debug.json rename to web/i18n/locales/ru-RU/app-debug.json diff --git a/web/i18n/ru-RU/app-log.json b/web/i18n/locales/ru-RU/app-log.json similarity index 100% rename from web/i18n/ru-RU/app-log.json rename to web/i18n/locales/ru-RU/app-log.json diff --git a/web/i18n/ru-RU/app-overview.json b/web/i18n/locales/ru-RU/app-overview.json similarity index 100% rename from web/i18n/ru-RU/app-overview.json rename to web/i18n/locales/ru-RU/app-overview.json diff --git a/web/i18n/ru-RU/app.json b/web/i18n/locales/ru-RU/app.json similarity index 100% rename from web/i18n/ru-RU/app.json rename to web/i18n/locales/ru-RU/app.json diff --git a/web/i18n/ru-RU/billing.json b/web/i18n/locales/ru-RU/billing.json similarity index 100% rename from web/i18n/ru-RU/billing.json rename to web/i18n/locales/ru-RU/billing.json diff --git a/web/i18n/ru-RU/common.json b/web/i18n/locales/ru-RU/common.json similarity index 100% rename from web/i18n/ru-RU/common.json rename to web/i18n/locales/ru-RU/common.json diff --git a/web/i18n/ru-RU/custom.json b/web/i18n/locales/ru-RU/custom.json similarity index 100% rename from web/i18n/ru-RU/custom.json rename to web/i18n/locales/ru-RU/custom.json diff --git a/web/i18n/ru-RU/dataset-creation.json b/web/i18n/locales/ru-RU/dataset-creation.json similarity index 100% rename from web/i18n/ru-RU/dataset-creation.json rename to web/i18n/locales/ru-RU/dataset-creation.json diff --git a/web/i18n/ru-RU/dataset-documents.json b/web/i18n/locales/ru-RU/dataset-documents.json similarity index 100% rename from web/i18n/ru-RU/dataset-documents.json rename to web/i18n/locales/ru-RU/dataset-documents.json diff --git a/web/i18n/ru-RU/dataset-hit-testing.json b/web/i18n/locales/ru-RU/dataset-hit-testing.json similarity index 100% rename from web/i18n/ru-RU/dataset-hit-testing.json rename to web/i18n/locales/ru-RU/dataset-hit-testing.json diff --git a/web/i18n/ru-RU/dataset-pipeline.json b/web/i18n/locales/ru-RU/dataset-pipeline.json similarity index 100% rename from web/i18n/ru-RU/dataset-pipeline.json rename to web/i18n/locales/ru-RU/dataset-pipeline.json diff --git a/web/i18n/ru-RU/dataset-settings.json b/web/i18n/locales/ru-RU/dataset-settings.json similarity index 100% rename from web/i18n/ru-RU/dataset-settings.json rename to web/i18n/locales/ru-RU/dataset-settings.json diff --git a/web/i18n/ru-RU/dataset.json b/web/i18n/locales/ru-RU/dataset.json similarity index 100% rename from web/i18n/ru-RU/dataset.json rename to web/i18n/locales/ru-RU/dataset.json diff --git a/web/i18n/ru-RU/deployments.json b/web/i18n/locales/ru-RU/deployments.json similarity index 100% rename from web/i18n/ru-RU/deployments.json rename to web/i18n/locales/ru-RU/deployments.json diff --git a/web/i18n/ru-RU/device-flow.json b/web/i18n/locales/ru-RU/device-flow.json similarity index 100% rename from web/i18n/ru-RU/device-flow.json rename to web/i18n/locales/ru-RU/device-flow.json diff --git a/web/i18n/ru-RU/education.json b/web/i18n/locales/ru-RU/education.json similarity index 100% rename from web/i18n/ru-RU/education.json rename to web/i18n/locales/ru-RU/education.json diff --git a/web/i18n/ru-RU/explore.json b/web/i18n/locales/ru-RU/explore.json similarity index 100% rename from web/i18n/ru-RU/explore.json rename to web/i18n/locales/ru-RU/explore.json diff --git a/web/i18n/ru-RU/layout.json b/web/i18n/locales/ru-RU/layout.json similarity index 100% rename from web/i18n/ru-RU/layout.json rename to web/i18n/locales/ru-RU/layout.json diff --git a/web/i18n/ru-RU/login.json b/web/i18n/locales/ru-RU/login.json similarity index 100% rename from web/i18n/ru-RU/login.json rename to web/i18n/locales/ru-RU/login.json diff --git a/web/i18n/ru-RU/oauth.json b/web/i18n/locales/ru-RU/oauth.json similarity index 100% rename from web/i18n/ru-RU/oauth.json rename to web/i18n/locales/ru-RU/oauth.json diff --git a/web/i18n/ru-RU/permission-keys.json b/web/i18n/locales/ru-RU/permission-keys.json similarity index 100% rename from web/i18n/ru-RU/permission-keys.json rename to web/i18n/locales/ru-RU/permission-keys.json diff --git a/web/i18n/ru-RU/permission.json b/web/i18n/locales/ru-RU/permission.json similarity index 100% rename from web/i18n/ru-RU/permission.json rename to web/i18n/locales/ru-RU/permission.json diff --git a/web/i18n/ru-RU/pipeline.json b/web/i18n/locales/ru-RU/pipeline.json similarity index 100% rename from web/i18n/ru-RU/pipeline.json rename to web/i18n/locales/ru-RU/pipeline.json diff --git a/web/i18n/ru-RU/plugin-tags.json b/web/i18n/locales/ru-RU/plugin-tags.json similarity index 100% rename from web/i18n/ru-RU/plugin-tags.json rename to web/i18n/locales/ru-RU/plugin-tags.json diff --git a/web/i18n/ru-RU/plugin-trigger.json b/web/i18n/locales/ru-RU/plugin-trigger.json similarity index 100% rename from web/i18n/ru-RU/plugin-trigger.json rename to web/i18n/locales/ru-RU/plugin-trigger.json diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/locales/ru-RU/plugin.json similarity index 100% rename from web/i18n/ru-RU/plugin.json rename to web/i18n/locales/ru-RU/plugin.json diff --git a/web/i18n/ru-RU/register.json b/web/i18n/locales/ru-RU/register.json similarity index 100% rename from web/i18n/ru-RU/register.json rename to web/i18n/locales/ru-RU/register.json diff --git a/web/i18n/ru-RU/run-log.json b/web/i18n/locales/ru-RU/run-log.json similarity index 100% rename from web/i18n/ru-RU/run-log.json rename to web/i18n/locales/ru-RU/run-log.json diff --git a/web/i18n/ru-RU/share.json b/web/i18n/locales/ru-RU/share.json similarity index 100% rename from web/i18n/ru-RU/share.json rename to web/i18n/locales/ru-RU/share.json diff --git a/web/i18n/ru-RU/skill.json b/web/i18n/locales/ru-RU/skill.json similarity index 100% rename from web/i18n/ru-RU/skill.json rename to web/i18n/locales/ru-RU/skill.json diff --git a/web/i18n/ru-RU/snippet.json b/web/i18n/locales/ru-RU/snippet.json similarity index 100% rename from web/i18n/ru-RU/snippet.json rename to web/i18n/locales/ru-RU/snippet.json diff --git a/web/i18n/ru-RU/time.json b/web/i18n/locales/ru-RU/time.json similarity index 100% rename from web/i18n/ru-RU/time.json rename to web/i18n/locales/ru-RU/time.json diff --git a/web/i18n/ru-RU/tools.json b/web/i18n/locales/ru-RU/tools.json similarity index 100% rename from web/i18n/ru-RU/tools.json rename to web/i18n/locales/ru-RU/tools.json diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/locales/ru-RU/workflow.json similarity index 100% rename from web/i18n/ru-RU/workflow.json rename to web/i18n/locales/ru-RU/workflow.json diff --git a/web/i18n/sl-SI/agent-v-2.json b/web/i18n/locales/sl-SI/agent-v-2.json similarity index 100% rename from web/i18n/sl-SI/agent-v-2.json rename to web/i18n/locales/sl-SI/agent-v-2.json diff --git a/web/i18n/sl-SI/app-annotation.json b/web/i18n/locales/sl-SI/app-annotation.json similarity index 100% rename from web/i18n/sl-SI/app-annotation.json rename to web/i18n/locales/sl-SI/app-annotation.json diff --git a/web/i18n/sl-SI/app-api.json b/web/i18n/locales/sl-SI/app-api.json similarity index 100% rename from web/i18n/sl-SI/app-api.json rename to web/i18n/locales/sl-SI/app-api.json diff --git a/web/i18n/sl-SI/app-debug.json b/web/i18n/locales/sl-SI/app-debug.json similarity index 100% rename from web/i18n/sl-SI/app-debug.json rename to web/i18n/locales/sl-SI/app-debug.json diff --git a/web/i18n/sl-SI/app-log.json b/web/i18n/locales/sl-SI/app-log.json similarity index 100% rename from web/i18n/sl-SI/app-log.json rename to web/i18n/locales/sl-SI/app-log.json diff --git a/web/i18n/sl-SI/app-overview.json b/web/i18n/locales/sl-SI/app-overview.json similarity index 100% rename from web/i18n/sl-SI/app-overview.json rename to web/i18n/locales/sl-SI/app-overview.json diff --git a/web/i18n/sl-SI/app.json b/web/i18n/locales/sl-SI/app.json similarity index 100% rename from web/i18n/sl-SI/app.json rename to web/i18n/locales/sl-SI/app.json diff --git a/web/i18n/sl-SI/billing.json b/web/i18n/locales/sl-SI/billing.json similarity index 100% rename from web/i18n/sl-SI/billing.json rename to web/i18n/locales/sl-SI/billing.json diff --git a/web/i18n/sl-SI/common.json b/web/i18n/locales/sl-SI/common.json similarity index 100% rename from web/i18n/sl-SI/common.json rename to web/i18n/locales/sl-SI/common.json diff --git a/web/i18n/sl-SI/custom.json b/web/i18n/locales/sl-SI/custom.json similarity index 100% rename from web/i18n/sl-SI/custom.json rename to web/i18n/locales/sl-SI/custom.json diff --git a/web/i18n/sl-SI/dataset-creation.json b/web/i18n/locales/sl-SI/dataset-creation.json similarity index 100% rename from web/i18n/sl-SI/dataset-creation.json rename to web/i18n/locales/sl-SI/dataset-creation.json diff --git a/web/i18n/sl-SI/dataset-documents.json b/web/i18n/locales/sl-SI/dataset-documents.json similarity index 100% rename from web/i18n/sl-SI/dataset-documents.json rename to web/i18n/locales/sl-SI/dataset-documents.json diff --git a/web/i18n/sl-SI/dataset-hit-testing.json b/web/i18n/locales/sl-SI/dataset-hit-testing.json similarity index 100% rename from web/i18n/sl-SI/dataset-hit-testing.json rename to web/i18n/locales/sl-SI/dataset-hit-testing.json diff --git a/web/i18n/sl-SI/dataset-pipeline.json b/web/i18n/locales/sl-SI/dataset-pipeline.json similarity index 100% rename from web/i18n/sl-SI/dataset-pipeline.json rename to web/i18n/locales/sl-SI/dataset-pipeline.json diff --git a/web/i18n/sl-SI/dataset-settings.json b/web/i18n/locales/sl-SI/dataset-settings.json similarity index 100% rename from web/i18n/sl-SI/dataset-settings.json rename to web/i18n/locales/sl-SI/dataset-settings.json diff --git a/web/i18n/sl-SI/dataset.json b/web/i18n/locales/sl-SI/dataset.json similarity index 100% rename from web/i18n/sl-SI/dataset.json rename to web/i18n/locales/sl-SI/dataset.json diff --git a/web/i18n/sl-SI/deployments.json b/web/i18n/locales/sl-SI/deployments.json similarity index 100% rename from web/i18n/sl-SI/deployments.json rename to web/i18n/locales/sl-SI/deployments.json diff --git a/web/i18n/sl-SI/device-flow.json b/web/i18n/locales/sl-SI/device-flow.json similarity index 100% rename from web/i18n/sl-SI/device-flow.json rename to web/i18n/locales/sl-SI/device-flow.json diff --git a/web/i18n/sl-SI/education.json b/web/i18n/locales/sl-SI/education.json similarity index 100% rename from web/i18n/sl-SI/education.json rename to web/i18n/locales/sl-SI/education.json diff --git a/web/i18n/sl-SI/explore.json b/web/i18n/locales/sl-SI/explore.json similarity index 100% rename from web/i18n/sl-SI/explore.json rename to web/i18n/locales/sl-SI/explore.json diff --git a/web/i18n/sl-SI/layout.json b/web/i18n/locales/sl-SI/layout.json similarity index 100% rename from web/i18n/sl-SI/layout.json rename to web/i18n/locales/sl-SI/layout.json diff --git a/web/i18n/sl-SI/login.json b/web/i18n/locales/sl-SI/login.json similarity index 100% rename from web/i18n/sl-SI/login.json rename to web/i18n/locales/sl-SI/login.json diff --git a/web/i18n/sl-SI/oauth.json b/web/i18n/locales/sl-SI/oauth.json similarity index 100% rename from web/i18n/sl-SI/oauth.json rename to web/i18n/locales/sl-SI/oauth.json diff --git a/web/i18n/sl-SI/permission-keys.json b/web/i18n/locales/sl-SI/permission-keys.json similarity index 100% rename from web/i18n/sl-SI/permission-keys.json rename to web/i18n/locales/sl-SI/permission-keys.json diff --git a/web/i18n/sl-SI/permission.json b/web/i18n/locales/sl-SI/permission.json similarity index 100% rename from web/i18n/sl-SI/permission.json rename to web/i18n/locales/sl-SI/permission.json diff --git a/web/i18n/sl-SI/pipeline.json b/web/i18n/locales/sl-SI/pipeline.json similarity index 100% rename from web/i18n/sl-SI/pipeline.json rename to web/i18n/locales/sl-SI/pipeline.json diff --git a/web/i18n/sl-SI/plugin-tags.json b/web/i18n/locales/sl-SI/plugin-tags.json similarity index 100% rename from web/i18n/sl-SI/plugin-tags.json rename to web/i18n/locales/sl-SI/plugin-tags.json diff --git a/web/i18n/sl-SI/plugin-trigger.json b/web/i18n/locales/sl-SI/plugin-trigger.json similarity index 100% rename from web/i18n/sl-SI/plugin-trigger.json rename to web/i18n/locales/sl-SI/plugin-trigger.json diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/locales/sl-SI/plugin.json similarity index 100% rename from web/i18n/sl-SI/plugin.json rename to web/i18n/locales/sl-SI/plugin.json diff --git a/web/i18n/sl-SI/register.json b/web/i18n/locales/sl-SI/register.json similarity index 100% rename from web/i18n/sl-SI/register.json rename to web/i18n/locales/sl-SI/register.json diff --git a/web/i18n/sl-SI/run-log.json b/web/i18n/locales/sl-SI/run-log.json similarity index 100% rename from web/i18n/sl-SI/run-log.json rename to web/i18n/locales/sl-SI/run-log.json diff --git a/web/i18n/sl-SI/share.json b/web/i18n/locales/sl-SI/share.json similarity index 100% rename from web/i18n/sl-SI/share.json rename to web/i18n/locales/sl-SI/share.json diff --git a/web/i18n/sl-SI/skill.json b/web/i18n/locales/sl-SI/skill.json similarity index 100% rename from web/i18n/sl-SI/skill.json rename to web/i18n/locales/sl-SI/skill.json diff --git a/web/i18n/sl-SI/snippet.json b/web/i18n/locales/sl-SI/snippet.json similarity index 100% rename from web/i18n/sl-SI/snippet.json rename to web/i18n/locales/sl-SI/snippet.json diff --git a/web/i18n/sl-SI/time.json b/web/i18n/locales/sl-SI/time.json similarity index 100% rename from web/i18n/sl-SI/time.json rename to web/i18n/locales/sl-SI/time.json diff --git a/web/i18n/sl-SI/tools.json b/web/i18n/locales/sl-SI/tools.json similarity index 100% rename from web/i18n/sl-SI/tools.json rename to web/i18n/locales/sl-SI/tools.json diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/locales/sl-SI/workflow.json similarity index 100% rename from web/i18n/sl-SI/workflow.json rename to web/i18n/locales/sl-SI/workflow.json diff --git a/web/i18n/th-TH/agent-v-2.json b/web/i18n/locales/th-TH/agent-v-2.json similarity index 100% rename from web/i18n/th-TH/agent-v-2.json rename to web/i18n/locales/th-TH/agent-v-2.json diff --git a/web/i18n/th-TH/app-annotation.json b/web/i18n/locales/th-TH/app-annotation.json similarity index 100% rename from web/i18n/th-TH/app-annotation.json rename to web/i18n/locales/th-TH/app-annotation.json diff --git a/web/i18n/th-TH/app-api.json b/web/i18n/locales/th-TH/app-api.json similarity index 100% rename from web/i18n/th-TH/app-api.json rename to web/i18n/locales/th-TH/app-api.json diff --git a/web/i18n/th-TH/app-debug.json b/web/i18n/locales/th-TH/app-debug.json similarity index 100% rename from web/i18n/th-TH/app-debug.json rename to web/i18n/locales/th-TH/app-debug.json diff --git a/web/i18n/th-TH/app-log.json b/web/i18n/locales/th-TH/app-log.json similarity index 100% rename from web/i18n/th-TH/app-log.json rename to web/i18n/locales/th-TH/app-log.json diff --git a/web/i18n/th-TH/app-overview.json b/web/i18n/locales/th-TH/app-overview.json similarity index 100% rename from web/i18n/th-TH/app-overview.json rename to web/i18n/locales/th-TH/app-overview.json diff --git a/web/i18n/th-TH/app.json b/web/i18n/locales/th-TH/app.json similarity index 100% rename from web/i18n/th-TH/app.json rename to web/i18n/locales/th-TH/app.json diff --git a/web/i18n/th-TH/billing.json b/web/i18n/locales/th-TH/billing.json similarity index 100% rename from web/i18n/th-TH/billing.json rename to web/i18n/locales/th-TH/billing.json diff --git a/web/i18n/th-TH/common.json b/web/i18n/locales/th-TH/common.json similarity index 100% rename from web/i18n/th-TH/common.json rename to web/i18n/locales/th-TH/common.json diff --git a/web/i18n/th-TH/custom.json b/web/i18n/locales/th-TH/custom.json similarity index 100% rename from web/i18n/th-TH/custom.json rename to web/i18n/locales/th-TH/custom.json diff --git a/web/i18n/th-TH/dataset-creation.json b/web/i18n/locales/th-TH/dataset-creation.json similarity index 100% rename from web/i18n/th-TH/dataset-creation.json rename to web/i18n/locales/th-TH/dataset-creation.json diff --git a/web/i18n/th-TH/dataset-documents.json b/web/i18n/locales/th-TH/dataset-documents.json similarity index 100% rename from web/i18n/th-TH/dataset-documents.json rename to web/i18n/locales/th-TH/dataset-documents.json diff --git a/web/i18n/th-TH/dataset-hit-testing.json b/web/i18n/locales/th-TH/dataset-hit-testing.json similarity index 100% rename from web/i18n/th-TH/dataset-hit-testing.json rename to web/i18n/locales/th-TH/dataset-hit-testing.json diff --git a/web/i18n/th-TH/dataset-pipeline.json b/web/i18n/locales/th-TH/dataset-pipeline.json similarity index 100% rename from web/i18n/th-TH/dataset-pipeline.json rename to web/i18n/locales/th-TH/dataset-pipeline.json diff --git a/web/i18n/th-TH/dataset-settings.json b/web/i18n/locales/th-TH/dataset-settings.json similarity index 100% rename from web/i18n/th-TH/dataset-settings.json rename to web/i18n/locales/th-TH/dataset-settings.json diff --git a/web/i18n/th-TH/dataset.json b/web/i18n/locales/th-TH/dataset.json similarity index 100% rename from web/i18n/th-TH/dataset.json rename to web/i18n/locales/th-TH/dataset.json diff --git a/web/i18n/th-TH/deployments.json b/web/i18n/locales/th-TH/deployments.json similarity index 100% rename from web/i18n/th-TH/deployments.json rename to web/i18n/locales/th-TH/deployments.json diff --git a/web/i18n/th-TH/device-flow.json b/web/i18n/locales/th-TH/device-flow.json similarity index 100% rename from web/i18n/th-TH/device-flow.json rename to web/i18n/locales/th-TH/device-flow.json diff --git a/web/i18n/th-TH/education.json b/web/i18n/locales/th-TH/education.json similarity index 100% rename from web/i18n/th-TH/education.json rename to web/i18n/locales/th-TH/education.json diff --git a/web/i18n/th-TH/explore.json b/web/i18n/locales/th-TH/explore.json similarity index 100% rename from web/i18n/th-TH/explore.json rename to web/i18n/locales/th-TH/explore.json diff --git a/web/i18n/th-TH/layout.json b/web/i18n/locales/th-TH/layout.json similarity index 100% rename from web/i18n/th-TH/layout.json rename to web/i18n/locales/th-TH/layout.json diff --git a/web/i18n/th-TH/login.json b/web/i18n/locales/th-TH/login.json similarity index 100% rename from web/i18n/th-TH/login.json rename to web/i18n/locales/th-TH/login.json diff --git a/web/i18n/th-TH/oauth.json b/web/i18n/locales/th-TH/oauth.json similarity index 100% rename from web/i18n/th-TH/oauth.json rename to web/i18n/locales/th-TH/oauth.json diff --git a/web/i18n/th-TH/permission-keys.json b/web/i18n/locales/th-TH/permission-keys.json similarity index 100% rename from web/i18n/th-TH/permission-keys.json rename to web/i18n/locales/th-TH/permission-keys.json diff --git a/web/i18n/th-TH/permission.json b/web/i18n/locales/th-TH/permission.json similarity index 100% rename from web/i18n/th-TH/permission.json rename to web/i18n/locales/th-TH/permission.json diff --git a/web/i18n/th-TH/pipeline.json b/web/i18n/locales/th-TH/pipeline.json similarity index 100% rename from web/i18n/th-TH/pipeline.json rename to web/i18n/locales/th-TH/pipeline.json diff --git a/web/i18n/th-TH/plugin-tags.json b/web/i18n/locales/th-TH/plugin-tags.json similarity index 100% rename from web/i18n/th-TH/plugin-tags.json rename to web/i18n/locales/th-TH/plugin-tags.json diff --git a/web/i18n/th-TH/plugin-trigger.json b/web/i18n/locales/th-TH/plugin-trigger.json similarity index 100% rename from web/i18n/th-TH/plugin-trigger.json rename to web/i18n/locales/th-TH/plugin-trigger.json diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/locales/th-TH/plugin.json similarity index 100% rename from web/i18n/th-TH/plugin.json rename to web/i18n/locales/th-TH/plugin.json diff --git a/web/i18n/th-TH/register.json b/web/i18n/locales/th-TH/register.json similarity index 100% rename from web/i18n/th-TH/register.json rename to web/i18n/locales/th-TH/register.json diff --git a/web/i18n/th-TH/run-log.json b/web/i18n/locales/th-TH/run-log.json similarity index 100% rename from web/i18n/th-TH/run-log.json rename to web/i18n/locales/th-TH/run-log.json diff --git a/web/i18n/th-TH/share.json b/web/i18n/locales/th-TH/share.json similarity index 100% rename from web/i18n/th-TH/share.json rename to web/i18n/locales/th-TH/share.json diff --git a/web/i18n/th-TH/skill.json b/web/i18n/locales/th-TH/skill.json similarity index 100% rename from web/i18n/th-TH/skill.json rename to web/i18n/locales/th-TH/skill.json diff --git a/web/i18n/th-TH/snippet.json b/web/i18n/locales/th-TH/snippet.json similarity index 100% rename from web/i18n/th-TH/snippet.json rename to web/i18n/locales/th-TH/snippet.json diff --git a/web/i18n/th-TH/time.json b/web/i18n/locales/th-TH/time.json similarity index 100% rename from web/i18n/th-TH/time.json rename to web/i18n/locales/th-TH/time.json diff --git a/web/i18n/th-TH/tools.json b/web/i18n/locales/th-TH/tools.json similarity index 100% rename from web/i18n/th-TH/tools.json rename to web/i18n/locales/th-TH/tools.json diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/locales/th-TH/workflow.json similarity index 100% rename from web/i18n/th-TH/workflow.json rename to web/i18n/locales/th-TH/workflow.json diff --git a/web/i18n/tr-TR/agent-v-2.json b/web/i18n/locales/tr-TR/agent-v-2.json similarity index 100% rename from web/i18n/tr-TR/agent-v-2.json rename to web/i18n/locales/tr-TR/agent-v-2.json diff --git a/web/i18n/tr-TR/app-annotation.json b/web/i18n/locales/tr-TR/app-annotation.json similarity index 100% rename from web/i18n/tr-TR/app-annotation.json rename to web/i18n/locales/tr-TR/app-annotation.json diff --git a/web/i18n/tr-TR/app-api.json b/web/i18n/locales/tr-TR/app-api.json similarity index 100% rename from web/i18n/tr-TR/app-api.json rename to web/i18n/locales/tr-TR/app-api.json diff --git a/web/i18n/tr-TR/app-debug.json b/web/i18n/locales/tr-TR/app-debug.json similarity index 100% rename from web/i18n/tr-TR/app-debug.json rename to web/i18n/locales/tr-TR/app-debug.json diff --git a/web/i18n/tr-TR/app-log.json b/web/i18n/locales/tr-TR/app-log.json similarity index 100% rename from web/i18n/tr-TR/app-log.json rename to web/i18n/locales/tr-TR/app-log.json diff --git a/web/i18n/tr-TR/app-overview.json b/web/i18n/locales/tr-TR/app-overview.json similarity index 100% rename from web/i18n/tr-TR/app-overview.json rename to web/i18n/locales/tr-TR/app-overview.json diff --git a/web/i18n/tr-TR/app.json b/web/i18n/locales/tr-TR/app.json similarity index 100% rename from web/i18n/tr-TR/app.json rename to web/i18n/locales/tr-TR/app.json diff --git a/web/i18n/tr-TR/billing.json b/web/i18n/locales/tr-TR/billing.json similarity index 100% rename from web/i18n/tr-TR/billing.json rename to web/i18n/locales/tr-TR/billing.json diff --git a/web/i18n/tr-TR/common.json b/web/i18n/locales/tr-TR/common.json similarity index 100% rename from web/i18n/tr-TR/common.json rename to web/i18n/locales/tr-TR/common.json diff --git a/web/i18n/tr-TR/custom.json b/web/i18n/locales/tr-TR/custom.json similarity index 100% rename from web/i18n/tr-TR/custom.json rename to web/i18n/locales/tr-TR/custom.json diff --git a/web/i18n/tr-TR/dataset-creation.json b/web/i18n/locales/tr-TR/dataset-creation.json similarity index 100% rename from web/i18n/tr-TR/dataset-creation.json rename to web/i18n/locales/tr-TR/dataset-creation.json diff --git a/web/i18n/tr-TR/dataset-documents.json b/web/i18n/locales/tr-TR/dataset-documents.json similarity index 100% rename from web/i18n/tr-TR/dataset-documents.json rename to web/i18n/locales/tr-TR/dataset-documents.json diff --git a/web/i18n/tr-TR/dataset-hit-testing.json b/web/i18n/locales/tr-TR/dataset-hit-testing.json similarity index 100% rename from web/i18n/tr-TR/dataset-hit-testing.json rename to web/i18n/locales/tr-TR/dataset-hit-testing.json diff --git a/web/i18n/tr-TR/dataset-pipeline.json b/web/i18n/locales/tr-TR/dataset-pipeline.json similarity index 100% rename from web/i18n/tr-TR/dataset-pipeline.json rename to web/i18n/locales/tr-TR/dataset-pipeline.json diff --git a/web/i18n/tr-TR/dataset-settings.json b/web/i18n/locales/tr-TR/dataset-settings.json similarity index 100% rename from web/i18n/tr-TR/dataset-settings.json rename to web/i18n/locales/tr-TR/dataset-settings.json diff --git a/web/i18n/tr-TR/dataset.json b/web/i18n/locales/tr-TR/dataset.json similarity index 100% rename from web/i18n/tr-TR/dataset.json rename to web/i18n/locales/tr-TR/dataset.json diff --git a/web/i18n/tr-TR/deployments.json b/web/i18n/locales/tr-TR/deployments.json similarity index 100% rename from web/i18n/tr-TR/deployments.json rename to web/i18n/locales/tr-TR/deployments.json diff --git a/web/i18n/tr-TR/device-flow.json b/web/i18n/locales/tr-TR/device-flow.json similarity index 100% rename from web/i18n/tr-TR/device-flow.json rename to web/i18n/locales/tr-TR/device-flow.json diff --git a/web/i18n/tr-TR/education.json b/web/i18n/locales/tr-TR/education.json similarity index 100% rename from web/i18n/tr-TR/education.json rename to web/i18n/locales/tr-TR/education.json diff --git a/web/i18n/tr-TR/explore.json b/web/i18n/locales/tr-TR/explore.json similarity index 100% rename from web/i18n/tr-TR/explore.json rename to web/i18n/locales/tr-TR/explore.json diff --git a/web/i18n/tr-TR/layout.json b/web/i18n/locales/tr-TR/layout.json similarity index 100% rename from web/i18n/tr-TR/layout.json rename to web/i18n/locales/tr-TR/layout.json diff --git a/web/i18n/tr-TR/login.json b/web/i18n/locales/tr-TR/login.json similarity index 100% rename from web/i18n/tr-TR/login.json rename to web/i18n/locales/tr-TR/login.json diff --git a/web/i18n/tr-TR/oauth.json b/web/i18n/locales/tr-TR/oauth.json similarity index 100% rename from web/i18n/tr-TR/oauth.json rename to web/i18n/locales/tr-TR/oauth.json diff --git a/web/i18n/tr-TR/permission-keys.json b/web/i18n/locales/tr-TR/permission-keys.json similarity index 100% rename from web/i18n/tr-TR/permission-keys.json rename to web/i18n/locales/tr-TR/permission-keys.json diff --git a/web/i18n/tr-TR/permission.json b/web/i18n/locales/tr-TR/permission.json similarity index 100% rename from web/i18n/tr-TR/permission.json rename to web/i18n/locales/tr-TR/permission.json diff --git a/web/i18n/tr-TR/pipeline.json b/web/i18n/locales/tr-TR/pipeline.json similarity index 100% rename from web/i18n/tr-TR/pipeline.json rename to web/i18n/locales/tr-TR/pipeline.json diff --git a/web/i18n/tr-TR/plugin-tags.json b/web/i18n/locales/tr-TR/plugin-tags.json similarity index 100% rename from web/i18n/tr-TR/plugin-tags.json rename to web/i18n/locales/tr-TR/plugin-tags.json diff --git a/web/i18n/tr-TR/plugin-trigger.json b/web/i18n/locales/tr-TR/plugin-trigger.json similarity index 100% rename from web/i18n/tr-TR/plugin-trigger.json rename to web/i18n/locales/tr-TR/plugin-trigger.json diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/locales/tr-TR/plugin.json similarity index 100% rename from web/i18n/tr-TR/plugin.json rename to web/i18n/locales/tr-TR/plugin.json diff --git a/web/i18n/tr-TR/register.json b/web/i18n/locales/tr-TR/register.json similarity index 100% rename from web/i18n/tr-TR/register.json rename to web/i18n/locales/tr-TR/register.json diff --git a/web/i18n/tr-TR/run-log.json b/web/i18n/locales/tr-TR/run-log.json similarity index 100% rename from web/i18n/tr-TR/run-log.json rename to web/i18n/locales/tr-TR/run-log.json diff --git a/web/i18n/tr-TR/share.json b/web/i18n/locales/tr-TR/share.json similarity index 100% rename from web/i18n/tr-TR/share.json rename to web/i18n/locales/tr-TR/share.json diff --git a/web/i18n/tr-TR/skill.json b/web/i18n/locales/tr-TR/skill.json similarity index 100% rename from web/i18n/tr-TR/skill.json rename to web/i18n/locales/tr-TR/skill.json diff --git a/web/i18n/tr-TR/snippet.json b/web/i18n/locales/tr-TR/snippet.json similarity index 100% rename from web/i18n/tr-TR/snippet.json rename to web/i18n/locales/tr-TR/snippet.json diff --git a/web/i18n/tr-TR/time.json b/web/i18n/locales/tr-TR/time.json similarity index 100% rename from web/i18n/tr-TR/time.json rename to web/i18n/locales/tr-TR/time.json diff --git a/web/i18n/tr-TR/tools.json b/web/i18n/locales/tr-TR/tools.json similarity index 100% rename from web/i18n/tr-TR/tools.json rename to web/i18n/locales/tr-TR/tools.json diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/locales/tr-TR/workflow.json similarity index 100% rename from web/i18n/tr-TR/workflow.json rename to web/i18n/locales/tr-TR/workflow.json diff --git a/web/i18n/uk-UA/agent-v-2.json b/web/i18n/locales/uk-UA/agent-v-2.json similarity index 100% rename from web/i18n/uk-UA/agent-v-2.json rename to web/i18n/locales/uk-UA/agent-v-2.json diff --git a/web/i18n/uk-UA/app-annotation.json b/web/i18n/locales/uk-UA/app-annotation.json similarity index 100% rename from web/i18n/uk-UA/app-annotation.json rename to web/i18n/locales/uk-UA/app-annotation.json diff --git a/web/i18n/uk-UA/app-api.json b/web/i18n/locales/uk-UA/app-api.json similarity index 100% rename from web/i18n/uk-UA/app-api.json rename to web/i18n/locales/uk-UA/app-api.json diff --git a/web/i18n/uk-UA/app-debug.json b/web/i18n/locales/uk-UA/app-debug.json similarity index 100% rename from web/i18n/uk-UA/app-debug.json rename to web/i18n/locales/uk-UA/app-debug.json diff --git a/web/i18n/uk-UA/app-log.json b/web/i18n/locales/uk-UA/app-log.json similarity index 100% rename from web/i18n/uk-UA/app-log.json rename to web/i18n/locales/uk-UA/app-log.json diff --git a/web/i18n/uk-UA/app-overview.json b/web/i18n/locales/uk-UA/app-overview.json similarity index 100% rename from web/i18n/uk-UA/app-overview.json rename to web/i18n/locales/uk-UA/app-overview.json diff --git a/web/i18n/uk-UA/app.json b/web/i18n/locales/uk-UA/app.json similarity index 100% rename from web/i18n/uk-UA/app.json rename to web/i18n/locales/uk-UA/app.json diff --git a/web/i18n/uk-UA/billing.json b/web/i18n/locales/uk-UA/billing.json similarity index 100% rename from web/i18n/uk-UA/billing.json rename to web/i18n/locales/uk-UA/billing.json diff --git a/web/i18n/uk-UA/common.json b/web/i18n/locales/uk-UA/common.json similarity index 100% rename from web/i18n/uk-UA/common.json rename to web/i18n/locales/uk-UA/common.json diff --git a/web/i18n/uk-UA/custom.json b/web/i18n/locales/uk-UA/custom.json similarity index 100% rename from web/i18n/uk-UA/custom.json rename to web/i18n/locales/uk-UA/custom.json diff --git a/web/i18n/uk-UA/dataset-creation.json b/web/i18n/locales/uk-UA/dataset-creation.json similarity index 100% rename from web/i18n/uk-UA/dataset-creation.json rename to web/i18n/locales/uk-UA/dataset-creation.json diff --git a/web/i18n/uk-UA/dataset-documents.json b/web/i18n/locales/uk-UA/dataset-documents.json similarity index 100% rename from web/i18n/uk-UA/dataset-documents.json rename to web/i18n/locales/uk-UA/dataset-documents.json diff --git a/web/i18n/uk-UA/dataset-hit-testing.json b/web/i18n/locales/uk-UA/dataset-hit-testing.json similarity index 100% rename from web/i18n/uk-UA/dataset-hit-testing.json rename to web/i18n/locales/uk-UA/dataset-hit-testing.json diff --git a/web/i18n/uk-UA/dataset-pipeline.json b/web/i18n/locales/uk-UA/dataset-pipeline.json similarity index 100% rename from web/i18n/uk-UA/dataset-pipeline.json rename to web/i18n/locales/uk-UA/dataset-pipeline.json diff --git a/web/i18n/uk-UA/dataset-settings.json b/web/i18n/locales/uk-UA/dataset-settings.json similarity index 100% rename from web/i18n/uk-UA/dataset-settings.json rename to web/i18n/locales/uk-UA/dataset-settings.json diff --git a/web/i18n/uk-UA/dataset.json b/web/i18n/locales/uk-UA/dataset.json similarity index 100% rename from web/i18n/uk-UA/dataset.json rename to web/i18n/locales/uk-UA/dataset.json diff --git a/web/i18n/uk-UA/deployments.json b/web/i18n/locales/uk-UA/deployments.json similarity index 100% rename from web/i18n/uk-UA/deployments.json rename to web/i18n/locales/uk-UA/deployments.json diff --git a/web/i18n/uk-UA/device-flow.json b/web/i18n/locales/uk-UA/device-flow.json similarity index 100% rename from web/i18n/uk-UA/device-flow.json rename to web/i18n/locales/uk-UA/device-flow.json diff --git a/web/i18n/uk-UA/education.json b/web/i18n/locales/uk-UA/education.json similarity index 100% rename from web/i18n/uk-UA/education.json rename to web/i18n/locales/uk-UA/education.json diff --git a/web/i18n/uk-UA/explore.json b/web/i18n/locales/uk-UA/explore.json similarity index 100% rename from web/i18n/uk-UA/explore.json rename to web/i18n/locales/uk-UA/explore.json diff --git a/web/i18n/uk-UA/layout.json b/web/i18n/locales/uk-UA/layout.json similarity index 100% rename from web/i18n/uk-UA/layout.json rename to web/i18n/locales/uk-UA/layout.json diff --git a/web/i18n/uk-UA/login.json b/web/i18n/locales/uk-UA/login.json similarity index 100% rename from web/i18n/uk-UA/login.json rename to web/i18n/locales/uk-UA/login.json diff --git a/web/i18n/uk-UA/oauth.json b/web/i18n/locales/uk-UA/oauth.json similarity index 100% rename from web/i18n/uk-UA/oauth.json rename to web/i18n/locales/uk-UA/oauth.json diff --git a/web/i18n/uk-UA/permission-keys.json b/web/i18n/locales/uk-UA/permission-keys.json similarity index 100% rename from web/i18n/uk-UA/permission-keys.json rename to web/i18n/locales/uk-UA/permission-keys.json diff --git a/web/i18n/uk-UA/permission.json b/web/i18n/locales/uk-UA/permission.json similarity index 100% rename from web/i18n/uk-UA/permission.json rename to web/i18n/locales/uk-UA/permission.json diff --git a/web/i18n/uk-UA/pipeline.json b/web/i18n/locales/uk-UA/pipeline.json similarity index 100% rename from web/i18n/uk-UA/pipeline.json rename to web/i18n/locales/uk-UA/pipeline.json diff --git a/web/i18n/uk-UA/plugin-tags.json b/web/i18n/locales/uk-UA/plugin-tags.json similarity index 100% rename from web/i18n/uk-UA/plugin-tags.json rename to web/i18n/locales/uk-UA/plugin-tags.json diff --git a/web/i18n/uk-UA/plugin-trigger.json b/web/i18n/locales/uk-UA/plugin-trigger.json similarity index 100% rename from web/i18n/uk-UA/plugin-trigger.json rename to web/i18n/locales/uk-UA/plugin-trigger.json diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/locales/uk-UA/plugin.json similarity index 100% rename from web/i18n/uk-UA/plugin.json rename to web/i18n/locales/uk-UA/plugin.json diff --git a/web/i18n/uk-UA/register.json b/web/i18n/locales/uk-UA/register.json similarity index 100% rename from web/i18n/uk-UA/register.json rename to web/i18n/locales/uk-UA/register.json diff --git a/web/i18n/uk-UA/run-log.json b/web/i18n/locales/uk-UA/run-log.json similarity index 100% rename from web/i18n/uk-UA/run-log.json rename to web/i18n/locales/uk-UA/run-log.json diff --git a/web/i18n/uk-UA/share.json b/web/i18n/locales/uk-UA/share.json similarity index 100% rename from web/i18n/uk-UA/share.json rename to web/i18n/locales/uk-UA/share.json diff --git a/web/i18n/uk-UA/skill.json b/web/i18n/locales/uk-UA/skill.json similarity index 100% rename from web/i18n/uk-UA/skill.json rename to web/i18n/locales/uk-UA/skill.json diff --git a/web/i18n/uk-UA/snippet.json b/web/i18n/locales/uk-UA/snippet.json similarity index 100% rename from web/i18n/uk-UA/snippet.json rename to web/i18n/locales/uk-UA/snippet.json diff --git a/web/i18n/uk-UA/time.json b/web/i18n/locales/uk-UA/time.json similarity index 100% rename from web/i18n/uk-UA/time.json rename to web/i18n/locales/uk-UA/time.json diff --git a/web/i18n/uk-UA/tools.json b/web/i18n/locales/uk-UA/tools.json similarity index 100% rename from web/i18n/uk-UA/tools.json rename to web/i18n/locales/uk-UA/tools.json diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/locales/uk-UA/workflow.json similarity index 100% rename from web/i18n/uk-UA/workflow.json rename to web/i18n/locales/uk-UA/workflow.json diff --git a/web/i18n/vi-VN/agent-v-2.json b/web/i18n/locales/vi-VN/agent-v-2.json similarity index 100% rename from web/i18n/vi-VN/agent-v-2.json rename to web/i18n/locales/vi-VN/agent-v-2.json diff --git a/web/i18n/vi-VN/app-annotation.json b/web/i18n/locales/vi-VN/app-annotation.json similarity index 100% rename from web/i18n/vi-VN/app-annotation.json rename to web/i18n/locales/vi-VN/app-annotation.json diff --git a/web/i18n/vi-VN/app-api.json b/web/i18n/locales/vi-VN/app-api.json similarity index 100% rename from web/i18n/vi-VN/app-api.json rename to web/i18n/locales/vi-VN/app-api.json diff --git a/web/i18n/vi-VN/app-debug.json b/web/i18n/locales/vi-VN/app-debug.json similarity index 100% rename from web/i18n/vi-VN/app-debug.json rename to web/i18n/locales/vi-VN/app-debug.json diff --git a/web/i18n/vi-VN/app-log.json b/web/i18n/locales/vi-VN/app-log.json similarity index 100% rename from web/i18n/vi-VN/app-log.json rename to web/i18n/locales/vi-VN/app-log.json diff --git a/web/i18n/vi-VN/app-overview.json b/web/i18n/locales/vi-VN/app-overview.json similarity index 100% rename from web/i18n/vi-VN/app-overview.json rename to web/i18n/locales/vi-VN/app-overview.json diff --git a/web/i18n/vi-VN/app.json b/web/i18n/locales/vi-VN/app.json similarity index 100% rename from web/i18n/vi-VN/app.json rename to web/i18n/locales/vi-VN/app.json diff --git a/web/i18n/vi-VN/billing.json b/web/i18n/locales/vi-VN/billing.json similarity index 100% rename from web/i18n/vi-VN/billing.json rename to web/i18n/locales/vi-VN/billing.json diff --git a/web/i18n/vi-VN/common.json b/web/i18n/locales/vi-VN/common.json similarity index 100% rename from web/i18n/vi-VN/common.json rename to web/i18n/locales/vi-VN/common.json diff --git a/web/i18n/vi-VN/custom.json b/web/i18n/locales/vi-VN/custom.json similarity index 100% rename from web/i18n/vi-VN/custom.json rename to web/i18n/locales/vi-VN/custom.json diff --git a/web/i18n/vi-VN/dataset-creation.json b/web/i18n/locales/vi-VN/dataset-creation.json similarity index 100% rename from web/i18n/vi-VN/dataset-creation.json rename to web/i18n/locales/vi-VN/dataset-creation.json diff --git a/web/i18n/vi-VN/dataset-documents.json b/web/i18n/locales/vi-VN/dataset-documents.json similarity index 100% rename from web/i18n/vi-VN/dataset-documents.json rename to web/i18n/locales/vi-VN/dataset-documents.json diff --git a/web/i18n/vi-VN/dataset-hit-testing.json b/web/i18n/locales/vi-VN/dataset-hit-testing.json similarity index 100% rename from web/i18n/vi-VN/dataset-hit-testing.json rename to web/i18n/locales/vi-VN/dataset-hit-testing.json diff --git a/web/i18n/vi-VN/dataset-pipeline.json b/web/i18n/locales/vi-VN/dataset-pipeline.json similarity index 100% rename from web/i18n/vi-VN/dataset-pipeline.json rename to web/i18n/locales/vi-VN/dataset-pipeline.json diff --git a/web/i18n/vi-VN/dataset-settings.json b/web/i18n/locales/vi-VN/dataset-settings.json similarity index 100% rename from web/i18n/vi-VN/dataset-settings.json rename to web/i18n/locales/vi-VN/dataset-settings.json diff --git a/web/i18n/vi-VN/dataset.json b/web/i18n/locales/vi-VN/dataset.json similarity index 100% rename from web/i18n/vi-VN/dataset.json rename to web/i18n/locales/vi-VN/dataset.json diff --git a/web/i18n/vi-VN/deployments.json b/web/i18n/locales/vi-VN/deployments.json similarity index 100% rename from web/i18n/vi-VN/deployments.json rename to web/i18n/locales/vi-VN/deployments.json diff --git a/web/i18n/vi-VN/device-flow.json b/web/i18n/locales/vi-VN/device-flow.json similarity index 100% rename from web/i18n/vi-VN/device-flow.json rename to web/i18n/locales/vi-VN/device-flow.json diff --git a/web/i18n/vi-VN/education.json b/web/i18n/locales/vi-VN/education.json similarity index 100% rename from web/i18n/vi-VN/education.json rename to web/i18n/locales/vi-VN/education.json diff --git a/web/i18n/vi-VN/explore.json b/web/i18n/locales/vi-VN/explore.json similarity index 100% rename from web/i18n/vi-VN/explore.json rename to web/i18n/locales/vi-VN/explore.json diff --git a/web/i18n/vi-VN/layout.json b/web/i18n/locales/vi-VN/layout.json similarity index 100% rename from web/i18n/vi-VN/layout.json rename to web/i18n/locales/vi-VN/layout.json diff --git a/web/i18n/vi-VN/login.json b/web/i18n/locales/vi-VN/login.json similarity index 100% rename from web/i18n/vi-VN/login.json rename to web/i18n/locales/vi-VN/login.json diff --git a/web/i18n/vi-VN/oauth.json b/web/i18n/locales/vi-VN/oauth.json similarity index 100% rename from web/i18n/vi-VN/oauth.json rename to web/i18n/locales/vi-VN/oauth.json diff --git a/web/i18n/vi-VN/permission-keys.json b/web/i18n/locales/vi-VN/permission-keys.json similarity index 100% rename from web/i18n/vi-VN/permission-keys.json rename to web/i18n/locales/vi-VN/permission-keys.json diff --git a/web/i18n/vi-VN/permission.json b/web/i18n/locales/vi-VN/permission.json similarity index 100% rename from web/i18n/vi-VN/permission.json rename to web/i18n/locales/vi-VN/permission.json diff --git a/web/i18n/vi-VN/pipeline.json b/web/i18n/locales/vi-VN/pipeline.json similarity index 100% rename from web/i18n/vi-VN/pipeline.json rename to web/i18n/locales/vi-VN/pipeline.json diff --git a/web/i18n/vi-VN/plugin-tags.json b/web/i18n/locales/vi-VN/plugin-tags.json similarity index 100% rename from web/i18n/vi-VN/plugin-tags.json rename to web/i18n/locales/vi-VN/plugin-tags.json diff --git a/web/i18n/vi-VN/plugin-trigger.json b/web/i18n/locales/vi-VN/plugin-trigger.json similarity index 100% rename from web/i18n/vi-VN/plugin-trigger.json rename to web/i18n/locales/vi-VN/plugin-trigger.json diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/locales/vi-VN/plugin.json similarity index 100% rename from web/i18n/vi-VN/plugin.json rename to web/i18n/locales/vi-VN/plugin.json diff --git a/web/i18n/vi-VN/register.json b/web/i18n/locales/vi-VN/register.json similarity index 100% rename from web/i18n/vi-VN/register.json rename to web/i18n/locales/vi-VN/register.json diff --git a/web/i18n/vi-VN/run-log.json b/web/i18n/locales/vi-VN/run-log.json similarity index 100% rename from web/i18n/vi-VN/run-log.json rename to web/i18n/locales/vi-VN/run-log.json diff --git a/web/i18n/vi-VN/share.json b/web/i18n/locales/vi-VN/share.json similarity index 100% rename from web/i18n/vi-VN/share.json rename to web/i18n/locales/vi-VN/share.json diff --git a/web/i18n/vi-VN/skill.json b/web/i18n/locales/vi-VN/skill.json similarity index 100% rename from web/i18n/vi-VN/skill.json rename to web/i18n/locales/vi-VN/skill.json diff --git a/web/i18n/vi-VN/snippet.json b/web/i18n/locales/vi-VN/snippet.json similarity index 100% rename from web/i18n/vi-VN/snippet.json rename to web/i18n/locales/vi-VN/snippet.json diff --git a/web/i18n/vi-VN/time.json b/web/i18n/locales/vi-VN/time.json similarity index 100% rename from web/i18n/vi-VN/time.json rename to web/i18n/locales/vi-VN/time.json diff --git a/web/i18n/vi-VN/tools.json b/web/i18n/locales/vi-VN/tools.json similarity index 100% rename from web/i18n/vi-VN/tools.json rename to web/i18n/locales/vi-VN/tools.json diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/locales/vi-VN/workflow.json similarity index 100% rename from web/i18n/vi-VN/workflow.json rename to web/i18n/locales/vi-VN/workflow.json diff --git a/web/i18n/zh-Hans/agent-v-2.json b/web/i18n/locales/zh-Hans/agent-v-2.json similarity index 100% rename from web/i18n/zh-Hans/agent-v-2.json rename to web/i18n/locales/zh-Hans/agent-v-2.json diff --git a/web/i18n/zh-Hans/app-annotation.json b/web/i18n/locales/zh-Hans/app-annotation.json similarity index 100% rename from web/i18n/zh-Hans/app-annotation.json rename to web/i18n/locales/zh-Hans/app-annotation.json diff --git a/web/i18n/zh-Hans/app-api.json b/web/i18n/locales/zh-Hans/app-api.json similarity index 100% rename from web/i18n/zh-Hans/app-api.json rename to web/i18n/locales/zh-Hans/app-api.json diff --git a/web/i18n/zh-Hans/app-debug.json b/web/i18n/locales/zh-Hans/app-debug.json similarity index 100% rename from web/i18n/zh-Hans/app-debug.json rename to web/i18n/locales/zh-Hans/app-debug.json diff --git a/web/i18n/zh-Hans/app-log.json b/web/i18n/locales/zh-Hans/app-log.json similarity index 100% rename from web/i18n/zh-Hans/app-log.json rename to web/i18n/locales/zh-Hans/app-log.json diff --git a/web/i18n/zh-Hans/app-overview.json b/web/i18n/locales/zh-Hans/app-overview.json similarity index 100% rename from web/i18n/zh-Hans/app-overview.json rename to web/i18n/locales/zh-Hans/app-overview.json diff --git a/web/i18n/zh-Hans/app.json b/web/i18n/locales/zh-Hans/app.json similarity index 100% rename from web/i18n/zh-Hans/app.json rename to web/i18n/locales/zh-Hans/app.json diff --git a/web/i18n/zh-Hans/billing.json b/web/i18n/locales/zh-Hans/billing.json similarity index 100% rename from web/i18n/zh-Hans/billing.json rename to web/i18n/locales/zh-Hans/billing.json diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/locales/zh-Hans/common.json similarity index 100% rename from web/i18n/zh-Hans/common.json rename to web/i18n/locales/zh-Hans/common.json diff --git a/web/i18n/zh-Hans/custom.json b/web/i18n/locales/zh-Hans/custom.json similarity index 100% rename from web/i18n/zh-Hans/custom.json rename to web/i18n/locales/zh-Hans/custom.json diff --git a/web/i18n/zh-Hans/dataset-creation.json b/web/i18n/locales/zh-Hans/dataset-creation.json similarity index 100% rename from web/i18n/zh-Hans/dataset-creation.json rename to web/i18n/locales/zh-Hans/dataset-creation.json diff --git a/web/i18n/zh-Hans/dataset-documents.json b/web/i18n/locales/zh-Hans/dataset-documents.json similarity index 100% rename from web/i18n/zh-Hans/dataset-documents.json rename to web/i18n/locales/zh-Hans/dataset-documents.json diff --git a/web/i18n/zh-Hans/dataset-hit-testing.json b/web/i18n/locales/zh-Hans/dataset-hit-testing.json similarity index 100% rename from web/i18n/zh-Hans/dataset-hit-testing.json rename to web/i18n/locales/zh-Hans/dataset-hit-testing.json diff --git a/web/i18n/zh-Hans/dataset-pipeline.json b/web/i18n/locales/zh-Hans/dataset-pipeline.json similarity index 100% rename from web/i18n/zh-Hans/dataset-pipeline.json rename to web/i18n/locales/zh-Hans/dataset-pipeline.json diff --git a/web/i18n/zh-Hans/dataset-settings.json b/web/i18n/locales/zh-Hans/dataset-settings.json similarity index 100% rename from web/i18n/zh-Hans/dataset-settings.json rename to web/i18n/locales/zh-Hans/dataset-settings.json diff --git a/web/i18n/zh-Hans/dataset.json b/web/i18n/locales/zh-Hans/dataset.json similarity index 100% rename from web/i18n/zh-Hans/dataset.json rename to web/i18n/locales/zh-Hans/dataset.json diff --git a/web/i18n/zh-Hans/deployments.json b/web/i18n/locales/zh-Hans/deployments.json similarity index 100% rename from web/i18n/zh-Hans/deployments.json rename to web/i18n/locales/zh-Hans/deployments.json diff --git a/web/i18n/zh-Hans/device-flow.json b/web/i18n/locales/zh-Hans/device-flow.json similarity index 100% rename from web/i18n/zh-Hans/device-flow.json rename to web/i18n/locales/zh-Hans/device-flow.json diff --git a/web/i18n/zh-Hans/education.json b/web/i18n/locales/zh-Hans/education.json similarity index 100% rename from web/i18n/zh-Hans/education.json rename to web/i18n/locales/zh-Hans/education.json diff --git a/web/i18n/zh-Hans/explore.json b/web/i18n/locales/zh-Hans/explore.json similarity index 100% rename from web/i18n/zh-Hans/explore.json rename to web/i18n/locales/zh-Hans/explore.json diff --git a/web/i18n/zh-Hans/layout.json b/web/i18n/locales/zh-Hans/layout.json similarity index 100% rename from web/i18n/zh-Hans/layout.json rename to web/i18n/locales/zh-Hans/layout.json diff --git a/web/i18n/zh-Hans/login.json b/web/i18n/locales/zh-Hans/login.json similarity index 100% rename from web/i18n/zh-Hans/login.json rename to web/i18n/locales/zh-Hans/login.json diff --git a/web/i18n/zh-Hans/oauth.json b/web/i18n/locales/zh-Hans/oauth.json similarity index 100% rename from web/i18n/zh-Hans/oauth.json rename to web/i18n/locales/zh-Hans/oauth.json diff --git a/web/i18n/zh-Hans/permission-keys.json b/web/i18n/locales/zh-Hans/permission-keys.json similarity index 100% rename from web/i18n/zh-Hans/permission-keys.json rename to web/i18n/locales/zh-Hans/permission-keys.json diff --git a/web/i18n/zh-Hans/permission.json b/web/i18n/locales/zh-Hans/permission.json similarity index 100% rename from web/i18n/zh-Hans/permission.json rename to web/i18n/locales/zh-Hans/permission.json diff --git a/web/i18n/zh-Hans/pipeline.json b/web/i18n/locales/zh-Hans/pipeline.json similarity index 100% rename from web/i18n/zh-Hans/pipeline.json rename to web/i18n/locales/zh-Hans/pipeline.json diff --git a/web/i18n/zh-Hans/plugin-tags.json b/web/i18n/locales/zh-Hans/plugin-tags.json similarity index 100% rename from web/i18n/zh-Hans/plugin-tags.json rename to web/i18n/locales/zh-Hans/plugin-tags.json diff --git a/web/i18n/zh-Hans/plugin-trigger.json b/web/i18n/locales/zh-Hans/plugin-trigger.json similarity index 100% rename from web/i18n/zh-Hans/plugin-trigger.json rename to web/i18n/locales/zh-Hans/plugin-trigger.json diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/locales/zh-Hans/plugin.json similarity index 100% rename from web/i18n/zh-Hans/plugin.json rename to web/i18n/locales/zh-Hans/plugin.json diff --git a/web/i18n/zh-Hans/register.json b/web/i18n/locales/zh-Hans/register.json similarity index 100% rename from web/i18n/zh-Hans/register.json rename to web/i18n/locales/zh-Hans/register.json diff --git a/web/i18n/zh-Hans/run-log.json b/web/i18n/locales/zh-Hans/run-log.json similarity index 100% rename from web/i18n/zh-Hans/run-log.json rename to web/i18n/locales/zh-Hans/run-log.json diff --git a/web/i18n/zh-Hans/share.json b/web/i18n/locales/zh-Hans/share.json similarity index 100% rename from web/i18n/zh-Hans/share.json rename to web/i18n/locales/zh-Hans/share.json diff --git a/web/i18n/zh-Hans/skill.json b/web/i18n/locales/zh-Hans/skill.json similarity index 100% rename from web/i18n/zh-Hans/skill.json rename to web/i18n/locales/zh-Hans/skill.json diff --git a/web/i18n/zh-Hans/snippet.json b/web/i18n/locales/zh-Hans/snippet.json similarity index 100% rename from web/i18n/zh-Hans/snippet.json rename to web/i18n/locales/zh-Hans/snippet.json diff --git a/web/i18n/zh-Hans/time.json b/web/i18n/locales/zh-Hans/time.json similarity index 100% rename from web/i18n/zh-Hans/time.json rename to web/i18n/locales/zh-Hans/time.json diff --git a/web/i18n/zh-Hans/tools.json b/web/i18n/locales/zh-Hans/tools.json similarity index 100% rename from web/i18n/zh-Hans/tools.json rename to web/i18n/locales/zh-Hans/tools.json diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/locales/zh-Hans/workflow.json similarity index 100% rename from web/i18n/zh-Hans/workflow.json rename to web/i18n/locales/zh-Hans/workflow.json diff --git a/web/i18n/zh-Hant/agent-v-2.json b/web/i18n/locales/zh-Hant/agent-v-2.json similarity index 100% rename from web/i18n/zh-Hant/agent-v-2.json rename to web/i18n/locales/zh-Hant/agent-v-2.json diff --git a/web/i18n/zh-Hant/app-annotation.json b/web/i18n/locales/zh-Hant/app-annotation.json similarity index 100% rename from web/i18n/zh-Hant/app-annotation.json rename to web/i18n/locales/zh-Hant/app-annotation.json diff --git a/web/i18n/zh-Hant/app-api.json b/web/i18n/locales/zh-Hant/app-api.json similarity index 100% rename from web/i18n/zh-Hant/app-api.json rename to web/i18n/locales/zh-Hant/app-api.json diff --git a/web/i18n/zh-Hant/app-debug.json b/web/i18n/locales/zh-Hant/app-debug.json similarity index 100% rename from web/i18n/zh-Hant/app-debug.json rename to web/i18n/locales/zh-Hant/app-debug.json diff --git a/web/i18n/zh-Hant/app-log.json b/web/i18n/locales/zh-Hant/app-log.json similarity index 100% rename from web/i18n/zh-Hant/app-log.json rename to web/i18n/locales/zh-Hant/app-log.json diff --git a/web/i18n/zh-Hant/app-overview.json b/web/i18n/locales/zh-Hant/app-overview.json similarity index 100% rename from web/i18n/zh-Hant/app-overview.json rename to web/i18n/locales/zh-Hant/app-overview.json diff --git a/web/i18n/zh-Hant/app.json b/web/i18n/locales/zh-Hant/app.json similarity index 100% rename from web/i18n/zh-Hant/app.json rename to web/i18n/locales/zh-Hant/app.json diff --git a/web/i18n/zh-Hant/billing.json b/web/i18n/locales/zh-Hant/billing.json similarity index 100% rename from web/i18n/zh-Hant/billing.json rename to web/i18n/locales/zh-Hant/billing.json diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/locales/zh-Hant/common.json similarity index 100% rename from web/i18n/zh-Hant/common.json rename to web/i18n/locales/zh-Hant/common.json diff --git a/web/i18n/zh-Hant/custom.json b/web/i18n/locales/zh-Hant/custom.json similarity index 100% rename from web/i18n/zh-Hant/custom.json rename to web/i18n/locales/zh-Hant/custom.json diff --git a/web/i18n/zh-Hant/dataset-creation.json b/web/i18n/locales/zh-Hant/dataset-creation.json similarity index 100% rename from web/i18n/zh-Hant/dataset-creation.json rename to web/i18n/locales/zh-Hant/dataset-creation.json diff --git a/web/i18n/zh-Hant/dataset-documents.json b/web/i18n/locales/zh-Hant/dataset-documents.json similarity index 100% rename from web/i18n/zh-Hant/dataset-documents.json rename to web/i18n/locales/zh-Hant/dataset-documents.json diff --git a/web/i18n/zh-Hant/dataset-hit-testing.json b/web/i18n/locales/zh-Hant/dataset-hit-testing.json similarity index 100% rename from web/i18n/zh-Hant/dataset-hit-testing.json rename to web/i18n/locales/zh-Hant/dataset-hit-testing.json diff --git a/web/i18n/zh-Hant/dataset-pipeline.json b/web/i18n/locales/zh-Hant/dataset-pipeline.json similarity index 100% rename from web/i18n/zh-Hant/dataset-pipeline.json rename to web/i18n/locales/zh-Hant/dataset-pipeline.json diff --git a/web/i18n/zh-Hant/dataset-settings.json b/web/i18n/locales/zh-Hant/dataset-settings.json similarity index 100% rename from web/i18n/zh-Hant/dataset-settings.json rename to web/i18n/locales/zh-Hant/dataset-settings.json diff --git a/web/i18n/zh-Hant/dataset.json b/web/i18n/locales/zh-Hant/dataset.json similarity index 100% rename from web/i18n/zh-Hant/dataset.json rename to web/i18n/locales/zh-Hant/dataset.json diff --git a/web/i18n/zh-Hant/deployments.json b/web/i18n/locales/zh-Hant/deployments.json similarity index 100% rename from web/i18n/zh-Hant/deployments.json rename to web/i18n/locales/zh-Hant/deployments.json diff --git a/web/i18n/zh-Hant/device-flow.json b/web/i18n/locales/zh-Hant/device-flow.json similarity index 100% rename from web/i18n/zh-Hant/device-flow.json rename to web/i18n/locales/zh-Hant/device-flow.json diff --git a/web/i18n/zh-Hant/education.json b/web/i18n/locales/zh-Hant/education.json similarity index 100% rename from web/i18n/zh-Hant/education.json rename to web/i18n/locales/zh-Hant/education.json diff --git a/web/i18n/zh-Hant/explore.json b/web/i18n/locales/zh-Hant/explore.json similarity index 100% rename from web/i18n/zh-Hant/explore.json rename to web/i18n/locales/zh-Hant/explore.json diff --git a/web/i18n/zh-Hant/layout.json b/web/i18n/locales/zh-Hant/layout.json similarity index 100% rename from web/i18n/zh-Hant/layout.json rename to web/i18n/locales/zh-Hant/layout.json diff --git a/web/i18n/zh-Hant/login.json b/web/i18n/locales/zh-Hant/login.json similarity index 100% rename from web/i18n/zh-Hant/login.json rename to web/i18n/locales/zh-Hant/login.json diff --git a/web/i18n/zh-Hant/oauth.json b/web/i18n/locales/zh-Hant/oauth.json similarity index 100% rename from web/i18n/zh-Hant/oauth.json rename to web/i18n/locales/zh-Hant/oauth.json diff --git a/web/i18n/zh-Hant/permission-keys.json b/web/i18n/locales/zh-Hant/permission-keys.json similarity index 100% rename from web/i18n/zh-Hant/permission-keys.json rename to web/i18n/locales/zh-Hant/permission-keys.json diff --git a/web/i18n/zh-Hant/permission.json b/web/i18n/locales/zh-Hant/permission.json similarity index 100% rename from web/i18n/zh-Hant/permission.json rename to web/i18n/locales/zh-Hant/permission.json diff --git a/web/i18n/zh-Hant/pipeline.json b/web/i18n/locales/zh-Hant/pipeline.json similarity index 100% rename from web/i18n/zh-Hant/pipeline.json rename to web/i18n/locales/zh-Hant/pipeline.json diff --git a/web/i18n/zh-Hant/plugin-tags.json b/web/i18n/locales/zh-Hant/plugin-tags.json similarity index 100% rename from web/i18n/zh-Hant/plugin-tags.json rename to web/i18n/locales/zh-Hant/plugin-tags.json diff --git a/web/i18n/zh-Hant/plugin-trigger.json b/web/i18n/locales/zh-Hant/plugin-trigger.json similarity index 100% rename from web/i18n/zh-Hant/plugin-trigger.json rename to web/i18n/locales/zh-Hant/plugin-trigger.json diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/locales/zh-Hant/plugin.json similarity index 100% rename from web/i18n/zh-Hant/plugin.json rename to web/i18n/locales/zh-Hant/plugin.json diff --git a/web/i18n/zh-Hant/register.json b/web/i18n/locales/zh-Hant/register.json similarity index 100% rename from web/i18n/zh-Hant/register.json rename to web/i18n/locales/zh-Hant/register.json diff --git a/web/i18n/zh-Hant/run-log.json b/web/i18n/locales/zh-Hant/run-log.json similarity index 100% rename from web/i18n/zh-Hant/run-log.json rename to web/i18n/locales/zh-Hant/run-log.json diff --git a/web/i18n/zh-Hant/share.json b/web/i18n/locales/zh-Hant/share.json similarity index 100% rename from web/i18n/zh-Hant/share.json rename to web/i18n/locales/zh-Hant/share.json diff --git a/web/i18n/zh-Hant/skill.json b/web/i18n/locales/zh-Hant/skill.json similarity index 100% rename from web/i18n/zh-Hant/skill.json rename to web/i18n/locales/zh-Hant/skill.json diff --git a/web/i18n/zh-Hant/snippet.json b/web/i18n/locales/zh-Hant/snippet.json similarity index 100% rename from web/i18n/zh-Hant/snippet.json rename to web/i18n/locales/zh-Hant/snippet.json diff --git a/web/i18n/zh-Hant/time.json b/web/i18n/locales/zh-Hant/time.json similarity index 100% rename from web/i18n/zh-Hant/time.json rename to web/i18n/locales/zh-Hant/time.json diff --git a/web/i18n/zh-Hant/tools.json b/web/i18n/locales/zh-Hant/tools.json similarity index 100% rename from web/i18n/zh-Hant/tools.json rename to web/i18n/locales/zh-Hant/tools.json diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/locales/zh-Hant/workflow.json similarity index 100% rename from web/i18n/zh-Hant/workflow.json rename to web/i18n/locales/zh-Hant/workflow.json diff --git a/web/i18n-config/metadata.ts b/web/i18n/metadata.ts similarity index 100% rename from web/i18n-config/metadata.ts rename to web/i18n/metadata.ts diff --git a/web/i18n-config/resources.ts b/web/i18n/resources.ts similarity index 63% rename from web/i18n-config/resources.ts rename to web/i18n/resources.ts index cdd72e747f7..6447c457510 100644 --- a/web/i18n-config/resources.ts +++ b/web/i18n/resources.ts @@ -1,40 +1,40 @@ -import type agentV2 from '../i18n/en-US/agent-v-2.json' -import type appAnnotation from '../i18n/en-US/app-annotation.json' -import type appApi from '../i18n/en-US/app-api.json' -import type appDebug from '../i18n/en-US/app-debug.json' -import type appLog from '../i18n/en-US/app-log.json' -import type appOverview from '../i18n/en-US/app-overview.json' -import type app from '../i18n/en-US/app.json' -import type billing from '../i18n/en-US/billing.json' -import type common from '../i18n/en-US/common.json' -import type custom from '../i18n/en-US/custom.json' -import type datasetCreation from '../i18n/en-US/dataset-creation.json' -import type datasetDocuments from '../i18n/en-US/dataset-documents.json' -import type datasetHitTesting from '../i18n/en-US/dataset-hit-testing.json' -import type datasetPipeline from '../i18n/en-US/dataset-pipeline.json' -import type datasetSettings from '../i18n/en-US/dataset-settings.json' -import type dataset from '../i18n/en-US/dataset.json' -import type deployments from '../i18n/en-US/deployments.json' -import type deviceFlow from '../i18n/en-US/device-flow.json' -import type education from '../i18n/en-US/education.json' -import type explore from '../i18n/en-US/explore.json' -import type layout from '../i18n/en-US/layout.json' -import type login from '../i18n/en-US/login.json' -import type oauth from '../i18n/en-US/oauth.json' -import type permissionKeys from '../i18n/en-US/permission-keys.json' -import type permission from '../i18n/en-US/permission.json' -import type pipeline from '../i18n/en-US/pipeline.json' -import type pluginTags from '../i18n/en-US/plugin-tags.json' -import type pluginTrigger from '../i18n/en-US/plugin-trigger.json' -import type plugin from '../i18n/en-US/plugin.json' -import type register from '../i18n/en-US/register.json' -import type runLog from '../i18n/en-US/run-log.json' -import type share from '../i18n/en-US/share.json' -import type skill from '../i18n/en-US/skill.json' -import type snippet from '../i18n/en-US/snippet.json' -import type time from '../i18n/en-US/time.json' -import type tools from '../i18n/en-US/tools.json' -import type workflow from '../i18n/en-US/workflow.json' +import type agentV2 from './locales/en-US/agent-v-2.json' +import type appAnnotation from './locales/en-US/app-annotation.json' +import type appApi from './locales/en-US/app-api.json' +import type appDebug from './locales/en-US/app-debug.json' +import type appLog from './locales/en-US/app-log.json' +import type appOverview from './locales/en-US/app-overview.json' +import type app from './locales/en-US/app.json' +import type billing from './locales/en-US/billing.json' +import type common from './locales/en-US/common.json' +import type custom from './locales/en-US/custom.json' +import type datasetCreation from './locales/en-US/dataset-creation.json' +import type datasetDocuments from './locales/en-US/dataset-documents.json' +import type datasetHitTesting from './locales/en-US/dataset-hit-testing.json' +import type datasetPipeline from './locales/en-US/dataset-pipeline.json' +import type datasetSettings from './locales/en-US/dataset-settings.json' +import type dataset from './locales/en-US/dataset.json' +import type deployments from './locales/en-US/deployments.json' +import type deviceFlow from './locales/en-US/device-flow.json' +import type education from './locales/en-US/education.json' +import type explore from './locales/en-US/explore.json' +import type layout from './locales/en-US/layout.json' +import type login from './locales/en-US/login.json' +import type oauth from './locales/en-US/oauth.json' +import type permissionKeys from './locales/en-US/permission-keys.json' +import type permission from './locales/en-US/permission.json' +import type pipeline from './locales/en-US/pipeline.json' +import type pluginTags from './locales/en-US/plugin-tags.json' +import type pluginTrigger from './locales/en-US/plugin-trigger.json' +import type plugin from './locales/en-US/plugin.json' +import type register from './locales/en-US/register.json' +import type runLog from './locales/en-US/run-log.json' +import type share from './locales/en-US/share.json' +import type skill from './locales/en-US/skill.json' +import type snippet from './locales/en-US/snippet.json' +import type time from './locales/en-US/time.json' +import type tools from './locales/en-US/tools.json' +import type workflow from './locales/en-US/workflow.json' import { kebabCase } from 'string-ts' type RawResources = { diff --git a/web/i18n-config/server.ts b/web/i18n/server.ts similarity index 100% rename from web/i18n-config/server.ts rename to web/i18n/server.ts diff --git a/web/i18n-config/settings.ts b/web/i18n/settings.ts similarity index 100% rename from web/i18n-config/settings.ts rename to web/i18n/settings.ts diff --git a/web/models/access-control.ts b/web/models/access-control.ts index 72b7a1a4457..346c351b97d 100644 --- a/web/models/access-control.ts +++ b/web/models/access-control.ts @@ -1,4 +1,4 @@ -import type { AccessControlTemplateLanguage } from '@/i18n-config/language' +import type { AccessControlTemplateLanguage } from '@/i18n/language' export const SubjectType = { GROUP: 'group', diff --git a/web/models/common.ts b/web/models/common.ts index 692cc7c9b0a..1fbf40d6c9a 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -1,6 +1,6 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' import type { Role } from './access-control' -import type { I18nText } from '@/i18n-config/language' +import type { I18nText } from '@/i18n/language' import type { Model } from '@/types/app' export type CommonResponse = { diff --git a/web/models/share.ts b/web/models/share.ts index 28463103d04..95116aaec1b 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -1,5 +1,5 @@ import type { AppMode } from '@dify/contracts/api/web/types.gen' -import type { Locale } from '@/i18n-config' +import type { Locale } from '@/i18n' import type { AppIconType } from '@/types/app' export type ConversationItem = { diff --git a/web/package.json b/web/package.json index 02249452527..4f87e9aff96 100644 --- a/web/package.json +++ b/web/package.json @@ -5,8 +5,8 @@ "type": "module", "imports": { "#i18n": { - "react-server": "./i18n-config/lib.server.ts", - "default": "./i18n-config/lib.client.ts" + "react-server": "./i18n/lib.server.ts", + "default": "./i18n/lib.client.ts" } }, "scripts": { diff --git a/web/plugins/eslint/rules/no-extra-keys.js b/web/plugins/eslint/rules/no-extra-keys.js index 024693417e5..147bd55dca5 100644 --- a/web/plugins/eslint/rules/no-extra-keys.js +++ b/web/plugins/eslint/rules/no-extra-keys.js @@ -18,7 +18,7 @@ export default { if (!filename.endsWith('.json')) return const parts = normalize(filename).split(sep) - // e.g., i18n/ar-TN/common.json -> jsonFile = common.json, lang = ar-TN + // e.g., i18n/locales/ar-TN/common.json -> jsonFile = common.json, lang = ar-TN const jsonFile = parts.at(-1) const lang = parts.at(-2) @@ -31,7 +31,7 @@ export default { try { currentJson = JSON.parse(sourceCode.text) // Look for the same filename in en-US folder - // e.g., i18n/ar-TN/common.json -> i18n/en-US/common.json + // e.g., i18n/locales/ar-TN/common.json -> i18n/locales/en-US/common.json const englishFilePath = path.join(path.dirname(filename), '..', 'en-US', jsonFile ?? '') englishJson = JSON.parse(fs.readFileSync(englishFilePath, 'utf8')) } catch (error) { diff --git a/web/plugins/vite/__tests__/i18n-graph.spec.ts b/web/plugins/vite/__tests__/i18n-graph.spec.ts index 73f54f237a6..f2f66f305ba 100644 --- a/web/plugins/vite/__tests__/i18n-graph.spec.ts +++ b/web/plugins/vite/__tests__/i18n-graph.spec.ts @@ -38,9 +38,12 @@ describe('translation graph analysis', () => { describe('Usage Analysis', () => { it('resolves generic metadata selectors using the namespace at each call site', () => { - writeJson('i18n/en-US/app.json', { resetPassword: 'Unused app key' }) - writeJson('i18n/en-US/login.json', { resetPassword: 'Reset password', unused: 'Unused' }) - writeJson('i18n/en-US/common.json', { title: 'Title', unused: 'Unused' }) + writeJson('i18n/locales/en-US/app.json', { resetPassword: 'Unused app key' }) + writeJson('i18n/locales/en-US/login.json', { + resetPassword: 'Reset password', + unused: 'Unused', + }) + writeJson('i18n/locales/en-US/common.json', { title: 'Title', unused: 'Unused' }) writeSource( 'src/metadata.ts', ` @@ -67,8 +70,8 @@ describe('translation graph analysis', () => { }) it('keeps possible matches across namespaces when a selector namespace cannot be resolved', () => { - writeJson('i18n/en-US/app.json', { title: 'App', unused: 'Unused' }) - writeJson('i18n/en-US/login.json', { title: 'Login', unused: 'Unused' }) + writeJson('i18n/locales/en-US/app.json', { title: 'App', unused: 'Unused' }) + writeJson('i18n/locales/en-US/login.json', { title: 'Login', unused: 'Unused' }) writeSource( 'src/metadata.ts', ` @@ -87,8 +90,8 @@ describe('translation graph analysis', () => { }) it('protects every possible namespace when the namespace is dynamic', () => { - writeJson('i18n/en-US/app.json', { used: 'App' }) - writeJson('i18n/en-US/common.json', { used: 'Common' }) + writeJson('i18n/locales/en-US/app.json', { used: 'App' }) + writeJson('i18n/locales/en-US/common.json', { used: 'Common' }) writeSource( 'src/dynamic-namespace.ts', ` @@ -107,8 +110,8 @@ describe('translation graph analysis', () => { }) it('matches dynamically namespaced keys without keeping unrelated keys', () => { - writeJson('i18n/en-US/app.json', { used: 'App', unused: 'Unused' }) - writeJson('i18n/en-US/common.json', { used: 'Common', unused: 'Unused' }) + writeJson('i18n/locales/en-US/app.json', { used: 'App', unused: 'Unused' }) + writeJson('i18n/locales/en-US/common.json', { used: 'Common', unused: 'Unused' }) writeSource( 'src/dynamic-namespace.ts', ` @@ -128,7 +131,7 @@ describe('translation graph analysis', () => { it('should keep flat keys selected by t and Trans', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'account.changeEmail.title': 'Change email', 'account.changeEmail.description': 'Description', 'account.changeEmail.unused': 'Unused', @@ -165,10 +168,10 @@ describe('translation graph analysis', () => { it('should use the default namespace for unresolved selectors', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'dynamic.app': 'Dynamic app key', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'unused.common': 'Unused common key', }) writeSource( @@ -195,7 +198,7 @@ describe('translation graph analysis', () => { it('should resolve selectors stored in variables', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { members_one: '1 member', members_other: '{{count}} members', unused: 'Unused', @@ -224,7 +227,7 @@ describe('translation graph analysis', () => { it('should resolve finite selector maps with computed keys and optional entries', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { first: 'First', second: 'Second', unused: 'Unused', @@ -265,7 +268,7 @@ describe('translation graph analysis', () => { it('should protect a selector map namespace when any candidate is unresolved', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { hidden: 'Potentially used by the dynamic selector', used: 'Used', }) @@ -296,7 +299,7 @@ describe('translation graph analysis', () => { it('should protect known selector properties that dynamic entries can override', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { hidden: 'Potentially used by the dynamic selector', used: 'Used', }) @@ -336,7 +339,7 @@ describe('translation graph analysis', () => { it('should resolve statically computed selector map properties', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { unused: 'Unused', used: 'Used', }) @@ -369,7 +372,7 @@ describe('translation graph analysis', () => { it('should ignore dotted literals passed to unrelated generic functions', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { used: 'Used', unused: 'Unused', }) @@ -399,7 +402,7 @@ describe('translation graph analysis', () => { it('should analyze translation adapter consumers without protecting their namespace', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { used: 'Used', unused: 'Unused', }) @@ -435,10 +438,10 @@ describe('translation graph analysis', () => { it('should infer the namespace of a typed destructured translation parameter', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app key', }) - writeJson('i18n/en-US/deployments.json', { + writeJson('i18n/locales/en-US/deployments.json', { unused: 'Unused deployment key', 'versions.deployTo': 'Deploy to {{name}}', }) @@ -471,11 +474,11 @@ describe('translation graph analysis', () => { it('should prefer checker namespaces for a translation parameter named t', () => { // Arrange - writeJson('i18n/en-US/agent-v-2.json', { + writeJson('i18n/locales/en-US/agent-v-2.json', { 'agentDetail.used': 'Used agent key', 'agentDetail.unused': 'Unused agent key', }) - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app key', }) writeSource( @@ -506,12 +509,12 @@ describe('translation graph analysis', () => { it('should infer branded TFunction namespaces for direct and destructured parameters', () => { // Arrange - writeJson('i18n/en-US/agent-v-2.json', { + writeJson('i18n/locales/en-US/agent-v-2.json', { 'agentDetail.direct': 'Direct use', 'agentDetail.destructured': 'Destructured use', 'agentDetail.unused': 'Unused agent key', }) - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app key', }) writeSource( @@ -545,11 +548,11 @@ describe('translation graph analysis', () => { it('should ignore typed selector forwarding inside an adapter block body', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { unused: 'Unused', used: 'Used', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'unused.common': 'Unused common key', }) writeSource( @@ -590,7 +593,7 @@ describe('translation graph analysis', () => { it('should ignore a named adapter forwarding its typed selector parameter', () => { // Arrange - writeJson('i18n/en-US/workflow.json', { + writeJson('i18n/locales/en-US/workflow.json', { unused: 'Unused', used: 'Used', }) @@ -632,7 +635,7 @@ describe('translation graph analysis', () => { it('should resolve a selected field from nested selector map entries', () => { // Arrange - writeJson('i18n/en-US/plugin.json', { + writeJson('i18n/locales/en-US/plugin.json', { 'source.first': 'First source', 'source.second': 'Second source', unused: 'Unused', @@ -686,7 +689,7 @@ describe('translation graph analysis', () => { it('should conservatively protect untyped JavaScript translation adapters', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { hidden: 'Potentially used', used: 'Used', }) @@ -713,10 +716,10 @@ describe('translation graph analysis', () => { it('should protect the selected namespace for an open string-key adapter', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/permission-keys.json', { + writeJson('i18n/locales/en-US/permission-keys.json', { 'server.permission': 'Server permission', }) writeSource( @@ -744,7 +747,7 @@ describe('translation graph analysis', () => { it('should keep keys matching a dynamic selector pattern', () => { // Arrange - writeJson('i18n/en-US/plugin.json', { + writeJson('i18n/locales/en-US/plugin.json', { 'voice.language.enUS': 'English', 'voice.language.zhCN': 'Chinese', unrelated: 'Unrelated', @@ -772,7 +775,7 @@ describe('translation graph analysis', () => { it('should keep selector keys from a typed union', () => { // Arrange - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'status.ready': 'Ready', 'status.failed': 'Failed', 'status.unused': 'Unused', @@ -802,10 +805,10 @@ describe('translation graph analysis', () => { it('should keep selector keys from secondary namespaces', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'operation.close': 'Close', 'unused.common': 'Unused common', }) @@ -833,10 +836,10 @@ describe('translation graph analysis', () => { it('should keep property and element access selectors from secondary namespaces', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { close: 'Close', confirm: 'Confirm', unused: 'Unused', @@ -867,10 +870,10 @@ describe('translation graph analysis', () => { it('should only protect the selected namespace for an unresolved selector', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'maybe.used': 'Maybe used', }) writeSource( @@ -897,13 +900,13 @@ describe('translation graph analysis', () => { it('should keep literal keys, aliased t functions, ns options, namespace separators, and Trans keys', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'literal.title': 'Title', withDefault: 'With default', 'trans.shared': 'Shared app', 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'operation.close': 'Close', 'trans.shared': 'Shared common', 'unused.common': 'Unused common', @@ -944,7 +947,7 @@ describe('translation graph analysis', () => { it('should expand resolvable dynamic keys and keep matching prefixes for unresolved dynamic keys', () => { // Arrange - writeJson('i18n/en-US/plugin.json', { + writeJson('i18n/locales/en-US/plugin.json', { 'notice.fullMessage': 'Full message', 'notice.reason.bad': 'Bad reason', 'notice.reason.legacy': 'Legacy reason', @@ -981,7 +984,7 @@ describe('translation graph analysis', () => { it('should protect an entire namespace when a dynamic key has no static prefix', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'maybe.used': 'Maybe used', 'otherwise.unused': 'Otherwise unused', }) @@ -1007,7 +1010,7 @@ describe('translation graph analysis', () => { it('should keep typed key prefixes without protecting the whole namespace', () => { // Arrange - writeJson('i18n/en-US/app-debug.json', { + writeJson('i18n/locales/en-US/app-debug.json', { 'duplicateError.name': 'Name', 'duplicateError.value': 'Value', 'outside.unused': 'Outside', @@ -1037,7 +1040,7 @@ describe('translation graph analysis', () => { it('should expand object map values when indexed with a dynamic key', () => { // Arrange - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'status.ready': 'Ready', 'status.failed': 'Failed', 'status.unused': 'Unused', @@ -1070,7 +1073,7 @@ describe('translation graph analysis', () => { it('should treat simple i18n key identity helpers as their literal argument', () => { // Arrange - writeJson('i18n/en-US/common.json', { + writeJson('i18n/locales/en-US/common.json', { 'mainNav.workspace.searchPlaceholder': 'Search', 'mainNav.workspace.unused': 'Unused', }) @@ -1099,7 +1102,7 @@ describe('translation graph analysis', () => { it('should keep i18next plural variants when the base key is referenced', () => { // Arrange - writeJson('i18n/en-US/deployments.json', { + writeJson('i18n/locales/en-US/deployments.json', { 'overview.environments_one': '1 environment', 'overview.environments_other': '{{count}} environments', 'overview.unused_one': '1 unused', @@ -1128,7 +1131,7 @@ describe('translation graph analysis', () => { it('should infer plural namespaces from typed TFunction parameters', () => { // Arrange - writeJson('i18n/en-US/deployments.json', { + writeJson('i18n/locales/en-US/deployments.json', { 'overview.chip.behind_one': '1 release behind', 'overview.chip.behind_other': '{{count}} releases behind', 'overview.chip.unused_one': '1 unused', @@ -1156,7 +1159,7 @@ describe('translation graph analysis', () => { it('should keep literals when conditional i18n key types expand into unions', () => { // Arrange - writeJson('i18n/en-US/agent-v-2.json', { + writeJson('i18n/locales/en-US/agent-v-2.json', { 'agentDetail.configure.tools.credential.authOne': 'Auth 1', 'agentDetail.configure.tools.unused': 'Unused', }) @@ -1198,7 +1201,7 @@ describe('translation graph analysis', () => { it('should collect keys from i18next instance t calls', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { 'gotoAnything.actions.createChatflow': 'Chatflow', 'gotoAnything.actions.createChatflowDesc': 'Create a chatflow', 'gotoAnything.actions.unused': 'Unused', @@ -1229,16 +1232,16 @@ describe('translation graph analysis', () => { it('should collect keys from imported and parameterized t functions', () => { // Arrange - writeJson('i18n/en-US/app.json', { + writeJson('i18n/locales/en-US/app.json', { noAccessPermission: 'No access', 'typeSelector.chatbot': 'Chatbot', 'unused.app': 'Unused app', }) - writeJson('i18n/en-US/app-api.json', { + writeJson('i18n/locales/en-US/app-api.json', { pause: 'Pause', 'unused.api': 'Unused API', }) - writeJson('i18n/en-US/tools.json', { + writeJson('i18n/locales/en-US/tools.json', { 'mcp.server.publishTip': 'Publish first', 'unused.tools': 'Unused tools', }) diff --git a/web/plugins/vite/__tests__/i18n-prune.spec.ts b/web/plugins/vite/__tests__/i18n-prune.spec.ts index c7bb3252d04..461eadec89b 100644 --- a/web/plugins/vite/__tests__/i18n-prune.spec.ts +++ b/web/plugins/vite/__tests__/i18n-prune.spec.ts @@ -12,8 +12,8 @@ describe('i18n build check', () => { beforeEach(() => { root = realpathSync(mkdtempSync(path.join(tmpdir(), 'dify-i18n-build-'))) - mkdirSync(path.join(root, 'i18n/en-US'), { recursive: true }) - localeFile = path.join(root, 'i18n/en-US/app.json') + mkdirSync(path.join(root, 'i18n/locales/en-US'), { recursive: true }) + localeFile = path.join(root, 'i18n/locales/en-US/app.json') writeFileSync( path.join(root, 'entry.ts'), ` diff --git a/web/plugins/vite/custom-i18n-hmr.ts b/web/plugins/vite/custom-i18n-hmr.ts index 701a187e9b1..db1c77c1280 100644 --- a/web/plugins/vite/custom-i18n-hmr.ts +++ b/web/plugins/vite/custom-i18n-hmr.ts @@ -11,7 +11,7 @@ export const customI18nHmrPlugin = ({ injectTarget }: CustomI18nHmrPluginOptions const i18nHmrClientSnippet = `/* ${i18nHmrClientMarker} */ if (import.meta.hot) { const getI18nUpdateTarget = (file) => { - const match = file.match(/[/\\\\]i18n[/\\\\]([^/\\\\]+)[/\\\\]([^/\\\\]+)\\.json$/) + const match = file.match(/[/\\\\]i18n[/\\\\]locales[/\\\\]([^/\\\\]+)[/\\\\]([^/\\\\]+)\\.json$/) if (!match) return null const [, locale, namespaceFile] = match @@ -53,7 +53,7 @@ if (import.meta.hot) { name: 'custom-i18n-hmr', apply: 'serve', handleHotUpdate({ file, server }) { - if (file.endsWith('.json') && file.includes('/i18n/')) { + if (file.endsWith('.json') && file.includes('/i18n/locales/')) { server.ws.send({ type: 'custom', event: 'i18n-update', diff --git a/web/plugins/vite/i18n-prune.ts b/web/plugins/vite/i18n-prune.ts index 34b7f277d18..ccf8c722d80 100644 --- a/web/plugins/vite/i18n-prune.ts +++ b/web/plugins/vite/i18n-prune.ts @@ -43,7 +43,7 @@ export function i18nPrunePlugin(): Plugin[] { 'This report uses static analysis of the current build module graph and may contain false positives or miss unused keys.', 'Review actual usage before removing any keys.', ...keys.map((key) => ` - ${key}`), - 'Remove confirmed unused keys from web/i18n/ locale files or restore their application usage.', + 'Remove confirmed unused keys from web/i18n/locales/ locale files or restore their application usage.', ...(protectedNamespaces.size ? [ `Dynamic keys protect these namespaces: ${[...protectedNamespaces].sort().join(', ')}`, diff --git a/web/plugins/vite/i18n-prune/graph.ts b/web/plugins/vite/i18n-prune/graph.ts index c11fb915e4d..1069bd3fbce 100644 --- a/web/plugins/vite/i18n-prune/graph.ts +++ b/web/plugins/vite/i18n-prune/graph.ts @@ -56,7 +56,7 @@ function initializer(node: ts.Node): Value | undefined { // expressions but never contribute usage merely by existing on disk. export function checkTranslationGraph(root: string, modules: ReadonlyMap) { const catalog = new Map>() - const directory = path.join(root, 'i18n/en-US') + const directory = path.join(root, 'i18n/locales/en-US') for (const file of fs .readdirSync(directory) .filter((file) => file.endsWith('.json')) diff --git a/web/scripts/__tests__/migrate-i18n-selectors.spec.ts b/web/scripts/__tests__/migrate-i18n-selectors.spec.ts index b7c26c077b6..c1f75bb3bd9 100644 --- a/web/scripts/__tests__/migrate-i18n-selectors.spec.ts +++ b/web/scripts/__tests__/migrate-i18n-selectors.spec.ts @@ -145,7 +145,7 @@ export function translateLegacy(t: any) { it('should follow aliased translation factory bindings without trusting shadowed names', () => { // Arrange const source = `import { useTranslation as useI18n } from 'react-i18next' -import { getTranslation as loadTranslation } from '@/i18n-config/server' +import { getTranslation as loadTranslation } from '@/i18n/server' function renderUnrelated(useI18n: () => { t: (key: string) => string }) { const { t } = useI18n() @@ -164,7 +164,7 @@ export async function Example() { // Assert expect(result.output).toBe(`import { useTranslation as useI18n } from 'react-i18next' -import { getTranslation as loadTranslation } from '@/i18n-config/server' +import { getTranslation as loadTranslation } from '@/i18n/server' function renderUnrelated(useI18n: () => { t: (key: string) => string }) { const { t } = useI18n() diff --git a/web/scripts/analyze-i18n-diff.ts b/web/scripts/analyze-i18n-diff.ts index 7120948c98d..fbe27cd3e07 100644 --- a/web/scripts/analyze-i18n-diff.ts +++ b/web/scripts/analyze-i18n-diff.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const I18N_DIR = path.join(__dirname, '../i18n/en-US') +const I18N_DIR = path.join(__dirname, '../i18n/locales/en-US') const LOCALE = 'en-US' type TranslationValue = string | string[] @@ -117,7 +117,7 @@ function parseTsContent(content: string): NestedTranslation { */ function getMainBranchFile(filePath: string): string | null { try { - const relativePath = `./i18n/${LOCALE}/${filePath}` + const relativePath = `./i18n/locales/${LOCALE}/${filePath}` return execSync(`git show main:${relativePath}`, { encoding: 'utf-8', @@ -141,7 +141,7 @@ function getTranslationFiles(): string[] { */ function getMainBranchNamespaces(): string[] { try { - const relativePath = `./i18n/${LOCALE}` + const relativePath = `./i18n/locales/${LOCALE}` const output = execSync(`git ls-tree --name-only main ${relativePath}/`, { encoding: 'utf-8', diff --git a/web/scripts/check-i18n.js b/web/scripts/check-i18n.js index 78617fb9e1b..cc206914fbd 100644 --- a/web/scripts/check-i18n.js +++ b/web/scripts/check-i18n.js @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import data from '../i18n-config/languages' +import data from '../i18n/languages' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -105,7 +105,7 @@ Examples: async function getKeysFromLanguage(language) { return new Promise((resolve, reject) => { - const folderPath = path.resolve(__dirname, '../i18n', language) + const folderPath = path.resolve(__dirname, '../i18n/locales', language) const allKeys = [] fs.readdir(folderPath, (err, files) => { if (err) { @@ -146,7 +146,7 @@ async function getKeysFromLanguage(language) { } async function removeExtraKeysFromFile(language, fileName, extraKeys) { - const filePath = path.resolve(__dirname, '../i18n', language, `${fileName}.json`) + const filePath = path.resolve(__dirname, '../i18n/locales', language, `${fileName}.json`) if (!fs.existsSync(filePath)) { console.log(`⚠️ File not found: ${filePath}`) @@ -260,7 +260,7 @@ async function main() { console.log(`\n🤖 Auto-removing extra keys from ${language}...`) // Get all translation files - const i18nFolder = path.resolve(__dirname, '../i18n', language) + const i18nFolder = path.resolve(__dirname, '../i18n/locales', language) const files = fs .readdirSync(i18nFolder) .filter((file) => file.endsWith('.json')) diff --git a/web/scripts/migrate-i18n-selectors.ts b/web/scripts/migrate-i18n-selectors.ts index a9e4639c159..0e552538f0e 100644 --- a/web/scripts/migrate-i18n-selectors.ts +++ b/web/scripts/migrate-i18n-selectors.ts @@ -27,7 +27,7 @@ const SKIPPED_DIRECTORIES = new Set([ '.vinext', 'coverage', 'dist', - 'i18n', + 'locales', 'node_modules', 'public', ]) diff --git a/web/service/access-control/use-app-access-config.ts b/web/service/access-control/use-app-access-config.ts index ee134da5ff6..41f66097974 100644 --- a/web/service/access-control/use-app-access-config.ts +++ b/web/service/access-control/use-app-access-config.ts @@ -1,4 +1,4 @@ -import type { AccessControlTemplateLanguage } from '@/i18n-config/language' +import type { AccessControlTemplateLanguage } from '@/i18n/language' import type { RemoveAppAccessPolicyMemberBindingsRequest, UpdateAppUserAccessSettingsRequest, diff --git a/web/service/access-control/use-dataset-access-config.ts b/web/service/access-control/use-dataset-access-config.ts index 2361a5fb776..b976a88ad85 100644 --- a/web/service/access-control/use-dataset-access-config.ts +++ b/web/service/access-control/use-dataset-access-config.ts @@ -1,4 +1,4 @@ -import type { AccessControlTemplateLanguage } from '@/i18n-config/language' +import type { AccessControlTemplateLanguage } from '@/i18n/language' import type { RemoveDatasetAccessPolicyMemberBindingsRequest, UpdateDatasetUserAccessSettingsRequest, diff --git a/web/service/access-control/use-member-roles.ts b/web/service/access-control/use-member-roles.ts index 8a351fc46b7..c7202da7923 100644 --- a/web/service/access-control/use-member-roles.ts +++ b/web/service/access-control/use-member-roles.ts @@ -1,4 +1,4 @@ -import type { AccessControlTemplateLanguage } from '@/i18n-config/language' +import type { AccessControlTemplateLanguage } from '@/i18n/language' import type { RolesOfMemberResponse, UpdateRolesOfMemberRequest } from '@/models/access-control' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { get, put } from '../base' diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 62483cbd829..3ae58ec4e21 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -4,7 +4,7 @@ import type { ModelProvider, ModelTypeEnum, } from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { AccessControlTemplateLanguage } from '@/i18n-config/language' +import type { AccessControlTemplateLanguage } from '@/i18n/language' import type { CodeBasedExtension, CommonResponse, diff --git a/web/types/app.ts b/web/types/app.ts index 48dda778bcb..b50027ada36 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -1,7 +1,7 @@ import type { ToolProviderType } from '@dify/contracts/api/console/agent/types.gen' import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { UploadFileSetting } from '@/app/components/workflow/types' -import type { LanguagesSupported } from '@/i18n-config/language' +import type { LanguagesSupported } from '@/i18n/language' import type { AccessMode } from '@/models/access-control' import type { ExternalDataTool } from '@/models/common' import type { RerankingModeEnum, WeightedScoreEnum } from '@/models/datasets' diff --git a/web/types/i18n.d.ts b/web/types/i18n.d.ts index dc1d0965a3e..bef0f328350 100644 --- a/web/types/i18n.d.ts +++ b/web/types/i18n.d.ts @@ -1,4 +1,4 @@ -import type { defaultNS, Namespace, Resources } from '../i18n-config/resources' +import type { defaultNS, Namespace, Resources } from '../i18n/resources' import 'i18next' declare module 'i18next' { diff --git a/web/utils/format.ts b/web/utils/format.ts index 13c94937072..80657e0f6c9 100644 --- a/web/utils/format.ts +++ b/web/utils/format.ts @@ -1,6 +1,6 @@ import type { Dayjs } from 'dayjs' -import type { Locale } from '@/i18n-config' -import { localeMap } from '@/i18n-config/language' +import type { Locale } from '@/i18n' +import { localeMap } from '@/i18n/language' import 'dayjs/locale/az' import 'dayjs/locale/de' import 'dayjs/locale/es' From 104f3b239b2f22cf3ef822ff3c7dfc990c896790 Mon Sep 17 00:00:00 2001 From: Mesrop Harutyunyan <104313591+mesrop2008@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:10:25 +0000 Subject: [PATCH 15/21] refactor(api): merge duplicated model builders in unit tests (#41549) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/services/file_service.py | 6 +- .../rag_pipeline_transform_service.py | 31 +- .../commands/test_reset_encrypt_key_pair.py | 5 +- .../console/agent/test_agent_controllers.py | 14 +- .../console/agent/test_app_helpers.py | 3 +- .../console/app/test_agent_app_sandbox.py | 5 +- .../console/app/test_annotation_api.py | 5 +- .../console/app/test_annotation_security.py | 5 +- .../controllers/console/app/test_audio.py | 16 +- .../console/app/test_conversation_api.py | 29 +- .../app/test_conversation_variables_api.py | 16 +- .../console/app/test_generator_api.py | 12 +- .../console/app/test_mcp_server_response.py | 17 +- .../console/app/test_workflow_convert_api.py | 18 +- .../console/app/test_workflow_run_api.py | 19 +- .../rag_pipeline/test_datasource_auth.py | 5 +- .../rag_pipeline/test_rag_pipeline.py | 5 +- .../test_rag_pipeline_datasets.py | 10 +- .../test_rag_pipeline_draft_variable.py | 10 +- .../rag_pipeline/test_rag_pipeline_import.py | 5 +- .../test_rag_pipeline_workflow.py | 22 +- .../datasets/test_data_source_notion_apis.py | 13 +- .../datasets/test_datasets_segments.py | 29 +- .../controllers/console/explore/test_trial.py | 20 +- .../controllers/console/explore/test_wraps.py | 18 +- .../console/snippets/test_snippet_workflow.py | 11 +- .../controllers/console/tag/test_tags.py | 12 +- .../controllers/console/test_files.py | 16 +- .../console/test_human_input_form.py | 13 +- .../console/test_workflow_run_archive.py | 6 +- .../console/test_workspace_members.py | 5 +- .../console/workspace/test_agent_providers.py | 5 +- .../workspace/test_load_balancing_config.py | 6 +- .../console/workspace/test_models.py | 5 +- .../console/workspace/test_plugin.py | 6 +- .../console/workspace/test_rbac.py | 5 +- .../console/workspace/test_snippets.py | 11 +- .../inner_api/plugin/test_plugin.py | 5 +- .../controllers/inner_api/test_agent_files.py | 5 +- .../controllers/inner_api/test_agent_llm.py | 12 +- .../unit_tests/controllers/mcp/test_mcp.py | 8 +- .../controllers/openapi/auth/test_prepare.py | 26 +- .../controllers/openapi/test_app_payloads.py | 17 +- .../openapi/test_workspaces_members.py | 10 +- .../controllers/service_api/app/test_audio.py | 21 +- .../service_api/app/test_conversation.py | 9 +- .../dataset/test_dataset_segment.py | 7 +- .../service_api/dataset/test_document.py | 17 +- ..._rag_pipeline_file_upload_serialization.py | 14 +- .../test_trace_session_id_parsing.py | 10 +- .../controllers/trigger/test_webhook.py | 3 +- .../unit_tests/controllers/web/test_audio.py | 12 +- .../controllers/web/test_completion.py | 27 +- .../unit_tests/controllers/web/test_files.py | 25 +- .../web/test_human_input_file_upload.py | 20 +- .../controllers/web/test_message_endpoints.py | 9 +- .../controllers/web/test_remote_files.py | 10 +- .../controllers/web/test_saved_message.py | 9 +- .../controllers/web/test_workflow.py | 9 +- .../controllers/web/test_workflow_events.py | 10 +- .../advanced_chat/test_app_config_manager.py | 15 +- .../app/apps/agent_app/test_input_guards.py | 5 +- .../app/apps/agent_app/test_resolve_agent.py | 11 +- .../app/apps/chat/test_app_config_manager.py | 3 +- .../app/apps/pipeline/test_pipeline_runner.py | 34 +- .../apps/test_message_based_app_generator.py | 13 +- .../app/apps/test_workflow_app_generator.py | 14 +- .../app/apps/test_workflow_app_runner_core.py | 11 +- .../apps/workflow/test_app_config_manager.py | 17 +- .../test_generate_task_pipeline_core.py | 25 +- .../app/features/test_annotation_reply.py | 16 +- ...sy_ui_based_generate_task_pipeline_core.py | 10 +- ...test_message_cycle_manager_optimization.py | 20 +- .../test_datasource_file_manager.py | 11 +- .../test_llm_generator_missing.py | 5 +- .../core/ops/test_ops_trace_manager.py | 16 +- api/tests/unit_tests/core/ops/test_utils.py | 8 +- .../plugin/test_backwards_invocation_app.py | 30 +- .../datasource/keyword/jieba/test_jieba.py | 7 +- .../datasource/keyword/test_keyword_base.py | 3 +- .../rag/docstore/test_dataset_docstore.py | 7 +- .../rag/extractor/test_extract_processor.py | 11 +- ...hemy_workflow_node_execution_repository.py | 21 +- ...rkflow_node_execution_conflict_handling.py | 15 +- .../tools/workflow_as_tool/test_provider.py | 23 +- .../debug/test_debug_event_selectors.py | 9 +- .../nodes/agent_v2/test_validators.py | 9 +- .../workflow/test_workflow_entry_helpers.py | 15 +- ...ault_plugin_install_when_tenant_created.py | 5 +- .../libs/test_workspace_member_helper.py | 7 +- api/tests/unit_tests/model_factories.py | 489 ++++++++++++++++++ .../unit_tests/models/test_comment_models.py | 5 +- .../agent/test_agent_observability_service.py | 23 +- .../services/agent/test_agent_services.py | 41 +- .../agent/test_workflow_publish_service.py | 16 +- .../test_rag_pipeline_dsl_service.py | 34 +- .../test_rag_pipeline_transform_service.py | 15 +- .../test_account_change_email_service.py | 17 +- .../services/test_account_deletion_service.py | 17 +- .../test_account_education_service.py | 12 +- ...test_account_email_registration_service.py | 12 +- .../test_account_initialization_service.py | 17 +- .../services/test_account_password_service.py | 18 +- .../services/test_account_profile_service.py | 18 +- .../services/test_account_service.py | 3 +- .../services/test_app_dsl_service.py | 42 +- .../unit_tests/services/test_audio_service.py | 8 +- .../services/test_billing_portal_service.py | 18 +- .../services/test_billing_service.py | 10 - .../services/test_dataset_service_dataset.py | 20 +- .../services/test_dataset_service_document.py | 23 +- .../unit_tests/services/test_file_service.py | 5 +- .../services/test_knowledge_fs_proxy.py | 6 +- .../test_knowledge_retrieval_inner_service.py | 15 +- .../test_metadata_service_session_boundary.py | 24 +- .../services/test_schedule_service.py | 16 +- .../services/test_snippet_dsl_service.py | 28 +- .../services/test_snippet_generate_service.py | 22 +- .../services/test_snippet_service.py | 5 +- .../test_step_by_step_tour_service.py | 17 +- .../services/test_vector_service.py | 14 +- .../test_vector_space_admission_service.py | 7 +- .../services/test_webhook_service.py | 27 +- .../test_workflow_collaboration_service.py | 17 +- .../services/test_workflow_comment_service.py | 11 +- .../test_workflow_human_input_delivery.py | 15 +- .../tasks/test_clean_document_task.py | 11 +- .../tasks/test_delete_conversation_task.py | 11 +- .../tasks/test_resume_agent_app_task.py | 12 +- ...est_sync_website_document_indexing_task.py | 14 +- .../tasks/test_trigger_processing_tasks.py | 39 +- 131 files changed, 1040 insertions(+), 1264 deletions(-) create mode 100644 api/tests/unit_tests/model_factories.py diff --git a/api/services/file_service.py b/api/services/file_service.py index ded70111e3d..e06069c8cc2 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -1,6 +1,7 @@ import base64 import hashlib import os +import posixpath import uuid from collections.abc import Generator, Sequence # Changed Iterator to Generator from contextlib import contextmanager, suppress @@ -315,8 +316,9 @@ class FileService: We keep this conservative: the upload flow already rejects `/` and `\\`, but older rows (or imported data) could still contain unsafe names. """ - # Drop any directory components and prevent empty names. - base = os.path.basename(name).strip() or "file" + # Drop any directory components and prevent empty names. ZIP names are always POSIX-style, so split on + # `/` explicitly rather than through `os.path`, whose separators differ on Windows hosts. + base = posixpath.basename(name).strip() or "file" # ZIP uses forward slashes as separators; remove any residual separator characters. return base.replace("/", "_").replace("\\", "_") diff --git a/api/services/rag_pipeline/rag_pipeline_transform_service.py b/api/services/rag_pipeline/rag_pipeline_transform_service.py index 90dd6e081bd..9304257de9a 100644 --- a/api/services/rag_pipeline/rag_pipeline_transform_service.py +++ b/api/services/rag_pipeline/rag_pipeline_transform_service.py @@ -123,29 +123,40 @@ class RagPipelineTransformService: case DataSourceType.UPLOAD_FILE: if indexing_technique == IndexTechniqueType.HIGH_QUALITY: # get graph from transform.file-general-high-quality.yml - with open(f"{Path(__file__).parent}/transform/file-general-high-quality.yml") as f: + with open( + f"{Path(__file__).parent}/transform/file-general-high-quality.yml", encoding="utf-8" + ) as f: pipeline_yaml = yaml.safe_load(f) if indexing_technique == IndexTechniqueType.ECONOMY: # get graph from transform.file-general-economy.yml - with open(f"{Path(__file__).parent}/transform/file-general-economy.yml") as f: + with open(f"{Path(__file__).parent}/transform/file-general-economy.yml", encoding="utf-8") as f: pipeline_yaml = yaml.safe_load(f) case DataSourceType.NOTION_IMPORT: if indexing_technique == IndexTechniqueType.HIGH_QUALITY: # get graph from transform.notion-general-high-quality.yml - with open(f"{Path(__file__).parent}/transform/notion-general-high-quality.yml") as f: + with open( + f"{Path(__file__).parent}/transform/notion-general-high-quality.yml", encoding="utf-8" + ) as f: pipeline_yaml = yaml.safe_load(f) if indexing_technique == IndexTechniqueType.ECONOMY: # get graph from transform.notion-general-economy.yml - with open(f"{Path(__file__).parent}/transform/notion-general-economy.yml") as f: + with open( + f"{Path(__file__).parent}/transform/notion-general-economy.yml", encoding="utf-8" + ) as f: pipeline_yaml = yaml.safe_load(f) case DataSourceType.WEBSITE_CRAWL: if indexing_technique == IndexTechniqueType.HIGH_QUALITY: # get graph from transform.website-crawl-general-high-quality.yml - with open(f"{Path(__file__).parent}/transform/website-crawl-general-high-quality.yml") as f: + with open( + f"{Path(__file__).parent}/transform/website-crawl-general-high-quality.yml", + encoding="utf-8", + ) as f: pipeline_yaml = yaml.safe_load(f) if indexing_technique == IndexTechniqueType.ECONOMY: # get graph from transform.website-crawl-general-economy.yml - with open(f"{Path(__file__).parent}/transform/website-crawl-general-economy.yml") as f: + with open( + f"{Path(__file__).parent}/transform/website-crawl-general-economy.yml", encoding="utf-8" + ) as f: pipeline_yaml = yaml.safe_load(f) case _: raise ValueError("Unsupported datasource type") @@ -153,15 +164,17 @@ class RagPipelineTransformService: match datasource_type: case DataSourceType.UPLOAD_FILE: # get graph from transform.file-parentchild.yml - with open(f"{Path(__file__).parent}/transform/file-parentchild.yml") as f: + with open(f"{Path(__file__).parent}/transform/file-parentchild.yml", encoding="utf-8") as f: pipeline_yaml = yaml.safe_load(f) case DataSourceType.NOTION_IMPORT: # get graph from transform.notion-parentchild.yml - with open(f"{Path(__file__).parent}/transform/notion-parentchild.yml") as f: + with open(f"{Path(__file__).parent}/transform/notion-parentchild.yml", encoding="utf-8") as f: pipeline_yaml = yaml.safe_load(f) case DataSourceType.WEBSITE_CRAWL: # get graph from transform.website-crawl-parentchild.yml - with open(f"{Path(__file__).parent}/transform/website-crawl-parentchild.yml") as f: + with open( + f"{Path(__file__).parent}/transform/website-crawl-parentchild.yml", encoding="utf-8" + ) as f: pipeline_yaml = yaml.safe_load(f) case _: raise ValueError("Unsupported datasource type") diff --git a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py index d8c47e2747e..bb3201bbdaa 100644 --- a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py +++ b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py @@ -22,6 +22,7 @@ from models import Tenant from models.provider import Provider, ProviderModel, ProviderType from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_tenant def _invoke_reset() -> int: @@ -38,9 +39,7 @@ USER_ID = "22222222-2222-2222-2222-222222222222" def _tenant(tenant_id: str, *, name: str = "Test tenant") -> Tenant: - tenant = Tenant(name=name, encrypt_public_key="old-key") - tenant.id = tenant_id - return tenant + return make_tenant(tenant_id=tenant_id, name=name, encrypt_public_key="old-key") def _encrypted_rows(tenant_id: str, *, suffix: str = "1") -> tuple[object, ...]: diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index ee10c5dc19f..d2af3b5fddc 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -75,6 +75,7 @@ from services.entities.agent_entities import ( WorkflowComposerCopyFromRosterPayload, ) from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_account def _persist_conversation_message( @@ -228,12 +229,13 @@ def _app_detail_obj(**overrides) -> App: def _account(*, account_id: str = "account-1", privileged: bool = False, timezone: str | None = None) -> Account: - account = Account(name="Agent Controller Tester", email=f"{account_id}@example.com") - account.id = account_id - account.timezone = timezone - if privileged: - account.role = TenantAccountRole.OWNER - return account + return make_account( + account_id=account_id, + name="Agent Controller Tester", + email=f"{account_id}@example.com", + timezone=timezone, + role=TenantAccountRole.OWNER if privileged else None, + ) def _candidates_response(variant: str) -> dict: diff --git a/api/tests/unit_tests/controllers/console/agent/test_app_helpers.py b/api/tests/unit_tests/controllers/console/agent/test_app_helpers.py index b797a2956ad..8ad20b6b84c 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_app_helpers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_app_helpers.py @@ -6,10 +6,11 @@ from sqlalchemy.orm import Session from controllers.console.agent import app_helpers from models.model import App, AppMode +from tests.unit_tests.model_factories import make_app def _app() -> App: - return App(id="app-1", tenant_id="tenant-1", name="Agent App", mode=AppMode.AGENT) + return make_app(name="Agent App", mode=AppMode.AGENT, icon_type=None) def test_resolve_agent_app_model_reuses_caller_session( diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py index 0ce728baef9..077b166917f 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py @@ -12,6 +12,7 @@ from models.account import Account from models.model import App, AppMode, IconType from services.agent_app_sandbox_service import AgentSandboxDownload, AgentSandboxInfo, AgentSandboxInspectorError from tests.unit_tests.controllers.rbac_introspection import rbac_checks +from tests.unit_tests.model_factories import make_account class _AgentAppService: @@ -142,9 +143,7 @@ def _app_model(app_id: str = "app-1") -> App: def _account() -> Account: - account = Account(name="Sandbox Tester", email="sandbox-tester@example.com") - account.id = "account-1" - return account + return make_account(name="Sandbox Tester", email="sandbox-tester@example.com") @pytest.mark.parametrize( diff --git a/api/tests/unit_tests/controllers/console/app/test_annotation_api.py b/api/tests/unit_tests/controllers/console/app/test_annotation_api.py index 38ae25284e1..5094bb1fab9 100644 --- a/api/tests/unit_tests/controllers/console/app/test_annotation_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_annotation_api.py @@ -13,6 +13,7 @@ from controllers.console.app import annotation as annotation_module from models.account import Account from models.model import App, AppAnnotationHitHistory, AppMode, IconType, MessageAnnotation from services.app_ref_service import AnnotationRef, AppRef +from tests.unit_tests.model_factories import make_account def _persist_app(session: Session) -> App: @@ -33,9 +34,7 @@ def _persist_app(session: Session) -> App: def _account() -> Account: - account = Account(name="Owner", email="owner@example.com") - account.id = "account-1" - return account + return make_account(name="Owner", email="owner@example.com") def _annotation_model(annotation_id: str = "ann-1") -> MessageAnnotation: diff --git a/api/tests/unit_tests/controllers/console/app/test_annotation_security.py b/api/tests/unit_tests/controllers/console/app/test_annotation_security.py index 0aa896337e3..9d0f6c33cc5 100644 --- a/api/tests/unit_tests/controllers/console/app/test_annotation_security.py +++ b/api/tests/unit_tests/controllers/console/app/test_annotation_security.py @@ -21,12 +21,11 @@ from models.model import App, AppMode, IconType from services.annotation_service import AppAnnotationService from tasks.annotation.batch_import_annotations_task import batch_import_annotations_task from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account def _account() -> Account: - account = Account(name="Annotation Tester", email="annotation-tester@example.com") - account.id = "user_id" - return account + return make_account(account_id="user_id", name="Annotation Tester", email="annotation-tester@example.com") def _persist_app(session: Session) -> App: diff --git a/api/tests/unit_tests/controllers/console/app/test_audio.py b/api/tests/unit_tests/controllers/console/app/test_audio.py index 1f8633923be..6e3efa6c985 100644 --- a/api/tests/unit_tests/controllers/console/app/test_audio.py +++ b/api/tests/unit_tests/controllers/console/app/test_audio.py @@ -50,6 +50,7 @@ from services.errors.audio import ( SpeechToTextDisabledServiceError, UnsupportedAudioTypeServiceError, ) +from tests.unit_tests.model_factories import make_account, make_app def _file_data(): @@ -57,22 +58,11 @@ def _file_data(): def _app(*, app_id: str = "a1", tenant_id: str = "tenant-1") -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name="Audio app", - description="", - mode=AppMode.CHAT, - enable_site=True, - enable_api=True, - max_active_requests=0, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, name="Audio app", icon_type=None, max_active_requests=0) def _account(account_id: str = "account-1") -> Account: - account = Account(name="Audio account", email=f"{account_id}@example.com") - account.id = account_id - return account + return make_account(account_id=account_id, name="Audio account", email=f"{account_id}@example.com") def test_console_audio_api_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/controllers/console/app/test_conversation_api.py b/api/tests/unit_tests/controllers/console/app/test_conversation_api.py index c43af0fde83..8425c9560de 100644 --- a/api/tests/unit_tests/controllers/console/app/test_conversation_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_conversation_api.py @@ -23,9 +23,10 @@ from models.enums import ( FeedbackRating, WorkflowRunTriggeredFrom, ) -from models.model import AppMode, Conversation, IconType +from models.model import AppMode, Conversation from models.workflow import WorkflowRun, WorkflowType from services.errors.conversation import ConversationNotExistsError +from tests.unit_tests.model_factories import make_app, make_conversation def _make_account() -> Account: @@ -35,30 +36,13 @@ def _make_account() -> Account: def _app(*, mode: AppMode = AppMode.CHAT) -> App: - return App( - id="app-1", - tenant_id="tenant-1", - name="Conversation app", - description="", - mode=mode, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, - max_active_requests=None, - ) + return make_app(name="Conversation app", mode=mode) def _conversation(*, conversation_id: str = "c1", app_id: str = "app-1") -> Conversation: - conversation = Conversation( + return make_conversation( + conversation_id=conversation_id, app_id=app_id, - app_model_config_id=None, - model_provider=None, - override_model_configs=None, - model_id=None, - mode=AppMode.CHAT, - name="Conversation", inputs={}, introduction="", system_instruction="", @@ -66,11 +50,8 @@ def _conversation(*, conversation_id: str = "c1", app_id: str = "app-1") -> Conv status="normal", invoke_from=InvokeFrom.EXPLORE, from_source=ConversationFromSource.CONSOLE, - from_end_user_id=None, from_account_id="u1", ) - conversation.id = conversation_id - return conversation def test_completion_conversation_list_returns_paginated_result( diff --git a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py index 07249da37c8..bfd030b2e7a 100644 --- a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py @@ -14,23 +14,11 @@ from controllers.console.app import conversation_variables as conversation_varia from factories import variable_factory from graphon.variables.types import SegmentType from models import App, AppMode, ConversationVariable -from models.model import IconType +from tests.unit_tests.model_factories import make_app def _app() -> App: - return App( - id="app-1", - tenant_id="tenant-1", - name="Conversation variables app", - description="", - mode=AppMode.ADVANCED_CHAT, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, - max_active_requests=None, - ) + return make_app(name="Conversation variables app", mode=AppMode.ADVANCED_CHAT) def test_get_conversation_variables_returns_paginated_response( diff --git a/api/tests/unit_tests/controllers/console/app/test_generator_api.py b/api/tests/unit_tests/controllers/console/app/test_generator_api.py index b028bd15761..184f0a3f988 100644 --- a/api/tests/unit_tests/controllers/console/app/test_generator_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_generator_api.py @@ -20,7 +20,8 @@ from controllers.console.app.generator import ( ) from core.errors.error import ProviderTokenNotInitError from models.model import App, AppMode -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow +from tests.unit_tests.model_factories import make_workflow def _persist_app(session: Session, *, tenant_id: str = "t1") -> App: @@ -44,14 +45,7 @@ def _model_config_payload(): def _workflow(graph_dict: dict[str, object]) -> Workflow: - return Workflow( - id="workflow-1", - tenant_id="t1", - app_id="app-1", - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph=json.dumps(graph_dict), - ) + return make_workflow(workflow_id="workflow-1", tenant_id="t1", graph=graph_dict) def _install_workflow_service(monkeypatch: pytest.MonkeyPatch, workflow): diff --git a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py index 0f53d0de7d8..edac7a463c0 100644 --- a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py +++ b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py @@ -20,8 +20,9 @@ from controllers.console.wraps import RBACPermission, RBACResourceScope from models import Account from models.account import AccountStatus from models.enums import AppMCPServerStatus -from models.model import App, AppMCPServer, AppMode, IconType +from models.model import App, AppMCPServer from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_app def _app( @@ -31,19 +32,7 @@ def _app( name: str = "Demo App", description: str = "App description", ) -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name=name, - description=description, - mode=AppMode.CHAT, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, - max_active_requests=None, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, name=name, description=description) def _server( diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_convert_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_convert_api.py index 54163d65448..f9a9bc80d06 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_convert_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_convert_api.py @@ -9,24 +9,12 @@ from flask import Flask from controllers.console.app import workflow as workflow_module from controllers.console.app.workflow import ConvertToWorkflowApi -from models import Account, App, AppMode -from models.model import IconType +from models import Account, App +from tests.unit_tests.model_factories import make_app def _app(app_id: str) -> App: - return App( - id=app_id, - tenant_id="tenant-1", - name=f"App {app_id}", - description="", - mode=AppMode.CHAT, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, - max_active_requests=None, - ) + return make_app(app_id=app_id, name=f"App {app_id}") class TestConvertToWorkflowApi: diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py index 16c85e24f62..7457dae7d80 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_run_api.py @@ -20,13 +20,13 @@ from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus from machinery.context import RequestContext from models import Account, App, AppMode from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom -from models.model import IconType from models.workflow import ( WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom, WorkflowRun, WorkflowType, ) +from tests.unit_tests.model_factories import make_account, make_app def _serialize_200_response(handler, payload: Any) -> Any: @@ -41,27 +41,14 @@ def _serialize_200_response(handler, payload: Any) -> Any: def _account(session: Session) -> Account: - account = Account(name="Alice", email="alice@example.com") - account.id = "account-1" + account = make_account(name="Alice", email="alice@example.com") session.add(account) session.commit() return account def _app() -> App: - return App( - id="app-1", - tenant_id="tenant-1", - name="Workflow run app", - description="", - mode=AppMode.WORKFLOW, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, - max_active_requests=None, - ) + return make_app(name="Workflow run app", mode=AppMode.WORKFLOW) def _request_context(*, workspace_id: str = "tenant-1") -> RequestContext: diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py index 63a61586ef9..c1cbbaa8682 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py @@ -30,14 +30,13 @@ from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from models.account import Account from services.datasource_provider_service import DatasourceProviderService from services.plugin.oauth_service import OAuthProxyService +from tests.unit_tests.model_factories import make_account _PROVIDER_ID = "langgenius/notion_datasource/notion" def _account() -> Account: - account = Account(name="Datasource Auth Tester", email="datasource-auth@example.com") - account.id = "user-1" - return account + return make_account(account_id="user-1", name="Datasource Auth Tester", email="datasource-auth@example.com") def _i18n(text: str) -> dict[str, str]: diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py index 5ef83ea902f..53445379c5c 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py @@ -29,6 +29,7 @@ from services.errors.account import NoPermissionError from services.errors.rag_pipeline import RagPipelineResourceNotFoundError from tests.unit_tests.config_override import config_overrides_context from tests.unit_tests.controllers.rbac_introspection import rbac_checks +from tests.unit_tests.model_factories import make_account def _template_item() -> dict[str, object]: @@ -63,9 +64,7 @@ def _payload() -> dict[str, object]: def _account() -> Account: - account = Account(name="Test User", email="test@example.com") - account.id = "account-1" - return account + return make_account(name="Test User", email="test@example.com") def _pipeline() -> Pipeline: diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py index 3e2c9cefd04..a892ad820fb 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py @@ -19,12 +19,16 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_datasets import ( ) from models.account import Account, TenantAccountRole from services.entities.dsl_entities import ImportStatus +from tests.unit_tests.model_factories import make_account def _account(*, editor: bool) -> Account: - account = Account(name="RAG Pipeline Tester", email="rag-pipeline@example.com") - account.role = TenantAccountRole.EDITOR if editor else TenantAccountRole.NORMAL - return account + return make_account( + account_id=None, + name="RAG Pipeline Tester", + email="rag-pipeline@example.com", + role=TenantAccountRole.EDITOR if editor else TenantAccountRole.NORMAL, + ) class TestCreateRagPipelineDatasetApi: diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py index 33ff6a39a81..c5f43180cee 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py @@ -32,6 +32,7 @@ from models.account import Account, TenantAccountRole from models.dataset import Pipeline from models.workflow import Workflow, WorkflowDraftVariable, WorkflowType from tests.unit_tests.controllers.rbac_introspection import rbac_checks +from tests.unit_tests.model_factories import make_workflow @pytest.fixture @@ -48,17 +49,12 @@ def _pipeline(*, pipeline_id: str = "p1", tenant_id: str = "t1") -> Pipeline: def _workflow(pipeline: Pipeline, environment_variables=()) -> Workflow: - return Workflow.new( + return make_workflow( tenant_id=pipeline.tenant_id, app_id=pipeline.id, - type=WorkflowType.RAG_PIPELINE, - version=Workflow.VERSION_DRAFT, + workflow_type=WorkflowType.RAG_PIPELINE, graph="{}", - features="{}", - created_by="account-1", environment_variables=environment_variables, - conversation_variables=[], - rag_pipeline_variables=[], ) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py index c2e80c24bd5..9875ef27c1b 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py @@ -24,12 +24,11 @@ from models.dataset import Pipeline from models.engine import db from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineImportInfo +from tests.unit_tests.model_factories import make_account def _account() -> Account: - account = Account(name="RAG Import Tester", email="rag-import@example.com") - account.id = "account-1" - return account + return make_account(name="RAG Import Tester", email="rag-import@example.com") @pytest.fixture diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py index f6ab01e9cce..294e0c04f35 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py @@ -25,7 +25,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import ( WorkflowUpdatePayload, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError -from models.account import Account, Tenant, TenantAccountRole +from models.account import Account, TenantAccountRole from models.dataset import Dataset, Pipeline from models.engine import db from models.enums import PermissionEnum @@ -35,6 +35,7 @@ from services.errors.llm import InvokeRateLimitError from services.errors.rag_pipeline import RagPipelineResourceNotFoundError from services.rag_pipeline.rag_pipeline import RagPipelineService from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account, make_dataset, make_tenant DEFAULT_WORKFLOW_TENANT_ID = "00000000-0000-0000-0000-000000000001" DEFAULT_WORKFLOW_APP_ID = "00000000-0000-0000-0000-000000000002" @@ -68,13 +69,13 @@ def _make_workflow(**overrides: object) -> Workflow: def _account() -> Account: - account = Account(name="Alice", email="alice@example.com") - account.id = DEFAULT_WORKFLOW_CREATED_BY - account.role = TenantAccountRole.EDITOR - tenant = Tenant(name="Tenant") - tenant.id = DEFAULT_WORKFLOW_TENANT_ID - account._current_tenant = tenant - return account + return make_account( + account_id=DEFAULT_WORKFLOW_CREATED_BY, + name="Alice", + email="alice@example.com", + role=TenantAccountRole.EDITOR, + tenant=make_tenant(tenant_id=DEFAULT_WORKFLOW_TENANT_ID, name="Tenant"), + ) def _pipeline() -> Pipeline: @@ -84,10 +85,9 @@ def _pipeline() -> Pipeline: def _dataset(*, tenant_id: str = DEFAULT_WORKFLOW_TENANT_ID, maintainer: str = DEFAULT_WORKFLOW_CREATED_BY) -> Dataset: - return Dataset( - id=DEFAULT_DATASET_ID, + return make_dataset( + dataset_id=DEFAULT_DATASET_ID, tenant_id=tenant_id, - name="Dataset", created_by=maintainer, maintainer=maintainer, permission=PermissionEnum.ONLY_ME, diff --git a/api/tests/unit_tests/controllers/console/datasets/test_data_source_notion_apis.py b/api/tests/unit_tests/controllers/console/datasets/test_data_source_notion_apis.py index b96c09334b6..a0a7e37b3e3 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_data_source_notion_apis.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_data_source_notion_apis.py @@ -20,23 +20,20 @@ from controllers.console.datasets.data_source import ( ) from core.rag.index_processor.constant.index_type import IndexStructureType from models import Account, Dataset, Document -from models.dataset import DataSourceType, DocumentCreatedFrom +from models.dataset import DataSourceType +from tests.unit_tests.model_factories import make_dataset, make_document def _dataset() -> Dataset: - return Dataset(id="ds-1", tenant_id="tenant-1", name="Dataset", created_by="u1") + return make_dataset(dataset_id="ds-1", created_by="u1") def _document() -> Document: - return Document( - id="d1", - tenant_id="tenant-1", + return make_document( + document_id="d1", dataset_id="ds-1", - position=1, data_source_type=DataSourceType.NOTION_IMPORT, - batch="batch-1", name="Notion page", - created_from=DocumentCreatedFrom.WEB, created_by="u1", ) diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py index 9b258aec8bc..00e4737228d 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py @@ -37,6 +37,7 @@ from models.enums import PermissionEnum, SegmentStatus, SegmentType from models.model import UploadFile from services.errors.chunk import ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError from services.errors.chunk import ChildChunkIndexingError as ChildChunkIndexingServiceError +from tests.unit_tests.model_factories import make_account, make_dataset, make_document, make_upload_file def _segment(): @@ -82,10 +83,9 @@ def _child_chunk(): def _account() -> Account: - account = Account(name="Dataset Editor", email="dataset-editor@example.com") - account.id = "u1" - account.role = TenantAccountRole.OWNER - return account + return make_account( + account_id="u1", name="Dataset Editor", email="dataset-editor@example.com", role=TenantAccountRole.OWNER + ) def _dataset( @@ -96,10 +96,9 @@ def _dataset( embedding_model_provider: str | None = None, embedding_model: str | None = None, ) -> Dataset: - return Dataset( - id=dataset_id, + return make_dataset( + dataset_id=dataset_id, tenant_id=tenant_id, - name="Dataset", description="", provider="vendor", permission=PermissionEnum.ONLY_ME, @@ -117,14 +116,10 @@ def _document( tenant_id: str = "tenant-1", doc_form: IndexStructureType = IndexStructureType.PARAGRAPH_INDEX, ) -> Document: - return Document( - id=document_id, + return make_document( + document_id=document_id, tenant_id=tenant_id, dataset_id=dataset_id, - position=1, - data_source_type="upload_file", - batch="batch-1", - name="Document", created_from="api", created_by="u1", doc_form=doc_form, @@ -132,21 +127,17 @@ def _document( def _upload_file(*, name: str = "test.csv", file_id: str = "test-file-id") -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", + return make_upload_file( + file_id=file_id, storage_type="opendal", key="test-key", name=name, size=0, extension=name.rsplit(".", maxsplit=1)[-1], mime_type="text/csv", - created_by_role="account", created_by="u1", created_at=datetime.now(), - used=False, ) - upload_file.id = file_id - return upload_file class SQLiteControllerTest: diff --git a/api/tests/unit_tests/controllers/console/explore/test_trial.py b/api/tests/unit_tests/controllers/console/explore/test_trial.py index 03d93348382..dc7d3ac12c3 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_trial.py +++ b/api/tests/unit_tests/controllers/console/explore/test_trial.py @@ -49,6 +49,7 @@ from services.app_ref_service import AppRef, MessageRef from services.errors.audio import SpeechToTextDisabledServiceError from services.errors.conversation import ConversationNotExistsError from services.errors.llm import InvokeRateLimitError +from tests.unit_tests.model_factories import make_app, make_upload_file unwrap: Any = inspect_unwrap @@ -80,32 +81,19 @@ def trial_app_usage(monkeypatch: pytest.MonkeyPatch) -> MagicMock: def _app(*, app_id: str, mode: AppMode, tenant_id: str = "tenant-1") -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name="Trial App", - mode=mode, - enable_site=True, - enable_api=False, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, name="Trial App", mode=mode, icon_type=None, enable_api=False) def _upload_file(*, file_id: str = "upload-file-id", tenant_id: str = "app-tenant-id") -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id=tenant_id, storage_type="opendal", key="trial/file.txt", name="file.txt", size=1, - extension="txt", - mime_type="text/plain", - created_by_role="account", created_by="u1", - created_at=datetime(2024, 1, 1), - used=False, ) - upload_file.id = file_id - return upload_file def _file_data() -> Any: diff --git a/api/tests/unit_tests/controllers/console/explore/test_wraps.py b/api/tests/unit_tests/controllers/console/explore/test_wraps.py index 53f87e22c5f..c3f104de1a6 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_wraps.py +++ b/api/tests/unit_tests/controllers/console/explore/test_wraps.py @@ -21,7 +21,8 @@ from controllers.console.explore.wraps import ( trial_feature_enable, user_allowed_to_access_app, ) -from models import Account, AccountTrialAppRecord, App, AppMode, InstalledApp, TrialApp +from models import Account, AccountTrialAppRecord, App, InstalledApp, TrialApp +from tests.unit_tests.model_factories import make_account, make_app def _bind_database(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: @@ -31,22 +32,11 @@ def _bind_database(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> def _account(*, account_id: str | None = None) -> Account: - account = Account(name="Explore user", email="user@example.com") - if account_id is not None: - account.id = account_id - return account + return make_account(account_id=account_id, name="Explore user", email="user@example.com") def _app() -> App: - app = App( - tenant_id=str(uuid4()), - name="Explore App", - mode=AppMode.CHAT, - enable_site=True, - enable_api=True, - ) - app.id = str(uuid4()) - return app + return make_app(app_id=str(uuid4()), tenant_id=str(uuid4()), name="Explore App", icon_type=None) def _installed_app(*, app_id: str, tenant_id: str) -> InstalledApp: diff --git a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py index 733ab1fb048..6d762791958 100644 --- a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py +++ b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow.py @@ -15,13 +15,16 @@ from werkzeug.exceptions import HTTPException, NotFound from controllers.console.snippets import snippet_workflow as snippet_workflow_module from models.account import Account, TenantAccountRole from models.snippet import CustomizedSnippet +from tests.unit_tests.model_factories import make_account def _account(account_id: str = "account-1") -> Account: - account = Account(name="Test User", email=f"{account_id}@example.com") - account.id = account_id - account.role = TenantAccountRole.EDITOR - return account + return make_account( + account_id=account_id, + name="Test User", + email=f"{account_id}@example.com", + role=TenantAccountRole.EDITOR, + ) def _snippet(**overrides) -> CustomizedSnippet: diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py index d40b84bd4dd..41aa923a471 100644 --- a/api/tests/unit_tests/controllers/console/tag/test_tags.py +++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py @@ -20,7 +20,7 @@ from controllers.console.tag.tags import ( ) from machinery.context import RequestContext from models import Account -from models.account import AccountStatus, TenantAccountRole +from models.account import TenantAccountRole from models.enums import TagType from services.tag_application_service import ( TagApplicationError, @@ -32,6 +32,7 @@ from services.tag_application_service import ( UpdateTagInput, ) from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account def unwrap(func): @@ -58,14 +59,7 @@ def request_context() -> RequestContext: def _account(role: TenantAccountRole) -> Account: - account = Account( - name="Tag User", - email=f"{role.value}@example.com", - status=AccountStatus.ACTIVE, - ) - account.id = "user-1" - account.role = role - return account + return make_account(account_id="user-1", name="Tag User", email=f"{role.value}@example.com", role=role) @pytest.fixture diff --git a/api/tests/unit_tests/controllers/console/test_files.py b/api/tests/unit_tests/controllers/console/test_files.py index e5db1174b83..bf2b3da2671 100644 --- a/api/tests/unit_tests/controllers/console/test_files.py +++ b/api/tests/unit_tests/controllers/console/test_files.py @@ -1,5 +1,4 @@ import io -from datetime import datetime from unittest.mock import patch import pytest @@ -22,12 +21,11 @@ from controllers.console.files import ( FileSupportTypeApi, upload_file_from_request, ) -from extensions.storage.storage_type import StorageType from machinery.context import RequestContext from models import Account from models.account import AccountStatus, TenantAccountRole -from models.enums import CreatorUserRole from models.model import UploadFile +from tests.unit_tests.model_factories import make_upload_file def unwrap(func): @@ -40,21 +38,13 @@ def unwrap(func): def _upload_file(*, file_id: str = "file-id-123", size: int = 1024) -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id="tenant-123", - storage_type=StorageType.LOCAL, key=f"upload/{file_id}/test.txt", - name="test.txt", size=size, - extension="txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-123", - created_at=datetime(2024, 1, 1), - used=False, ) - upload_file.id = file_id - return upload_file def _request_context(*, workspace_id: str = "tenant-1") -> RequestContext: diff --git a/api/tests/unit_tests/controllers/console/test_human_input_form.py b/api/tests/unit_tests/controllers/console/test_human_input_form.py index 39e8f089ce9..d799c634505 100644 --- a/api/tests/unit_tests/controllers/console/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/console/test_human_input_form.py @@ -23,12 +23,12 @@ from controllers.console.human_input_form import ( from core.workflow.human_input_policy import HumanInputSurface from graphon.enums import WorkflowExecutionStatus, WorkflowType from models import Account -from models.account import AccountStatus from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.human_input import RecipientType from models.model import App, AppMode from models.workflow import WorkflowRun from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_account, make_app @pytest.fixture(autouse=True) @@ -38,20 +38,15 @@ def bind_console_human_input_database(sqlite_engine: Engine, monkeypatch: pytest def _account(*, account_id: str = "user-1") -> Account: - account = Account(name="Console User", email=f"{account_id}@example.com", status=AccountStatus.ACTIVE) - account.id = account_id - return account + return make_account(account_id=account_id, name="Console User", email=f"{account_id}@example.com") def _app() -> App: - return App( - id="app-1", + return make_app( tenant_id="t1", name="Human Input App", - description="", mode=AppMode.WORKFLOW, - enable_site=True, - enable_api=True, + icon_type=None, max_active_requests=0, ) diff --git a/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py b/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py index 6348a2f8ce9..5b13c907f2d 100644 --- a/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py +++ b/api/tests/unit_tests/controllers/console/test_workflow_run_archive.py @@ -37,13 +37,11 @@ _ENDPOINTS = [ WorkflowRunArchiveDownloadFileApi.get, ] from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_account def _account(role: TenantAccountRole) -> Account: - account = Account(name="Test User", email="user@example.com") - account.id = "account-1" - account.role = role - return account + return make_account(name="Test User", email="user@example.com", role=role) def _original(method): diff --git a/api/tests/unit_tests/controllers/console/test_workspace_members.py b/api/tests/unit_tests/controllers/console/test_workspace_members.py index 2528b387623..0be530a33d6 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_members.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_members.py @@ -9,12 +9,11 @@ from controllers.console.workspace.error import InvalidMemberRoleError from controllers.console.workspace.members import MemberInviteEmailApi from enums import DeploymentEdition from models.account import Account, Tenant, TenantAccountRole +from tests.unit_tests.model_factories import make_tenant def _tenant() -> Tenant: - tenant = Tenant(name="Test Tenant") - tenant.id = "tenant-1" - return tenant + return make_tenant(name="Test Tenant") @pytest.fixture diff --git a/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py index f62a0818333..462d0e3bb32 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py @@ -8,12 +8,11 @@ from controllers.console.workspace.agent_providers import ( AgentProviderListApi, ) from models.account import Account +from tests.unit_tests.model_factories import make_account def _account() -> Account: - account = Account(name="Agent Provider Tester", email="agent-provider@example.com") - account.id = "user1" - return account + return make_account(account_id="user1", name="Agent Provider Tester", email="agent-provider@example.com") class TestAgentProviderListApi: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py index cdfdcb0ca8c..e436a4e6393 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py @@ -19,6 +19,7 @@ if not hasattr(builtins, "MethodView"): builtins.MethodView = MethodView # type: ignore[attr-defined] from models.account import Account, TenantAccountRole +from tests.unit_tests.model_factories import make_account @pytest.fixture @@ -57,10 +58,7 @@ def load_balancing_module(monkeypatch: pytest.MonkeyPatch): def _account(role: TenantAccountRole) -> Account: - account = Account(name="Owner", email="owner@example.com") - account.id = "account-1" - account.role = role - return account + return make_account(name="Owner", email="owner@example.com", role=role) def _prepare_context(module, monkeypatch: pytest.MonkeyPatch, role=TenantAccountRole.OWNER): diff --git a/api/tests/unit_tests/controllers/console/workspace/test_models.py b/api/tests/unit_tests/controllers/console/workspace/test_models.py index 9f49e97d3d2..38f4d21db0d 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_models.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_models.py @@ -28,12 +28,11 @@ from controllers.console.workspace.models import ( from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from models import Account +from tests.unit_tests.model_factories import make_account def _account() -> Account: - account = Account(name="Model User", email="model-user@example.com") - account.id = "u1" - return account + return make_account(account_id="u1", name="Model User", email="model-user@example.com") class TestDefaultModelApi: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py index ea0f79a851c..5424dc65634 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py @@ -83,6 +83,7 @@ from models.account import ( TenantPluginInstallPermission, ) from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account def _plugin_category_list_item(category: str = "tool") -> dict[str, Any]: @@ -358,10 +359,7 @@ def _expected_dynamic_option_dump() -> dict[str, Any]: def _account(role: TenantAccountRole = TenantAccountRole.OWNER) -> Account: - account = Account(name="Test User", email="u1@example.com") - account.id = "u1" - account.role = role - return account + return make_account(account_id="u1", name="Test User", email="u1@example.com", role=role) @pytest.fixture diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py index 15af9f1614d..3bbfe1e281c 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -28,6 +28,7 @@ from controllers.console.workspace import rbac as rbac_mod from controllers.console.workspace.rbac import _RolesListQuery from enums import DeploymentEdition from models import Account +from tests.unit_tests.model_factories import make_account @pytest.fixture @@ -47,9 +48,7 @@ def _rbac_config(config_overrides) -> None: def _account() -> Account: - account = Account(name="RBAC User", email="rbac@example.com") - account.id = "acct-1" - return account + return make_account(account_id="acct-1", name="RBAC User", email="rbac@example.com") class TestCurrentIds: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_snippets.py b/api/tests/unit_tests/controllers/console/workspace/test_snippets.py index 75dc6180e90..78943e87482 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_snippets.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_snippets.py @@ -14,6 +14,7 @@ from controllers.console.workspace import snippets as snippets_module from models.account import Account, TenantAccountRole from models.snippet import CustomizedSnippet from services.snippet_dsl_service import ImportStatus, SnippetImportInfo +from tests.unit_tests.model_factories import make_account @pytest.fixture(autouse=True) @@ -33,10 +34,12 @@ def _patch_snippet_service_factory( def _account(account_id: str = "account-1") -> Account: - account = Account(name="Test User", email=f"{account_id}@example.com") - account.id = account_id - account.role = TenantAccountRole.EDITOR - return account + return make_account( + account_id=account_id, + name="Test User", + email=f"{account_id}@example.com", + role=TenantAccountRole.EDITOR, + ) def _snippet(**overrides) -> CustomizedSnippet: diff --git a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin.py b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin.py index 3f8a6efde6b..c0e644e9c73 100644 --- a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin.py +++ b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin.py @@ -36,12 +36,11 @@ from controllers.inner_api.plugin.plugin import ( from core.workflow.file_reference import build_file_reference from models import Account, Tenant from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_tenant def _tenant() -> Tenant: - tenant = Tenant(name="Test Tenant") - tenant.id = "tenant-id" - return tenant + return make_tenant(tenant_id="tenant-id", name="Test Tenant") def _user() -> Account: diff --git a/api/tests/unit_tests/controllers/inner_api/test_agent_files.py b/api/tests/unit_tests/controllers/inner_api/test_agent_files.py index f5bb418d76a..040a0f13abc 100644 --- a/api/tests/unit_tests/controllers/inner_api/test_agent_files.py +++ b/api/tests/unit_tests/controllers/inner_api/test_agent_files.py @@ -16,14 +16,13 @@ from core.workflow.file_reference import build_file_reference from models.account import Account, Tenant from services.file_request_service import DownloadFileRequestResult from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_tenant MODULE = "controllers.inner_api.agent.files" def _tenant() -> Tenant: - tenant = Tenant(name="Test Workspace") - tenant.id = "tenant-1" - return tenant + return make_tenant(name="Test Workspace") def _raw[R](method: Callable[..., R]) -> Callable[..., R]: diff --git a/api/tests/unit_tests/controllers/inner_api/test_agent_llm.py b/api/tests/unit_tests/controllers/inner_api/test_agent_llm.py index f37e1acd971..f9eb3006bc5 100644 --- a/api/tests/unit_tests/controllers/inner_api/test_agent_llm.py +++ b/api/tests/unit_tests/controllers/inner_api/test_agent_llm.py @@ -50,7 +50,7 @@ def _agent_inner_auth() -> Generator[None]: yield -def _app() -> Flask: +def _flask_app() -> Flask: app = Flask(__name__) app.config["TESTING"] = True app.register_blueprint(inner_api_bp) @@ -75,7 +75,7 @@ def test_post_streams_plugin_compatible_envelope() -> None: patch("controllers.inner_api.agent.llm.AgentLLMInnerService.invoke", return_value=iter([chunk])), ): response = ( - _app() + _flask_app() .test_client() .post( "/inner/api/agent/llm/invoke", @@ -134,7 +134,7 @@ def test_post_preserves_prompt_messages_without_transport_validation() -> None: patch("controllers.inner_api.agent.llm.AgentLLMInnerService.invoke", return_value=iter([])), ): response = ( - _app() + _flask_app() .test_client() .post( "/inner/api/agent/llm/invoke", @@ -150,7 +150,7 @@ def test_post_preserves_prompt_messages_without_transport_validation() -> None: def test_post_rejects_invalid_body_before_model_resolution() -> None: with _agent_inner_auth(): response = ( - _app() + _flask_app() .test_client() .post( "/inner/api/agent/llm/invoke", @@ -176,7 +176,7 @@ def test_post_preserves_preflight_quota_failure() -> None: ), ): response = ( - _app() + _flask_app() .test_client() .post( "/inner/api/agent/llm/invoke", @@ -208,7 +208,7 @@ def test_post_preserves_stream_quota_failure() -> None: ), ): response = ( - _app() + _flask_app() .test_client() .post( "/inner/api/agent/llm/invoke", diff --git a/api/tests/unit_tests/controllers/mcp/test_mcp.py b/api/tests/unit_tests/controllers/mcp/test_mcp.py index 03679199403..0dd960de400 100644 --- a/api/tests/unit_tests/controllers/mcp/test_mcp.py +++ b/api/tests/unit_tests/controllers/mcp/test_mcp.py @@ -18,6 +18,7 @@ from models.engine import db from models.enums import EndUserType from models.model import App, AppAnnotationSetting, AppMCPServer, AppModelConfig, EndUser, IconType from models.workflow import Workflow, WorkflowType +from tests.unit_tests.model_factories import make_end_user @pytest.fixture @@ -127,14 +128,13 @@ def _app( def _end_user() -> EndUser: - end_user = EndUser( + return make_end_user( + end_user_id=str(uuid4()), tenant_id=_TENANT_ID, app_id=_APP_ID, - type=EndUserType.MCP, + end_user_type=EndUserType.MCP, session_id=_SERVER_ID, ) - end_user.id = str(uuid4()) - return end_user class DummyResult: diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py index 96d823b178e..a9ae90a3463 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py @@ -21,10 +21,9 @@ from controllers.openapi.auth.prepare import ( from libs.oauth_bearer import TokenType from models import Account, App, EndUser, Tenant, TenantAccountJoin from models.account import AccountStatus, TenantAccountRole, TenantStatus -from models.enums import AppStatus -from models.model import AppMode, IconType from services import end_user_service from services.enterprise.enterprise_service import WebAppAccessMode +from tests.unit_tests.model_factories import make_account, make_app, make_tenant APP_ID = "00000000-0000-0000-0000-000000000001" TENANT_ID = "00000000-0000-0000-0000-000000000002" @@ -46,32 +45,15 @@ def _app( tenant_id: str = TENANT_ID, enable_api: bool = True, ) -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name="OpenAPI app", - description="", - mode=AppMode.CHAT, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - status=AppStatus.NORMAL, - enable_site=True, - enable_api=enable_api, - max_active_requests=None, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, name="OpenAPI app", enable_api=enable_api) def _tenant(*, tenant_id: str = TENANT_ID, status: TenantStatus = TenantStatus.NORMAL) -> Tenant: - tenant = Tenant(name="OpenAPI tenant", status=status) - tenant.id = tenant_id - return tenant + return make_tenant(tenant_id=tenant_id, name="OpenAPI tenant", status=status) def _account(*, status: AccountStatus = AccountStatus.ACTIVE) -> Account: - account = Account(name="OpenAPI account", email="account@example.com", status=status) - account.id = ACCOUNT_ID - return account + return make_account(account_id=ACCOUNT_ID, name="OpenAPI account", email="account@example.com", status=status) def _persist(session: Session, *models: object) -> None: diff --git a/api/tests/unit_tests/controllers/openapi/test_app_payloads.py b/api/tests/unit_tests/controllers/openapi/test_app_payloads.py index c73e6f3f454..2f548714484 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_payloads.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_payloads.py @@ -13,23 +13,12 @@ from controllers.openapi.apps import ( # pyright: ignore[reportPrivateUsage] parameters_payload, ) from controllers.service_api.app.error import AppUnavailableError -from models.model import App, AppMode, IconType +from models.model import App, AppMode +from tests.unit_tests.model_factories import make_app def _app(*, mode: AppMode = AppMode.CHAT) -> App: - return App( - id="app1", - tenant_id="tenant-1", - name="X", - description="d", - mode=mode, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=False, - enable_api=True, - max_active_requests=0, - ) + return make_app(app_id="app1", name="X", description="d", mode=mode, enable_site=False, max_active_requests=0) def test_parameters_payload_raises_app_unavailable_when_no_config(unbound_session: Session): diff --git a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py index 33a726e9c41..02398cbd2ad 100644 --- a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py +++ b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py @@ -45,7 +45,7 @@ from controllers.openapi.workspaces import ( from enums import DeploymentEdition from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, reset_auth_ctx, set_auth_ctx from models import Account, Tenant, TenantAccountJoin -from models.account import AccountStatus, TenantAccountRole, TenantStatus +from models.account import TenantAccountRole, TenantStatus from services.account_service import TenantService as RealTenantService from services.errors.account import ( AccountAlreadyInTenantError, @@ -57,6 +57,7 @@ from services.errors.account import ( RoleAlreadyAssignedError, ) from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account, make_tenant if not hasattr(builtins, "MethodView"): builtins.MethodView = MethodView # type: ignore[attr-defined] @@ -130,14 +131,11 @@ def _auth_data(account_id: uuid.UUID) -> AuthData: def _account(account_id: str = "acct-1", email: str = "u@example.com") -> Account: - account = Account(name="User", email=email, status=AccountStatus.ACTIVE) - account.id = account_id - return account + return make_account(account_id=account_id, name="User", email=email) def _tenant(tenant_id: str = "ws-1", *, status: TenantStatus = TenantStatus.NORMAL) -> Tenant: - tenant = Tenant(name="WS", status=status) - tenant.id = tenant_id + tenant = make_tenant(tenant_id=tenant_id, name="WS", status=status) tenant.created_at = datetime(2026, 5, 18) return tenant diff --git a/api/tests/unit_tests/controllers/service_api/app/test_audio.py b/api/tests/unit_tests/controllers/service_api/app/test_audio.py index 9e51ffea837..325c5d33f77 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_audio.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_audio.py @@ -34,7 +34,7 @@ from controllers.service_api.app.error import ( from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from graphon.model_runtime.errors.invoke import InvokeError from models.enums import EndUserType -from models.model import App, AppMode, EndUser +from models.model import App, EndUser from services.app_ref_service import AppRef, MessageRef from services.audio_service import AudioService from services.errors.app_model_config import AppModelConfigBrokenError @@ -45,6 +45,7 @@ from services.errors.audio import ( SpeechToTextDisabledServiceError, UnsupportedAudioTypeServiceError, ) +from tests.unit_tests.model_factories import make_app, make_end_user def _file_data(): @@ -52,24 +53,14 @@ def _file_data(): def _app(*, app_id: str = "a1", tenant_id: str = "tenant-1") -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name="Audio app", - description="", - mode=AppMode.CHAT, - enable_site=True, - enable_api=True, - max_active_requests=0, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, name="Audio app", icon_type=None, max_active_requests=0) def _end_user(*, end_user_id: str = "u1", external_user_id: str | None = None) -> EndUser: - return EndUser( - id=end_user_id, - tenant_id="tenant-1", + return make_end_user( + end_user_id=end_user_id, app_id="a1", - type=EndUserType.SERVICE_API, + end_user_type=EndUserType.SERVICE_API, external_user_id=external_user_id, name="Audio user", session_id=f"session-{end_user_id}", diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py index 093852d792f..efb2c2566e2 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py @@ -53,6 +53,7 @@ from services.errors.conversation import ( ConversationVariableNotExistsError, ConversationVariableTypeMismatchError, ) +from tests.unit_tests.model_factories import make_conversation def _app(*, app_id: str = "app-1", mode: AppMode = AppMode.CHAT) -> App: @@ -86,17 +87,15 @@ def _conversation( app_id: str = "app-1", end_user_id: str = "end-user-1", ) -> Conversation: - conversation = Conversation( + return make_conversation( + conversation_id=conversation_id, app_id=app_id, - mode=AppMode.CHAT, name="Original Name", + inputs={}, from_source=ConversationFromSource.API, from_end_user_id=end_user_id, invoke_from=InvokeFrom.SERVICE_API, ) - conversation.id = conversation_id - conversation.inputs = {} - return conversation class TestConversationListQuery: diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py index ace817a4bf3..56aefeda1c7 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py @@ -43,6 +43,7 @@ from models.dataset import ChildChunk, Dataset, Document, DocumentSegment, Docum from models.enums import IndexingStatus, SegmentType from services.api_token_service import CachedApiToken from services.dataset_service import DocumentService, SegmentService +from tests.unit_tests.model_factories import make_account def _segment_response_dict(summary: str | None = None): @@ -85,9 +86,9 @@ def mock_tenant() -> Tenant: def _account() -> Account: - account = Account(name="Segment API User", email=f"segment-api-{uuid.uuid4()}@example.com") - account.id = str(uuid.uuid4()) - return account + return make_account( + account_id=str(uuid.uuid4()), name="Segment API User", email=f"segment-api-{uuid.uuid4()}@example.com" + ) def _api_token(tenant_id: str) -> CachedApiToken: diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_document.py b/api/tests/unit_tests/controllers/service_api/dataset/test_document.py index efb38d0e340..8ea26e001f8 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_document.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_document.py @@ -49,12 +49,10 @@ from controllers.service_api.dataset.document import ( from controllers.service_api.dataset.error import ArchivedDocumentImmutableError from core.rag.index_processor.constant.index_type import IndexStructureType from enums import DeploymentEdition -from extensions.storage.storage_type import StorageType from models.account import Account from models.dataset import Dataset, Document, DocumentSegment from models.enums import ( ApiTokenType, - CreatorUserRole, DataSourceType, DocumentCreatedFrom, DocumentDocType, @@ -67,6 +65,7 @@ from services.dataset_service import DocumentService from services.entities.knowledge_entities.knowledge_entities import ProcessRule, RetrievalModel from services.errors.file import FileTooLargeError as FileTooLargeServiceError from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account, make_upload_file def _document_data_source_info() -> dict[str, str]: @@ -74,27 +73,17 @@ def _document_data_source_info() -> dict[str, str]: def _account() -> Account: - account = Account(name="Document API User", email=f"document-api-{uuid.uuid4()}@example.com") - account.id = "user-1" - return account + return make_account(account_id="user-1", name="Document API User", email=f"document-api-{uuid.uuid4()}@example.com") def _upload_file() -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( key="documents/file.txt", name="file.txt", - size=10, - extension="txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-1", created_at=datetime.now(UTC), used=True, ) - upload_file.id = str(uuid.uuid4()) - return upload_file def _unwrap_non_wrapped_controller(view): diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_rag_pipeline_file_upload_serialization.py b/api/tests/unit_tests/controllers/service_api/dataset/test_rag_pipeline_file_upload_serialization.py index 9e09c8d8158..aa2b1f644af 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_rag_pipeline_file_upload_serialization.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_rag_pipeline_file_upload_serialization.py @@ -5,28 +5,20 @@ Unit tests for Service API knowledge pipeline file-upload serialization. from datetime import UTC, datetime from controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow import PipelineUploadFileResponse -from extensions.storage.storage_type import StorageType from libs.helper import dump_response -from models.enums import CreatorUserRole from models.model import UploadFile +from tests.unit_tests.model_factories import make_upload_file def _upload_file(*, created_at: datetime) -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, - key="uploads/file-1", + return make_upload_file( + file_id="file-1", name="test.pdf", size=123, extension="pdf", mime_type="application/pdf", - created_by_role=CreatorUserRole.ACCOUNT, - created_by="account-1", created_at=created_at, - used=False, ) - upload_file.id = "file-1" - return upload_file def test_file_upload_created_at_is_isoformat_string(): diff --git a/api/tests/unit_tests/controllers/service_api/test_trace_session_id_parsing.py b/api/tests/unit_tests/controllers/service_api/test_trace_session_id_parsing.py index 89766162a1d..05fcc5e400d 100644 --- a/api/tests/unit_tests/controllers/service_api/test_trace_session_id_parsing.py +++ b/api/tests/unit_tests/controllers/service_api/test_trace_session_id_parsing.py @@ -9,6 +9,7 @@ from controllers.service_api.app import workflow as workflow_module from core.helper.trace_id_helper import get_trace_session_id from models.enums import EndUserType from models.model import App, AppMode, EndUser +from tests.unit_tests.model_factories import make_app, make_end_user class _Request: @@ -41,16 +42,11 @@ def test_trace_session_id_invalid_highest_priority_raises_bad_request(): def _app(mode: AppMode) -> App: - return App(id="app-1", mode=mode, tenant_id="tenant-1") + return make_app(mode=mode, icon_type=None) def _end_user() -> EndUser: - return EndUser( - id="user-1", - tenant_id="tenant-1", - type=EndUserType.SERVICE_API, - session_id="session-1", - ) + return make_end_user(end_user_id="user-1", end_user_type=EndUserType.SERVICE_API) def _assert_generate_trace_session_id(mock_generate_service: MagicMock, expected: str) -> None: diff --git a/api/tests/unit_tests/controllers/trigger/test_webhook.py b/api/tests/unit_tests/controllers/trigger/test_webhook.py index 7cfd0306ff2..087922554ae 100644 --- a/api/tests/unit_tests/controllers/trigger/test_webhook.py +++ b/api/tests/unit_tests/controllers/trigger/test_webhook.py @@ -8,6 +8,7 @@ import controllers.trigger.webhook as module from models.trigger import WorkflowWebhookTrigger from models.workflow import Workflow from services.errors.app import QuotaExceededError +from tests.unit_tests.model_factories import make_workflow @pytest.fixture(autouse=True) @@ -35,7 +36,7 @@ def _webhook_trigger() -> WorkflowWebhookTrigger: def _workflow() -> Workflow: - return Workflow(id="workflow-1") + return make_workflow(workflow_id="workflow-1") class TestPrepareWebhookExecution: diff --git a/api/tests/unit_tests/controllers/web/test_audio.py b/api/tests/unit_tests/controllers/web/test_audio.py index 10dc7a3429f..78f40d6dcb9 100644 --- a/api/tests/unit_tests/controllers/web/test_audio.py +++ b/api/tests/unit_tests/controllers/web/test_audio.py @@ -22,7 +22,6 @@ from controllers.web.error import ( ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from graphon.model_runtime.errors.invoke import InvokeError -from models.enums import EndUserType from models.model import App, AppMode, EndUser, IconType from services.app_ref_service import AppRef, MessageRef from services.errors.audio import ( @@ -32,6 +31,7 @@ from services.errors.audio import ( SpeechToTextDisabledServiceError, UnsupportedAudioTypeServiceError, ) +from tests.unit_tests.model_factories import make_end_user def _app_model() -> App: @@ -51,15 +51,7 @@ def _app_model() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - app_id="app-1", - type=EndUserType.BROWSER, - external_user_id="ext-1", - name="Web User", - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1", app_id="app-1", external_user_id="ext-1", name="Web User") # --------------------------------------------------------------------------- diff --git a/api/tests/unit_tests/controllers/web/test_completion.py b/api/tests/unit_tests/controllers/web/test_completion.py index 6c7dce3c4ba..73f2ce6d20c 100644 --- a/api/tests/unit_tests/controllers/web/test_completion.py +++ b/api/tests/unit_tests/controllers/web/test_completion.py @@ -23,24 +23,12 @@ from controllers.web.error import ( from core.app.apps.agent_app.errors import AgentAppNotPublishedError from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from graphon.model_runtime.errors.invoke import InvokeError -from models.enums import EndUserType -from models.model import App, AppMode, EndUser, IconType +from models.model import App, AppMode, EndUser +from tests.unit_tests.model_factories import make_app, make_end_user def _app(mode: AppMode) -> App: - return App( - id="app-1", - tenant_id="tenant-1", - name="Web App", - description="", - mode=mode, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=False, - max_active_requests=0, - ) + return make_app(name="Web App", mode=mode, enable_api=False, max_active_requests=0) def _completion_app() -> App: @@ -52,14 +40,7 @@ def _chat_app() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - app_id="app-1", - type=EndUserType.BROWSER, - name="Web User", - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1", app_id="app-1", name="Web User") # --------------------------------------------------------------------------- diff --git a/api/tests/unit_tests/controllers/web/test_files.py b/api/tests/unit_tests/controllers/web/test_files.py index 03d4241b62f..30c36a8af24 100644 --- a/api/tests/unit_tests/controllers/web/test_files.py +++ b/api/tests/unit_tests/controllers/web/test_files.py @@ -2,7 +2,6 @@ from __future__ import annotations -from datetime import datetime from io import BytesIO from unittest.mock import MagicMock, patch @@ -18,11 +17,11 @@ from controllers.common.errors import ( UnsupportedFileTypeError, ) from controllers.web.files import FileApi -from extensions.storage.storage_type import StorageType from libs.exception import BaseHTTPException -from models.enums import CreatorUserRole, EndUserType +from models.enums import CreatorUserRole from models.model import App, AppMode, EndUser, UploadFile from services.errors import file as file_errors +from tests.unit_tests.model_factories import make_end_user, make_upload_file def _app_model() -> App: @@ -39,31 +38,17 @@ def _app_model() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - app_id="app-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1", app_id="app-1") def _upload_file() -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( + file_id="file-1", key="upload/test.txt", - name="test.txt", size=100, - extension="txt", - mime_type="text/plain", created_by_role=CreatorUserRole.END_USER, created_by="eu-1", - created_at=datetime(2024, 1, 1), - used=False, ) - upload_file.id = "file-1" - return upload_file class TestFileApi: diff --git a/api/tests/unit_tests/controllers/web/test_human_input_file_upload.py b/api/tests/unit_tests/controllers/web/test_human_input_file_upload.py index a3d2c8d188a..9285a6c8fc2 100644 --- a/api/tests/unit_tests/controllers/web/test_human_input_file_upload.py +++ b/api/tests/unit_tests/controllers/web/test_human_input_file_upload.py @@ -28,10 +28,7 @@ from controllers.web.human_input_file_upload import ( InvalidUploadTokenForbiddenError, InvalidUploadTokenUnauthorizedError, ) -from extensions.storage.storage_type import StorageType from models import Account -from models.account import AccountStatus -from models.enums import CreatorUserRole from models.model import UploadFile from services.errors.file import ( BlockedFileExtensionError as BlockedFileExtensionServiceError, @@ -53,6 +50,7 @@ from services.remote_file_service import RemoteFileNotFoundError as RemoteFileNo from services.remote_file_service import RemoteFileUnavailableError as RemoteFileUnavailableServiceError from services.remote_file_service import RemoteFileUploadResult from services.remote_file_service import RemoteFileUrlBlockedError as RemoteFileUrlBlockedServiceError +from tests.unit_tests.model_factories import make_account, make_upload_file @pytest.fixture @@ -63,9 +61,7 @@ def app() -> Flask: def _account() -> Account: - account = Account(name="Form Owner", email="owner@example.com", status=AccountStatus.ACTIVE) - account.id = "owner-1" - return account + return make_account(account_id="owner-1", name="Form Owner", email="owner@example.com") def _upload_context() -> HumanInputUploadContext: @@ -80,22 +76,14 @@ def _upload_context() -> HumanInputUploadContext: def _upload_file() -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( + file_id="file-1", key="upload/sample.txt", name="sample.txt", size=7, - extension="txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, created_by="end-user-1", - created_at=datetime(2024, 1, 1), - used=False, source_url="signed-source-url", ) - upload_file.id = "file-1" - return upload_file def _remote_upload_file() -> RemoteFileUploadResult: diff --git a/api/tests/unit_tests/controllers/web/test_message_endpoints.py b/api/tests/unit_tests/controllers/web/test_message_endpoints.py index 5feea96596d..9fc9ad2fedc 100644 --- a/api/tests/unit_tests/controllers/web/test_message_endpoints.py +++ b/api/tests/unit_tests/controllers/web/test_message_endpoints.py @@ -22,10 +22,10 @@ from controllers.web.message import ( MessageMoreLikeThisQuery, MessageSuggestedQuestionApi, ) -from models.enums import EndUserType from models.model import App, AppMode, EndUser from services.errors.app import MoreLikeThisDisabledError from services.errors.message import MessageNotExistsError +from tests.unit_tests.model_factories import make_end_user def _chat_app() -> App: @@ -37,12 +37,7 @@ def _completion_app() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1") # The @model_validate and @with_session decorators wrap the handlers; tests diff --git a/api/tests/unit_tests/controllers/web/test_remote_files.py b/api/tests/unit_tests/controllers/web/test_remote_files.py index 791ec2770fb..0ca69debd55 100644 --- a/api/tests/unit_tests/controllers/web/test_remote_files.py +++ b/api/tests/unit_tests/controllers/web/test_remote_files.py @@ -23,7 +23,6 @@ from controllers.common.errors import ( ) from controllers.web import remote_files as remote_files_module from libs.exception import BaseHTTPException -from models.enums import EndUserType from models.model import App, AppMode, EndUser from services.errors.file import ( BlockedFileExtensionError as ServiceBlockedFileExtensionError, @@ -55,6 +54,7 @@ REMOTE_FILE_ERROR_CASES: tuple[tuple[type[RemoteFileError], type[BaseHTTPExcepti 502, ), ) +from tests.unit_tests.model_factories import make_end_user def _app_model() -> App: @@ -71,13 +71,7 @@ def _app_model() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - app_id="app-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1", app_id="app-1") def _upload_result() -> RemoteFileUploadResult: diff --git a/api/tests/unit_tests/controllers/web/test_saved_message.py b/api/tests/unit_tests/controllers/web/test_saved_message.py index 22ec43a5b47..11ef11da686 100644 --- a/api/tests/unit_tests/controllers/web/test_saved_message.py +++ b/api/tests/unit_tests/controllers/web/test_saved_message.py @@ -14,9 +14,9 @@ from werkzeug.exceptions import NotFound from controllers.common.controller_schemas import SavedMessageCreatePayload, SavedMessageListQuery from controllers.web.error import NotCompletionAppError from controllers.web.saved_message import SavedMessageApi, SavedMessageListApi -from models.enums import EndUserType from models.model import App, AppMode, EndUser from services.errors.message import MessageNotExistsError +from tests.unit_tests.model_factories import make_end_user def _completion_app() -> App: @@ -28,12 +28,7 @@ def _chat_app() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1") # The @with_session and @model_validate decorators wrap the handler; tests call diff --git a/api/tests/unit_tests/controllers/web/test_workflow.py b/api/tests/unit_tests/controllers/web/test_workflow.py index 711976ed441..49c6ad74c6b 100644 --- a/api/tests/unit_tests/controllers/web/test_workflow.py +++ b/api/tests/unit_tests/controllers/web/test_workflow.py @@ -15,11 +15,11 @@ from controllers.web.error import ( ) from controllers.web.workflow import WorkflowRunApi, WorkflowTaskStopApi from core.errors.error import ProviderTokenNotInitError, QuotaExceededError -from models.enums import EndUserType from models.model import App, AppMode, EndUser from services.errors.app import ( TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, ) +from tests.unit_tests.model_factories import make_end_user def _workflow_app() -> App: @@ -31,12 +31,7 @@ def _chat_app() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1") # --------------------------------------------------------------------------- diff --git a/api/tests/unit_tests/controllers/web/test_workflow_events.py b/api/tests/unit_tests/controllers/web/test_workflow_events.py index 049ab04cb4f..e6a4abf86df 100644 --- a/api/tests/unit_tests/controllers/web/test_workflow_events.py +++ b/api/tests/unit_tests/controllers/web/test_workflow_events.py @@ -11,9 +11,10 @@ from flask import Flask from controllers.common.errors import NotFoundError from controllers.web.workflow_events import WorkflowEventsApi from graphon.enums import WorkflowExecutionStatus -from models.enums import CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom +from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.model import App, AppMode, EndUser from models.workflow import WorkflowRun, WorkflowType +from tests.unit_tests.model_factories import make_end_user def _workflow_app() -> App: @@ -21,12 +22,7 @@ def _workflow_app() -> App: def _end_user() -> EndUser: - return EndUser( - id="eu-1", - tenant_id="tenant-1", - type=EndUserType.BROWSER, - session_id="session-1", - ) + return make_end_user(end_user_id="eu-1") def _workflow_run( diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_config_manager.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_config_manager.py index aaea73832ef..b2ebdb96318 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_config_manager.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_config_manager.py @@ -1,26 +1,17 @@ -import json from unittest.mock import patch from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from models.model import App, AppMode from models.workflow import Workflow, WorkflowType +from tests.unit_tests.model_factories import make_app, make_workflow def _app() -> App: - return App(id="app-1", tenant_id="tenant-1", name="Advanced Chat App", mode=AppMode.ADVANCED_CHAT) + return make_app(name="Advanced Chat App", mode=AppMode.ADVANCED_CHAT, icon_type=None) def _workflow() -> Workflow: - return Workflow( - id="wf-1", - tenant_id="tenant-1", - app_id="app-1", - type=WorkflowType.CHAT, - version=Workflow.VERSION_DRAFT, - graph="{}", - features=json.dumps({}), - created_by="account-1", - ) + return make_workflow(workflow_id="wf-1", workflow_type=WorkflowType.CHAT, graph="{}") class TestAdvancedChatAppConfigManager: diff --git a/api/tests/unit_tests/core/app/apps/agent_app/test_input_guards.py b/api/tests/unit_tests/core/app/apps/agent_app/test_input_guards.py index 9445bc800d4..eaf50049612 100644 --- a/api/tests/unit_tests/core/app/apps/agent_app/test_input_guards.py +++ b/api/tests/unit_tests/core/app/apps/agent_app/test_input_guards.py @@ -23,6 +23,7 @@ from core.app.entities.queue_entities import ( ) from core.moderation.base import ModerationError from models.model import App, AppMode, Message, MessageAnnotation +from tests.unit_tests.model_factories import make_app, make_message class _FakeQueueManager: @@ -46,11 +47,11 @@ def _make_entity(query: str = "hello") -> SimpleNamespace: def _app() -> App: - return App(id="app-1", tenant_id="tenant-1", name="Agent App", mode=AppMode.AGENT_CHAT) + return make_app(name="Agent App", mode=AppMode.AGENT_CHAT, icon_type=None) def _message() -> Message: - return Message(id="msg-1", app_id="app-1", conversation_id="conversation-1") + return make_message(message_id="msg-1") def _patch_moderation(monkeypatch: pytest.MonkeyPatch, *, returns=None, raises: Exception | None = None) -> None: diff --git a/api/tests/unit_tests/core/app/apps/agent_app/test_resolve_agent.py b/api/tests/unit_tests/core/app/apps/agent_app/test_resolve_agent.py index 17f5fcfd19a..9ed6f108680 100644 --- a/api/tests/unit_tests/core/app/apps/agent_app/test_resolve_agent.py +++ b/api/tests/unit_tests/core/app/apps/agent_app/test_resolve_agent.py @@ -34,6 +34,7 @@ from services.agent.workspace_service import ( AgentWorkspaceBindingGenerationMismatchError, AgentWorkspaceService, ) +from tests.unit_tests.model_factories import make_account, make_app, make_conversation _SOUL_DICT = { "model": { @@ -84,24 +85,22 @@ def _snapshot( def _app() -> App: - return App( - id="app-1", + return make_app( tenant_id="t1", name="Agent App", mode=AppMode.AGENT_CHAT, + icon_type=None, enable_site=False, enable_api=False, ) def _account() -> Account: - account = Account(name="Agent User", email="agent-user@example.com") - account.id = "user-1" - return account + return make_account(account_id="user-1", name="Agent User", email="agent-user@example.com") def _conversation(*, binding_id: str) -> Conversation: - return Conversation(id="conversation-1", app_id="app-1", agent_workspace_binding_id=binding_id) + return make_conversation(agent_workspace_binding_id=binding_id) def _binding( diff --git a/api/tests/unit_tests/core/app/apps/chat/test_app_config_manager.py b/api/tests/unit_tests/core/app/apps/chat/test_app_config_manager.py index 7df8a5d8e4e..00052feff1e 100644 --- a/api/tests/unit_tests/core/app/apps/chat/test_app_config_manager.py +++ b/api/tests/unit_tests/core/app/apps/chat/test_app_config_manager.py @@ -6,10 +6,11 @@ from sqlalchemy.orm import Session from core.app.app_config.entities import EasyUIBasedAppModelConfigFrom, ModelConfigEntity, PromptTemplateEntity from core.app.apps.chat.app_config_manager import ChatAppConfigManager from models.model import App, AppMode, AppModelConfig +from tests.unit_tests.model_factories import make_app def _app() -> App: - return App(id="app-1", tenant_id="tenant-1", name="Chat App", mode=AppMode.CHAT) + return make_app(name="Chat App", icon_type=None) def _app_model_config() -> AppModelConfig: diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py index 3e847f65d73..37e3912ba68 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py @@ -26,9 +26,10 @@ from core.app.apps.pipeline.pipeline_runner import PipelineRunner from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from graphon.graph_events import GraphRunFailedEvent from models.dataset import Dataset, Document, Pipeline -from models.enums import DataSourceType, DocumentCreatedFrom, EndUserType +from models.enums import DocumentCreatedFrom from models.model import EndUser from models.workflow import Workflow, WorkflowType +from tests.unit_tests.model_factories import make_dataset, make_document, make_end_user, make_workflow def _pipeline(*, tenant_id: str = "tenant", pipeline_id: str = "pipe") -> Pipeline: @@ -39,10 +40,9 @@ def _pipeline(*, tenant_id: str = "tenant", pipeline_id: str = "pipe") -> Pipeli def _dataset(*, tenant_id: str = "tenant", dataset_id: str = "ds", pipeline_id: str = "pipe") -> Dataset: - return Dataset( - id=dataset_id, + return make_dataset( + dataset_id=dataset_id, tenant_id=tenant_id, - name="Dataset", description="", created_by="user", pipeline_id=pipeline_id, @@ -50,40 +50,26 @@ def _dataset(*, tenant_id: str = "tenant", dataset_id: str = "ds", pipeline_id: def _workflow(*, tenant_id: str = "tenant", pipeline_id: str = "pipe", graph: dict | None = None) -> Workflow: - return Workflow.new( + return make_workflow( tenant_id=tenant_id, app_id=pipeline_id, - type=WorkflowType.RAG_PIPELINE.value, + workflow_type=WorkflowType.RAG_PIPELINE, version="v1", - graph=json.dumps(graph if graph is not None else {"nodes": [], "edges": []}), - features="{}", + graph=graph, created_by="user", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) def _end_user() -> EndUser: - return EndUser( - id="user", - tenant_id="tenant", - app_id="pipe", - type=EndUserType.BROWSER, - name="User", - session_id="sess", - ) + return make_end_user(end_user_id="user", tenant_id="tenant", app_id="pipe", name="User", session_id="sess") def _document(*, document_id: str = "doc", dataset_id: str = "ds", tenant_id: str = "tenant") -> Document: - return Document( - id=document_id, + return make_document( + document_id=document_id, tenant_id=tenant_id, dataset_id=dataset_id, - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, batch="batch", - name="Document", created_from=DocumentCreatedFrom.API, created_by="user", ) diff --git a/api/tests/unit_tests/core/app/apps/test_message_based_app_generator.py b/api/tests/unit_tests/core/app/apps/test_message_based_app_generator.py index 410ec9db8a9..fdd7b53c50a 100644 --- a/api/tests/unit_tests/core/app/apps/test_message_based_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/test_message_based_app_generator.py @@ -18,6 +18,7 @@ from core.app.entities.app_invoke_entities import ChatAppGenerateEntity, InvokeF from models.account import Account from models.model import App, AppMode, Conversation, Message from services.errors.app_model_config import AppModelConfigBrokenError +from tests.unit_tests.model_factories import make_account, make_app class DummyModelConf: @@ -47,19 +48,11 @@ class DummyCompletionGenerateEntity: def _app(*, app_id: str = "app") -> App: - return App( - id=app_id, - tenant_id="tenant-id", - name="Message App", - mode=AppMode.CHAT, - app_model_config_id=None, - ) + return make_app(app_id=app_id, tenant_id="tenant-id", name="Message App", icon_type=None) def _account() -> Account: - account = Account(name="Message User", email="message-user@example.com") - account.id = "user-id" - return account + return make_account(account_id="user-id", name="Message User", email="message-user@example.com") def _make_app_config(app_mode: AppMode) -> EasyUIBasedAppConfig: diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_generator.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_generator.py index 43dcab8c241..c381fb1bbc4 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_generator.py @@ -1,7 +1,6 @@ """SQLite-backed tests for workflow app generation and worker reload behavior.""" import contextlib -import json from types import SimpleNamespace from unittest.mock import MagicMock @@ -22,7 +21,8 @@ from graphon.runtime import GraphRuntimeState, VariablePool from models.enums import CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom from models.model import App, AppMode, EndUser from models.snippet import CustomizedSnippet -from models.workflow import Workflow, WorkflowKind, WorkflowNodeExecutionTriggeredFrom, WorkflowRun, WorkflowType +from models.workflow import Workflow, WorkflowKind, WorkflowNodeExecutionTriggeredFrom, WorkflowRun +from tests.unit_tests.model_factories import make_workflow def _workflow( @@ -32,19 +32,13 @@ def _workflow( tenant_id: str = "tenant", kind: WorkflowKind = WorkflowKind.STANDARD, ) -> Workflow: - return Workflow( - id=workflow_id, + return make_workflow( + workflow_id=workflow_id, tenant_id=tenant_id, app_id=app_id, - type=WorkflowType.WORKFLOW, kind=kind, version="1", - graph=json.dumps({"nodes": [], "edges": []}), - features="{}", created_by="creator", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py index fbe5035d3fa..94dac233314 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py @@ -49,21 +49,16 @@ from graphon.node_events import NodeRunResult from graphon.runtime import GraphRuntimeState, VariablePool from graphon.variables.segments import StringSegment from graphon.variables.variables import StringVariable -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow +from tests.unit_tests.model_factories import make_workflow def _workflow(graph: dict[str, object] | None = None) -> Workflow: - return Workflow.new( + return make_workflow( tenant_id="tenant", app_id="app", - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, graph=json.dumps(graph or {}, default=str), - features="{}", created_by="account", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_app_config_manager.py b/api/tests/unit_tests/core/app/apps/workflow/test_app_config_manager.py index 55911e1cedf..a443dbd4a29 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_app_config_manager.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_app_config_manager.py @@ -1,26 +1,17 @@ -import json from unittest.mock import patch from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager from models.model import App, AppMode -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow +from tests.unit_tests.model_factories import make_app, make_workflow def _app() -> App: - return App(id="app-1", tenant_id="tenant-1", name="Workflow App", mode=AppMode.WORKFLOW) + return make_app(name="Workflow App", mode=AppMode.WORKFLOW, icon_type=None) def _workflow() -> Workflow: - return Workflow( - id="wf-1", - tenant_id="tenant-1", - app_id="app-1", - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph="{}", - features=json.dumps({}), - created_by="account-1", - ) + return make_workflow(workflow_id="wf-1", graph="{}") class TestWorkflowAppConfigManager: diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py index b4d8dbde01f..14af58bae44 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json from types import SimpleNamespace import pytest @@ -53,33 +52,19 @@ from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import naive_utc_now -from models.enums import CreatorUserRole, EndUserType +from models.enums import CreatorUserRole from models.model import AppMode, EndUser -from models.workflow import Workflow, WorkflowAppLog, WorkflowType +from models.workflow import Workflow, WorkflowAppLog +from tests.unit_tests.model_factories import make_end_user, make_workflow from tests.workflow_test_utils import build_test_variable_pool def _workflow() -> Workflow: - return Workflow( - id="workflow-id", - tenant_id="tenant", - app_id="app", - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph="{}", - features=json.dumps({}), - created_by="user", - ) + return make_workflow(workflow_id="workflow-id", tenant_id="tenant", app_id="app", graph="{}", created_by="user") def _end_user(*, end_user_id: str = "user", session_id: str = "session") -> EndUser: - return EndUser( - id=end_user_id, - tenant_id="tenant", - app_id="app", - type=EndUserType.BROWSER, - session_id=session_id, - ) + return make_end_user(end_user_id=end_user_id, tenant_id="tenant", app_id="app", session_id=session_id) def _make_pipeline(): diff --git a/api/tests/unit_tests/core/app/features/test_annotation_reply.py b/api/tests/unit_tests/core/app/features/test_annotation_reply.py index a9ae870fabc..a9001c96c61 100644 --- a/api/tests/unit_tests/core/app/features/test_annotation_reply.py +++ b/api/tests/unit_tests/core/app/features/test_annotation_reply.py @@ -10,26 +10,18 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.app.features.annotation_reply.annotation_reply import AnnotationReplyFeature from models.dataset import DatasetCollectionBinding from models.enums import CollectionBindingType, ConversationFromSource -from models.model import App, AppAnnotationHitHistory, AppAnnotationSetting, AppMode, Message, MessageAnnotation +from models.model import App, AppAnnotationHitHistory, AppAnnotationSetting, Message, MessageAnnotation +from tests.unit_tests.model_factories import make_app, make_message TABLES = (AppAnnotationSetting, DatasetCollectionBinding, MessageAnnotation, AppAnnotationHitHistory) def _app() -> App: - return App( - id="app-1", - tenant_id="tenant-1", - name="Test App", - description="", - mode=AppMode.CHAT, - enable_site=True, - enable_api=True, - max_active_requests=0, - ) + return make_app(icon_type=None, max_active_requests=0) def _message() -> Message: - return Message(id="msg-1") + return make_message(message_id="msg-1", app_id=None, conversation_id=None) def _persist_binding(session: Session) -> DatasetCollectionBinding: diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py index 7c696955c97..5ffb62a8a67 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py @@ -55,12 +55,12 @@ from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBa from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk from core.ops.entities.trace_entity import TraceTaskName from core.ops.ops_trace_manager import TraceQueueManager -from extensions.storage.storage_type import StorageType from graphon.file import FileTransferMethod, FileType from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, TextPromptMessageContent from models.enums import ConversationFromSource, CreatorUserRole from models.model import AppMode, Conversation, Message, MessageAgentThought, MessageFile, UploadFile +from tests.unit_tests.model_factories import make_upload_file class _DummyModelConf: @@ -212,21 +212,17 @@ def _message_file( def _upload_file(*, file_id: str, name: str, mime_type: str, size: int, extension: str) -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id="tenant", - storage_type=StorageType.LOCAL, key=f"uploads/{file_id}", name=name, size=size, extension=extension, mime_type=mime_type, - created_by_role=CreatorUserRole.ACCOUNT, created_by="user", created_at=datetime.now(UTC), - used=False, ) - upload_file.id = file_id - return upload_file def _agent_thought() -> MessageAgentThought: diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py b/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py index 535bd04bcfa..1277d08699f 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py @@ -20,7 +20,8 @@ from graphon.file import FileTransferMethod, FileType from models import model as model_module from models.base import TypeBase from models.enums import ConversationFromSource, CreatorUserRole, MessageFileBelongsTo -from models.model import App, AppMode, Conversation, MessageAnnotation, MessageFile +from models.model import App, Conversation, MessageAnnotation, MessageFile +from tests.unit_tests.model_factories import make_app, make_conversation @dataclass(frozen=True) @@ -46,29 +47,18 @@ def cycle_db(sqlite_engine: Engine, monkeypatch: pytest.MonkeyPatch) -> Iterator def _app(*, app_id: str = "app-id", tenant_id: str = "tenant-1") -> App: - return App( - id=app_id, - tenant_id=tenant_id, - name="Test App", - description="", - mode=AppMode.CHAT, - enable_site=True, - enable_api=True, - max_active_requests=0, - ) + return make_app(app_id=app_id, tenant_id=tenant_id, icon_type=None, max_active_requests=0) def _conversation(*, conversation_id: str = "conv-1", app_id: str = "app-id") -> Conversation: - conversation = Conversation( + return make_conversation( + conversation_id=conversation_id, app_id=app_id, - mode=AppMode.CHAT, name="", status="normal", from_source=ConversationFromSource.API, inputs={}, ) - conversation.id = conversation_id - return conversation def _message_file( diff --git a/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py b/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py index c61b170eb0d..efd856dfbf9 100644 --- a/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py +++ b/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py @@ -6,28 +6,23 @@ import pytest from sqlalchemy.orm import Session from core.datasource.datasource_file_manager import DatasourceFileManager -from extensions.storage.storage_type import StorageType from models.enums import CreatorUserRole from models.model import MessageFile, UploadFile from models.tools import ToolFile +from tests.unit_tests.model_factories import make_upload_file def _upload_file(id: str, *, key: str, mime_type: str) -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( + file_id=id, key=key, name="file.png", size=4, extension=".png", mime_type=mime_type, - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-1", created_at=datetime.now(UTC).replace(tzinfo=None), - used=False, ) - upload_file.id = id - return upload_file def _tool_file(id: str, *, key: str = "tool_key", mimetype: str = "image/png") -> ToolFile: diff --git a/api/tests/unit_tests/core/llm_generator/test_llm_generator_missing.py b/api/tests/unit_tests/core/llm_generator/test_llm_generator_missing.py index 77c887fef8d..6fbf18afbbb 100644 --- a/api/tests/unit_tests/core/llm_generator/test_llm_generator_missing.py +++ b/api/tests/unit_tests/core/llm_generator/test_llm_generator_missing.py @@ -15,6 +15,7 @@ from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage from graphon.model_runtime.entities.message_entities import AssistantPromptMessage from models.dataset import Dataset from services.workflow_service import WorkflowService +from tests.unit_tests.model_factories import make_dataset @pytest.fixture @@ -45,8 +46,8 @@ def _model_manager() -> tuple[MagicMock, MagicMock]: def _dataset(*, dataset_id: str, tenant_id: str, name: str, created_at: datetime) -> Dataset: - return Dataset( - id=dataset_id, + return make_dataset( + dataset_id=dataset_id, tenant_id=tenant_id, name=name, created_by="account-id", diff --git a/api/tests/unit_tests/core/ops/test_ops_trace_manager.py b/api/tests/unit_tests/core/ops/test_ops_trace_manager.py index b37553f8510..c0675a15900 100644 --- a/api/tests/unit_tests/core/ops/test_ops_trace_manager.py +++ b/api/tests/unit_tests/core/ops/test_ops_trace_manager.py @@ -26,6 +26,7 @@ from models.model import App, AppMode, AppModelConfig, Conversation, Message, Me from models.workflow import WorkflowAppLog, WorkflowAppLogCreatedFrom, WorkflowRun, WorkflowType from repositories.sqlalchemy_api_workflow_run_repository import DifyAPISQLAlchemyWorkflowRunRepository from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_app class DummyConfig: @@ -183,20 +184,7 @@ def encryption_functions( def _app(session: Session, *, app_id: str = "app-id", tracing: str | None = None) -> App: - app = App( - id=app_id, - tenant_id="tenant-1", - name="App", - description="description", - mode=AppMode.CHAT, - icon_type=None, - icon=None, - icon_background=None, - enable_site=True, - enable_api=True, - max_active_requests=None, - tracing=tracing, - ) + app = make_app(app_id=app_id, name="App", description="description", icon_type=None, tracing=tracing) session.add(app) session.commit() return app diff --git a/api/tests/unit_tests/core/ops/test_utils.py b/api/tests/unit_tests/core/ops/test_utils.py index 16fd87cfaa4..34752b2a447 100644 --- a/api/tests/unit_tests/core/ops/test_utils.py +++ b/api/tests/unit_tests/core/ops/test_utils.py @@ -19,6 +19,7 @@ from core.ops.utils import ( ) from models.enums import ConversationFromSource from models.model import Message +from tests.unit_tests.model_factories import make_message class _DatabaseBinding: @@ -39,10 +40,11 @@ def message_session(sqlite_session: Session, monkeypatch: pytest.MonkeyPatch) -> def _message(message_id: str) -> Message: - message = Message( - id=message_id, + return make_message( + message_id=message_id, app_id="app-id", conversation_id="conversation-id", + inputs={}, query="question", message={"role": "user", "content": "question"}, answer="answer", @@ -51,8 +53,6 @@ def _message(message_id: str) -> Message: currency="USD", from_source=ConversationFromSource.API, ) - message._inputs = {} - return message class TestValidateUrl: diff --git a/api/tests/unit_tests/core/plugin/test_backwards_invocation_app.py b/api/tests/unit_tests/core/plugin/test_backwards_invocation_app.py index 3716628927b..adfd4084b3e 100644 --- a/api/tests/unit_tests/core/plugin/test_backwards_invocation_app.py +++ b/api/tests/unit_tests/core/plugin/test_backwards_invocation_app.py @@ -13,7 +13,8 @@ from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from models import Account, Tenant, TenantAccountJoin from models.enums import EndUserType from models.model import App, AppMode, AppModelConfig, EndUser -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow +from tests.unit_tests.model_factories import make_app, make_end_user, make_workflow class _Chunk(BaseModel): @@ -33,30 +34,21 @@ def _app( workflow_id: str | None = None, app_model_config_id: str | None = None, ) -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, name="Plugin app", - description="", mode=mode, - enable_site=False, - enable_api=False, + icon_type=None, workflow_id=workflow_id, app_model_config_id=app_model_config_id, + enable_site=False, + enable_api=False, ) def _workflow(*, workflow_id: str = "workflow-1", app_id: str = "app-1", tenant_id: str = "tenant-1") -> Workflow: - return Workflow( - id=workflow_id, - tenant_id=tenant_id, - app_id=app_id, - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph="{}", - _features="{}", - created_by="account-1", - ) + return make_workflow(workflow_id=workflow_id, tenant_id=tenant_id, app_id=app_id, graph="{}") def _end_user( @@ -66,14 +58,12 @@ def _end_user( app_id: str = "app-1", session_id: str = "browser-session", ) -> EndUser: - return EndUser( - id=user_id, + return make_end_user( + end_user_id=user_id, tenant_id=tenant_id, app_id=app_id, - type=EndUserType.BROWSER, session_id=session_id, name="Browser user", - is_anonymous=True, ) diff --git a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py index 835e6b54e87..2cfbfd5db68 100644 --- a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py +++ b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py @@ -11,6 +11,7 @@ import core.rag.datasource.keyword.jieba.jieba as jieba_module from core.rag.datasource.keyword.jieba.jieba import Jieba, dumps_with_sets, set_orjson_default from core.rag.models.document import Document from models.dataset import Dataset, DatasetKeywordTable, DocumentSegment +from tests.unit_tests.model_factories import make_dataset class _DummyLock: @@ -34,11 +35,7 @@ def _dataset_keyword_table( def _dataset(dataset_keyword_table: DatasetKeywordTable | None = None, keyword_number: int | None = None) -> Dataset: - dataset = Dataset( - id="dataset-1", - tenant_id="tenant-1", - keyword_number=keyword_number, - ) + dataset = make_dataset(keyword_number=keyword_number) dataset.get_dataset_keyword_table = MagicMock(return_value=dataset_keyword_table) return dataset diff --git a/api/tests/unit_tests/core/rag/datasource/keyword/test_keyword_base.py b/api/tests/unit_tests/core/rag/datasource/keyword/test_keyword_base.py index e96472699b9..e3b781d3168 100644 --- a/api/tests/unit_tests/core/rag/datasource/keyword/test_keyword_base.py +++ b/api/tests/unit_tests/core/rag/datasource/keyword/test_keyword_base.py @@ -7,10 +7,11 @@ from sqlalchemy.orm import Session from core.rag.datasource.keyword.keyword_base import BaseKeyword from core.rag.models.document import Document from models import Dataset +from tests.unit_tests.model_factories import make_dataset def _dataset() -> Dataset: - return Dataset(id="dataset-1", tenant_id="tenant-1", name="Dataset", created_by="account-1") + return make_dataset() class _KeywordThatRaises(BaseKeyword): diff --git a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py index 91e1ff4b391..07f7a87372a 100644 --- a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py +++ b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import Session from core.rag.docstore.dataset_docstore import DatasetDocumentStore, DocumentSegment from core.rag.models.document import AttachmentDocument, ChildDocument, Document from models.dataset import ChildChunk, Dataset, SegmentAttachmentBinding +from tests.unit_tests.model_factories import make_dataset TENANT_ID = "00000000-0000-0000-0000-000000000001" DATASET_ID = "00000000-0000-0000-0000-000000000002" @@ -23,11 +24,7 @@ ATTACHMENT_ID = "00000000-0000-0000-0000-000000000005" def _dataset() -> Dataset: - dataset = Dataset( - id=DATASET_ID, - tenant_id=TENANT_ID, - ) - return dataset + return make_dataset(dataset_id=DATASET_ID, tenant_id=TENANT_ID) def _persist_segment( diff --git a/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py b/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py index 2f00fda06db..b065556125e 100644 --- a/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py +++ b/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py @@ -9,28 +9,23 @@ import core.rag.extractor.extract_processor as processor_module from core.rag.extractor.entity.datasource_type import DatasourceType from core.rag.extractor.extract_processor import ExtractProcessor from core.rag.models.document import Document -from extensions.storage.storage_type import StorageType -from models.enums import CreatorUserRole from models.model import UploadFile from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_upload_file def _upload_file(*, key: str, file_id: str = "upload-file-1") -> UploadFile: - upload_file = UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( + file_id=file_id, key=key, name=Path(key).name, size=1, extension=Path(key).suffix.lstrip("."), mime_type="application/octet-stream", - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-1", created_at=datetime(2025, 1, 1), used=True, ) - upload_file.id = file_id - return upload_file class _ExtractorFactory: diff --git a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py index b437cdcf205..953529b4c55 100644 --- a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py @@ -26,21 +26,22 @@ from core.repositories.sqlalchemy_workflow_node_execution_repository import ( _find_first, _replace_or_append_offload, ) -from extensions.storage.storage_type import StorageType from graphon.entities import WorkflowNodeExecution from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from models import Account, EndUser, Tenant +from models import Account, EndUser from models.enums import CreatorUserRole, ExecutionOffLoadType from models.model import UploadFile from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, WorkflowNodeExecutionTriggeredFrom +from tests.unit_tests.model_factories import make_account, make_tenant, make_upload_file def _account(*, tenant_id: str = "tenant-1", user_id: str = "user-1") -> Account: - user = Account(name="Test Account", email="test@example.com") - user.id = user_id - user._current_tenant = Tenant(name="Test Tenant") - user._current_tenant.id = tenant_id - return user + return make_account( + account_id=user_id, + name="Test Account", + email="test@example.com", + tenant=make_tenant(tenant_id=tenant_id, name="Test Tenant"), + ) def _end_user(*, tenant_id: str = "tenant-1", user_id: str = "end-user-1") -> EndUser: @@ -48,18 +49,14 @@ def _end_user(*, tenant_id: str = "tenant-1", user_id: str = "end-user-1") -> En def _upload_file(*, key: str = "storage-key") -> UploadFile: - return UploadFile( - tenant_id="tenant-1", - storage_type=StorageType.LOCAL, + return make_upload_file( key=key, name="offload.json", size=1, extension="json", mime_type="application/json", - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-1", created_at=datetime.now(UTC), - used=False, ) diff --git a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py index 0335bff634b..d9f39da073f 100644 --- a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py +++ b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py @@ -19,7 +19,8 @@ from graphon.entities.workflow_node_execution import ( ) from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now -from models import Account, Tenant, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom +from models import Account, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom +from tests.unit_tests.model_factories import make_account, make_tenant @dataclass(frozen=True) @@ -51,12 +52,12 @@ def conflict_database( def _account() -> Account: - tenant = Tenant(name="Conflict Tenant") - tenant.id = "test-tenant-id" - account = Account(name="Conflict User", email="conflict@example.com") - account.id = "test-user-id" - account._current_tenant = tenant - return account + return make_account( + account_id="test-user-id", + name="Conflict User", + email="conflict@example.com", + tenant=make_tenant(tenant_id="test-tenant-id", name="Conflict Tenant"), + ) @pytest.fixture diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py index 45d9bdbf770..efac03fa7ae 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py @@ -29,9 +29,10 @@ from extensions.ext_database import db from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.account import Account from models.base import TypeBase -from models.model import App, AppMode, IconType +from models.model import App, AppMode from models.tools import WorkflowToolProvider -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow +from tests.unit_tests.model_factories import make_account, make_app, make_workflow @pytest.fixture @@ -64,35 +65,27 @@ def _controller(provider_id: str = "provider-1") -> WorkflowToolProviderControll def _app(*, tenant_id: str | None = None) -> App: - return App( - id=str(uuid.uuid4()), + return make_app( + app_id=str(uuid.uuid4()), tenant_id=tenant_id or str(uuid.uuid4()), name="Workflow App", mode=AppMode.WORKFLOW, - icon_type=IconType.EMOJI, icon="workflow", - icon_background="#FFFFFF", - enable_site=True, enable_api=False, ) def _account() -> Account: - return Account(name="Alice", email="alice@example.com") + return make_account(account_id=None, name="Alice", email="alice@example.com") def _workflow(app: App, account: Account | None = None) -> Workflow: - return Workflow.new( + return make_workflow( tenant_id=app.tenant_id, app_id=app.id, - type=WorkflowType.WORKFLOW.value, version="1", - graph=json.dumps({"nodes": []}), - features="{}", + graph={"nodes": []}, created_by=account.id if account else str(uuid.uuid4()), - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) diff --git a/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py b/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py index 616ec72dcaa..32f8a47ac41 100644 --- a/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py +++ b/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py @@ -32,6 +32,7 @@ from graphon.enums import BuiltinNodeTypes, NodeType, WorkflowType from models.model import App, AppMode from models.workflow import Workflow from tests.unit_tests.core.trigger.conftest import VALID_PROVIDER_ID +from tests.unit_tests.model_factories import make_app def _make_poller_args(node_config: dict[str, Any] | None = None) -> dict[str, Any]: @@ -75,14 +76,12 @@ def _workflow_with_node(node_type: NodeType | None) -> Workflow: def _app() -> App: - return App( - id="a1", + return make_app( + app_id="a1", tenant_id="t1", name="Trigger App", - description="", mode=AppMode.WORKFLOW, - enable_site=True, - enable_api=True, + icon_type=None, max_active_requests=0, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py index f5361f9cc71..a49468aeea2 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py @@ -1,4 +1,3 @@ -import json from datetime import UTC, datetime from unittest.mock import Mock @@ -23,15 +22,11 @@ from models.agent_config_entities import AgentSoulConfig, AgentSoulModelConfig, from models.enums import CreatorUserRole from models.model import UploadFile from models.workflow import Workflow +from tests.unit_tests.model_factories import make_workflow def _workflow(graph: dict) -> Workflow: - return Workflow( - id="workflow-1", - tenant_id="tenant-1", - app_id="app-1", - graph=json.dumps(graph), - ) + return make_workflow(workflow_id="workflow-1", graph=graph) def _binding(node_job: WorkflowNodeJobConfig) -> WorkflowAgentNodeBinding: diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py index 938fe289b0a..384340c7d0b 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py @@ -1,4 +1,3 @@ -import json from collections import UserString from datetime import datetime from types import SimpleNamespace @@ -21,8 +20,9 @@ from graphon.node_events import NodeRunResult from graphon.nodes import BuiltinNodeTypes from graphon.runtime import VariablePool from graphon.variables.variables import StringVariable -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_workflow def _build_typed_node_config(node_type: NodeType): @@ -31,16 +31,7 @@ def _build_typed_node_config(node_type: NodeType): def _workflow() -> Workflow: """Build a real transient workflow for single-step orchestration tests.""" - return Workflow( - id="workflow-id", - tenant_id="tenant-id", - app_id="app-id", - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph=json.dumps({"nodes": [], "edges": []}), - _features="{}", - created_by="user-id", - ) + return make_workflow(workflow_id="workflow-id", tenant_id="tenant-id", app_id="app-id", created_by="user-id") def _build_minimal_workflow_entry( diff --git a/api/tests/unit_tests/events/event_handlers/test_queue_default_plugin_install_when_tenant_created.py b/api/tests/unit_tests/events/event_handlers/test_queue_default_plugin_install_when_tenant_created.py index 14303aef5d9..21c64cdd2fb 100644 --- a/api/tests/unit_tests/events/event_handlers/test_queue_default_plugin_install_when_tenant_created.py +++ b/api/tests/unit_tests/events/event_handlers/test_queue_default_plugin_install_when_tenant_created.py @@ -6,12 +6,11 @@ import pytest from events.event_handlers import queue_default_plugin_install_when_tenant_created as handler_module from models.account import Tenant from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_tenant def _tenant() -> Tenant: - tenant = Tenant(name="Test tenant") - tenant.id = "tenant-1" - return tenant + return make_tenant(name="Test tenant") def test_handle_skips_when_no_default_plugins_are_configured(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/libs/test_workspace_member_helper.py b/api/tests/unit_tests/libs/test_workspace_member_helper.py index 6a202e79fa4..af9acd9cf62 100644 --- a/api/tests/unit_tests/libs/test_workspace_member_helper.py +++ b/api/tests/unit_tests/libs/test_workspace_member_helper.py @@ -17,6 +17,7 @@ from libs import oauth_bearer from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, require_workspace_member from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_account pytestmark = pytest.mark.usefixtures("community_edition") @@ -92,12 +93,12 @@ def _persist_membership( def _account(account_id: uuid.UUID, *, status: AccountStatus = AccountStatus.ACTIVE) -> Account: - account = Account(name="Workspace member", email=f"{account_id}@example.com", status=status) # SQLite's StringUUID adapter binds UUID objects as compact hex, while # PostgreSQL binds their dashed string form. Persist the SQLite-bound form # so the production query can keep accepting the AuthContext UUID object. - account.id = account_id.hex - return account + return make_account( + account_id=account_id.hex, name="Workspace member", email=f"{account_id}@example.com", status=status + ) def test_skips_for_enterprise_edition(database: Database, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/model_factories.py b/api/tests/unit_tests/model_factories.py new file mode 100644 index 00000000000..3aeb0f061b5 --- /dev/null +++ b/api/tests/unit_tests/model_factories.py @@ -0,0 +1,489 @@ +"""Builders for the model instances unit tests construct over and over. + +Every builder returns a transient (unpersisted) instance carrying the field set +that most call sites need, so a test module only spells out the values it +actually asserts on. Keyword arguments map one-to-one onto model columns. +""" + +import json +from collections.abc import Mapping, Sequence +from datetime import datetime +from decimal import Decimal + +from extensions.storage.storage_type import StorageType +from graphon.variables import VariableBase +from models.account import Account, AccountStatus, Tenant, TenantAccountRole, TenantStatus +from models.dataset import Dataset, Document +from models.enums import ( + AppStatus, + CreatorUserRole, + DataSourceType, + DocumentCreatedFrom, + EndUserType, + PermissionEnum, +) +from models.model import ( + App, + AppMode, + Conversation, + ConversationFromSource, + EndUser, + IconType, + Message, + UploadFile, +) +from models.workflow import Workflow, WorkflowKind, WorkflowType +from services.entities.account_entities import AccountSnapshot + + +def make_app( + *, + app_id: str = "app-1", + tenant_id: str = "tenant-1", + name: str = "Test App", + description: str = "", + mode: AppMode = AppMode.CHAT, + icon_type: IconType | None = IconType.EMOJI, + icon: str | None = "robot", + icon_background: str | None = "#FFFFFF", + status: AppStatus = AppStatus.NORMAL, + app_model_config_id: str | None = None, + workflow_id: str | None = None, + enable_site: bool = True, + enable_api: bool = True, + api_rpm: int = 0, + api_rph: int = 0, + is_demo: bool = False, + is_public: bool = False, + is_universal: bool = False, + max_active_requests: int | None = None, + tracing: str | None = None, + created_by: str | None = None, + maintainer: str | None = None, + use_icon_as_answer_icon: bool = False, +) -> App: + """Build a transient ``App``. + + ``app_id`` is spelled out instead of ``id`` so call sites never shadow the + builtin. ``icon_type=None`` clears ``icon`` and ``icon_background`` too, for + the tests that assert on an app without icon metadata. + """ + if icon_type is None: + icon = None + icon_background = None + return App( + id=app_id, + tenant_id=tenant_id, + name=name, + description=description, + mode=mode, + icon_type=icon_type, + icon=icon, + icon_background=icon_background, + status=status, + app_model_config_id=app_model_config_id, + workflow_id=workflow_id, + enable_site=enable_site, + enable_api=enable_api, + api_rpm=api_rpm, + api_rph=api_rph, + is_demo=is_demo, + is_public=is_public, + is_universal=is_universal, + max_active_requests=max_active_requests, + tracing=tracing, + created_by=created_by, + maintainer=maintainer, + use_icon_as_answer_icon=use_icon_as_answer_icon, + ) + + +def make_account( + *, + account_id: str | None = "account-1", + name: str = "Test User", + email: str = "test@example.com", + status: AccountStatus = AccountStatus.ACTIVE, + interface_language: str | None = None, + timezone: str | None = None, + role: TenantAccountRole | None = None, + tenant: Tenant | None = None, +) -> Account: + """Build a transient ``Account``. + + ``id``, ``role`` and ``_current_tenant`` are ``init=False`` on the dataclass + model, so they are assigned after construction. Passing ``account_id=None`` + keeps the generated identifier. + """ + account = Account( + name=name, + email=email, + status=status, + interface_language=interface_language, + timezone=timezone, + ) + if account_id is not None: + account.id = account_id + account.role = role + account._current_tenant = tenant + return account + + +def make_tenant( + *, + tenant_id: str | None = "tenant-1", + name: str = "Test Tenant", + status: TenantStatus = TenantStatus.NORMAL, + encrypt_public_key: str | None = None, +) -> Tenant: + """Build a transient ``Tenant``; ``tenant_id=None`` keeps the generated one.""" + tenant = Tenant(name=name, status=status, encrypt_public_key=encrypt_public_key) + if tenant_id is not None: + tenant.id = tenant_id + return tenant + + +def make_end_user( + *, + end_user_id: str = "end-user-1", + tenant_id: str = "tenant-1", + app_id: str | None = None, + end_user_type: EndUserType = EndUserType.BROWSER, + session_id: str = "session-1", + external_user_id: str | None = None, + name: str | None = None, + is_anonymous: bool = True, +) -> EndUser: + """Build a transient ``EndUser``.""" + return EndUser( + id=end_user_id, + tenant_id=tenant_id, + app_id=app_id, + type=end_user_type, + session_id=session_id, + external_user_id=external_user_id, + name=name, + is_anonymous=is_anonymous, + ) + + +def make_account_snapshot( + *, + account_id: str = "account-1", + name: str = "Account", + email: str = "account@example.com", + avatar: str | None = None, + is_password_set: bool = False, + interface_language: str | None = "en-US", + interface_theme: str | None = "light", + timezone: str | None = "UTC", + last_login_at: datetime | None = None, + last_login_ip: str | None = None, + status: str = "active", + initialized_at: datetime | None = None, + created_at: datetime = datetime(2026, 1, 1), +) -> AccountSnapshot: + """Build the framework-neutral ``AccountSnapshot`` the account services return.""" + return AccountSnapshot( + id=account_id, + name=name, + email=email, + avatar=avatar, + is_password_set=is_password_set, + interface_language=interface_language, + interface_theme=interface_theme, + timezone=timezone, + last_login_at=last_login_at, + last_login_ip=last_login_ip, + status=status, + initialized_at=initialized_at, + created_at=created_at, + ) + + +def make_workflow( + *, + workflow_id: str | None = None, + tenant_id: str = "tenant-1", + app_id: str = "app-1", + workflow_type: WorkflowType = WorkflowType.WORKFLOW, + kind: WorkflowKind = WorkflowKind.STANDARD, + version: str = Workflow.VERSION_DRAFT, + graph: str | Mapping[str, object] | None = None, + features: str | Mapping[str, object] = "{}", + created_by: str = "account-1", + environment_variables: Sequence[VariableBase] = (), + conversation_variables: Sequence[VariableBase] = (), + rag_pipeline_variables: list[dict[str, object]] | None = None, + marked_name: str = "", + marked_comment: str = "", + version_number: int | None = None, +) -> Workflow: + """Build a transient ``Workflow`` through ``Workflow.new``. + + ``graph`` and ``features`` accept either the serialized column value or the + mapping to serialize, since most call sites start from a graph literal. + ``workflow_id=None`` keeps the identifier ``Workflow.new`` generates. + """ + workflow = Workflow.new( + tenant_id=tenant_id, + app_id=app_id, + type=workflow_type.value, + kind=kind.value, + version=version, + graph=graph if isinstance(graph, str) else json.dumps({"nodes": [], "edges": []} if graph is None else graph), + features=features if isinstance(features, str) else json.dumps(features), + created_by=created_by, + environment_variables=environment_variables, + conversation_variables=conversation_variables, + rag_pipeline_variables=rag_pipeline_variables or [], + marked_name=marked_name, + marked_comment=marked_comment, + version_number=version_number, + ) + if workflow_id is not None: + workflow.id = workflow_id + return workflow + + +def make_upload_file( + *, + file_id: str | None = None, + tenant_id: str = "tenant-1", + storage_type: StorageType = StorageType.LOCAL, + key: str = "uploads/file-1", + name: str = "test.txt", + size: int = 10, + extension: str = "txt", + mime_type: str = "text/plain", + created_by_role: CreatorUserRole = CreatorUserRole.ACCOUNT, + created_by: str = "account-1", + created_at: datetime | None = None, + used: bool = False, + source_url: str = "", +) -> UploadFile: + """Build a transient ``UploadFile``; ``file_id=None`` keeps the generated one.""" + upload_file = UploadFile( + tenant_id=tenant_id, + storage_type=storage_type, + key=key, + name=name, + size=size, + extension=extension, + mime_type=mime_type, + created_by_role=created_by_role, + created_by=created_by, + created_at=created_at if created_at is not None else datetime(2024, 1, 1), + used=used, + source_url=source_url, + ) + if file_id is not None: + upload_file.id = file_id + return upload_file + + +def make_dataset( + *, + dataset_id: str | None = "dataset-1", + tenant_id: str = "tenant-1", + name: str = "Dataset", + description: str | None = None, + provider: str | None = None, + permission: PermissionEnum | None = None, + data_source_type: DataSourceType | None = None, + indexing_technique: str | None = None, + index_struct: str | None = None, + created_by: str = "account-1", + maintainer: str | None = None, + created_at: datetime | None = None, + embedding_model: str | None = None, + embedding_model_provider: str | None = None, + keyword_number: int | None = None, + built_in_field_enabled: bool | None = None, + icon_info: dict[str, str] | None = None, + pipeline_id: str | None = None, + chunk_structure: str | None = None, + enable_api: bool | None = None, +) -> Dataset: + """Build a transient ``Dataset``. + + Optional columns default to ``None`` so an unset keyword leaves the column + unset, matching a bare ``Dataset(...)``. ``dataset_id=None`` keeps the + generated identifier. + """ + values: dict[str, object] = {"tenant_id": tenant_id, "name": name, "created_by": created_by} + optional: dict[str, object | None] = { + "id": dataset_id, + "description": description, + "provider": provider, + "permission": permission, + "data_source_type": data_source_type, + "indexing_technique": indexing_technique, + "index_struct": index_struct, + "maintainer": maintainer, + "created_at": created_at, + "embedding_model": embedding_model, + "embedding_model_provider": embedding_model_provider, + "keyword_number": keyword_number, + "built_in_field_enabled": built_in_field_enabled, + "icon_info": icon_info, + "pipeline_id": pipeline_id, + "chunk_structure": chunk_structure, + "enable_api": enable_api, + } + values.update({key: value for key, value in optional.items() if value is not None}) + return Dataset(**values) + + +def make_conversation( + *, + conversation_id: str | None = "conversation-1", + app_id: str = "app-1", + mode: AppMode = AppMode.CHAT, + name: str = "Conversation", + inputs: Mapping[str, object] | None = None, + status: str | None = None, + from_source: ConversationFromSource | None = None, + from_account_id: str | None = None, + from_end_user_id: str | None = None, + invoke_from: str | None = None, + introduction: str | None = None, + system_instruction: str | None = None, + system_instruction_tokens: int | None = None, + override_model_configs: str | None = None, + summary: str | None = None, + dialogue_count: int | None = None, + is_deleted: bool | None = None, + agent_workspace_binding_id: str | None = None, +) -> Conversation: + """Build a transient ``Conversation``; unset keywords leave their column unset.""" + values: dict[str, object] = {"app_id": app_id, "mode": mode, "name": name} + optional: dict[str, object | None] = { + "id": conversation_id, + "inputs": inputs, + "status": status, + "from_source": from_source, + "from_account_id": from_account_id, + "from_end_user_id": from_end_user_id, + "invoke_from": invoke_from, + "introduction": introduction, + "system_instruction": system_instruction, + "system_instruction_tokens": system_instruction_tokens, + "override_model_configs": override_model_configs, + "summary": summary, + "dialogue_count": dialogue_count, + "is_deleted": is_deleted, + "agent_workspace_binding_id": agent_workspace_binding_id, + } + values.update({key: value for key, value in optional.items() if value is not None}) + return Conversation(**values) + + +def make_message( + *, + message_id: str | None = "message-1", + app_id: str | None = "app-1", + conversation_id: str | None = "conversation-1", + inputs: Mapping[str, object] | None = None, + query: str | None = None, + message: object | None = None, + answer: str | None = None, + status: str | None = None, + message_unit_price: Decimal | None = None, + answer_unit_price: Decimal | None = None, + total_price: Decimal | None = None, + currency: str | None = None, + from_source: ConversationFromSource | None = None, + from_account_id: str | None = None, + from_end_user_id: str | None = None, + invoke_from: str | None = None, + message_tokens: int | None = None, + answer_tokens: int | None = None, + provider_response_latency: float | None = None, + workflow_run_id: str | None = None, + created_at: datetime | None = None, + updated_at: datetime | None = None, +) -> Message: + """Build a transient ``Message``; unset keywords leave their column unset.""" + values: dict[str, object] = {} + optional: dict[str, object | None] = { + "id": message_id, + "app_id": app_id, + "conversation_id": conversation_id, + "inputs": inputs, + "query": query, + "message": message, + "answer": answer, + "status": status, + "message_unit_price": message_unit_price, + "answer_unit_price": answer_unit_price, + "total_price": total_price, + "currency": currency, + "from_source": from_source, + "from_account_id": from_account_id, + "from_end_user_id": from_end_user_id, + "invoke_from": invoke_from, + "message_tokens": message_tokens, + "answer_tokens": answer_tokens, + "provider_response_latency": provider_response_latency, + "workflow_run_id": workflow_run_id, + "created_at": created_at, + "updated_at": updated_at, + } + values.update({key: value for key, value in optional.items() if value is not None}) + return Message(**values) + + +def make_document( + *, + document_id: str | None = "document-1", + tenant_id: str = "tenant-1", + dataset_id: str = "dataset-1", + position: int = 1, + data_source_type: DataSourceType = DataSourceType.UPLOAD_FILE, + data_source_info: str | None = None, + batch: str = "batch-1", + name: str = "Document", + created_from: str = DocumentCreatedFrom.WEB, + created_by: str = "account-1", + doc_form: str | None = None, + doc_metadata: dict[str, object] | None = None, + created_at: datetime | None = None, + updated_at: datetime | None = None, +) -> Document: + """Build a transient ``Document``; unset keywords leave their column unset.""" + values: dict[str, object] = { + "tenant_id": tenant_id, + "dataset_id": dataset_id, + "position": position, + "data_source_type": data_source_type, + "batch": batch, + "name": name, + "created_from": created_from, + "created_by": created_by, + } + optional: dict[str, object | None] = { + "id": document_id, + "data_source_info": data_source_info, + "doc_form": doc_form, + "doc_metadata": doc_metadata, + "created_at": created_at, + "updated_at": updated_at, + } + values.update({key: value for key, value in optional.items() if value is not None}) + return Document(**values) + + +__all__ = [ + "make_account", + "make_account_snapshot", + "make_app", + "make_conversation", + "make_dataset", + "make_document", + "make_end_user", + "make_message", + "make_tenant", + "make_upload_file", + "make_workflow", +] diff --git a/api/tests/unit_tests/models/test_comment_models.py b/api/tests/unit_tests/models/test_comment_models.py index 480b6fadcc3..0dc1e3339d2 100644 --- a/api/tests/unit_tests/models/test_comment_models.py +++ b/api/tests/unit_tests/models/test_comment_models.py @@ -5,12 +5,11 @@ from sqlalchemy.orm import Session from models.account import Account from models.comment import WorkflowComment, WorkflowCommentMention, WorkflowCommentReply +from tests.unit_tests.model_factories import make_account def _account(name: str) -> Account: - account = Account(name=name, email=f"{name.lower()}@example.com") - account.id = str(uuid4()) - return account + return make_account(account_id=str(uuid4()), name=name, email=f"{name.lower()}@example.com") def _comment(created_by: str, resolved_by: str | None = None) -> WorkflowComment: diff --git a/api/tests/unit_tests/services/agent/test_agent_observability_service.py b/api/tests/unit_tests/services/agent/test_agent_observability_service.py index 32a6e5a54d8..db6a8c19cc0 100644 --- a/api/tests/unit_tests/services/agent/test_agent_observability_service.py +++ b/api/tests/unit_tests/services/agent/test_agent_observability_service.py @@ -18,7 +18,7 @@ from models.enums import ( FeedbackRating, MessageStatus, ) -from models.model import App, AppMode, Conversation, IconType, Message, MessageFeedback +from models.model import App, AppMode, Conversation, Message, MessageFeedback from models.workflow import ( WorkflowExecutionStatus, WorkflowNodeExecutionModel, @@ -30,25 +30,16 @@ from models.workflow import ( from services.agent import observability_service as observability_service_module from services.agent.observability_service import AgentLogQueryParams, AgentObservabilityService from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_app, make_conversation, make_message def _app(*, app_id: str = "app-1", name: str = "Iris", mode: AppMode = AppMode.AGENT_CHAT) -> App: - return App( - id=app_id, - tenant_id="tenant-1", - name=name, - mode=mode, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#fff", - enable_site=False, - enable_api=False, - ) + return make_app(app_id=app_id, name=name, mode=mode, icon_background="#fff", enable_site=False, enable_api=False) def _conversation(*, conversation_id: str = "conversation-1", app_id: str = "app-1") -> Conversation: - return Conversation( - id=conversation_id, + return make_conversation( + conversation_id=conversation_id, app_id=app_id, mode=AppMode.AGENT_CHAT, name="Debug conversation", @@ -66,8 +57,8 @@ def _message( created_at: datetime | None = None, ) -> Message: timestamp = created_at or naive_utc_now() - return Message( - id=message_id, + return make_message( + message_id=message_id, app_id=app_id, conversation_id=conversation_id, inputs={}, diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index 7b1558e7999..ef219521c6b 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -37,11 +37,11 @@ from models.agent_config_entities import ( DeclaredOutputType, WorkflowNodeJobConfig, ) -from models.enums import AppStatus, ConversationFromSource, ConversationStatus +from models.enums import ConversationFromSource, ConversationStatus from models.model import App, AppMode, AppModelConfig, Conversation, IconType, Message from models.skill import AgentSkillBindingSnapshot, Skill, SkillVersion, SkillVersionManifest from models.tools import ToolFile -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from services.agent import composer_service, roster_service from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_service import AgentComposerService @@ -66,6 +66,7 @@ from services.entities.agent_entities import ( ComposerSaveStrategy, ComposerVariant, ) +from tests.unit_tests.model_factories import make_account, make_app, make_conversation, make_workflow def _agent_soul_with_model() -> AgentSoulConfig: @@ -124,14 +125,13 @@ def _snapshot( def _conversation(*, conversation_id: str = "conversation-1", account_id: str = "account-1") -> Conversation: - return Conversation( - id=conversation_id, - app_id="app-1", - override_model_configs="{}", + return make_conversation( + conversation_id=conversation_id, mode=AppMode.AGENT_CHAT, name="Debug", + inputs={}, + override_model_configs="{}", summary="", - _inputs={}, introduction="", system_instruction="", status=ConversationStatus.NORMAL, @@ -142,19 +142,7 @@ def _conversation(*, conversation_id: str = "conversation-1", account_id: str = def _workflow(*, workflow_id: str = "workflow-1", tenant_id: str = "tenant-1", app_id: str = "app-1") -> Workflow: - return Workflow( - id=workflow_id, - tenant_id=tenant_id, - app_id=app_id, - type=WorkflowType.WORKFLOW, - version=Workflow.VERSION_DRAFT, - graph='{"nodes": [], "edges": []}', - _features="{}", - created_by="account-1", - _environment_variables="{}", - _conversation_variables="{}", - _rag_pipeline_variables="{}", - ) + return make_workflow(workflow_id=workflow_id, tenant_id=tenant_id, app_id=app_id) def _app( @@ -164,27 +152,20 @@ def _app( name: str = "Agent App", mode: AppMode = AppMode.AGENT_CHAT, ) -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, name=name, - description="", mode=mode, - icon_type=IconType.EMOJI, icon="🤖", icon_background="#fff", - status=AppStatus.NORMAL, enable_site=False, - enable_api=True, - max_active_requests=None, created_by="account-1", ) def _account(*, account_id: str = "account-1") -> Account: - account = Account(name="Agent Tester", email=f"{account_id}@example.com") - account.id = account_id - return account + return make_account(account_id=account_id, name="Agent Tester", email=f"{account_id}@example.com") def test_agent_soul_has_model(): diff --git a/api/tests/unit_tests/services/agent/test_workflow_publish_service.py b/api/tests/unit_tests/services/agent/test_workflow_publish_service.py index d50f3589b68..fa8eee4c361 100644 --- a/api/tests/unit_tests/services/agent/test_workflow_publish_service.py +++ b/api/tests/unit_tests/services/agent/test_workflow_publish_service.py @@ -17,24 +17,14 @@ from models.agent import ( from models.agent_config_entities import AgentSoulConfig from models.enums import AppStatus from models.model import App, AppMode -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from services.agent.dsl_service import AgentDslService from services.agent.workflow_publish_service import WorkflowAgentPublishService +from tests.unit_tests.model_factories import make_workflow def _workflow(*, workflow_id: str = "workflow-1", version: str = Workflow.VERSION_DRAFT) -> Workflow: - return Workflow( - id=workflow_id, - tenant_id="tenant-1", - app_id="app-1", - type=WorkflowType.WORKFLOW, - version=version, - graph={"nodes": [], "edges": []}, - features={}, - created_by="account-1", - environment_variables=[], - conversation_variables=[], - ) + return make_workflow(workflow_id=workflow_id, version=version) def _inline_agent( diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py index 44859fb7e2d..de56869d4a2 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py @@ -22,7 +22,7 @@ from sqlalchemy.orm import Session, sessionmaker from core.workflow.llm_environment_variable import LLMEnvironmentVariable from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE from graphon.enums import BuiltinNodeTypes -from models import Account, Tenant +from models import Account from models.dataset import ( Dataset, Pipeline, @@ -39,6 +39,7 @@ from services.rag_pipeline.rag_pipeline_dsl_service import ( RagPipelineDslService, RagPipelinePendingData, ) +from tests.unit_tests.model_factories import make_account, make_dataset, make_tenant, make_workflow @pytest.fixture @@ -47,12 +48,12 @@ def service(sqlite_session: Session) -> RagPipelineDslService: def _account(*, tenant_id: str = "tenant-1", account_id: str = "account-1") -> Account: - tenant = Tenant(name="Tenant") - tenant.id = tenant_id - account = Account(name="Account", email="account@example.com") - account.id = account_id - account._current_tenant = tenant - return account + return make_account( + account_id=account_id, + name="Account", + email="account@example.com", + tenant=make_tenant(tenant_id=tenant_id, name="Tenant"), + ) def _pipeline( @@ -76,19 +77,12 @@ def _pipeline( def _workflow(session: Session, pipeline: Pipeline, *, graph: dict[str, Any] | None = None) -> Workflow: - workflow = Workflow( - id=f"workflow-{pipeline.id}", + workflow = make_workflow( + workflow_id=f"workflow-{pipeline.id}", tenant_id=pipeline.tenant_id, app_id=pipeline.id, - type=WorkflowType.RAG_PIPELINE, - kind=WorkflowKind.STANDARD, - version=Workflow.VERSION_DRAFT, - graph=json.dumps(graph or {"nodes": [], "edges": []}), - features="{}", - created_by="account-1", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], + workflow_type=WorkflowType.RAG_PIPELINE, + graph=graph, ) pipeline.workflow_id = workflow.id session.add(workflow) @@ -124,13 +118,13 @@ def _dataset( name: str = "Dataset", chunk_structure: str = "text_model", ) -> Dataset: - dataset = Dataset( + dataset = make_dataset( + dataset_id=None, tenant_id=pipeline.tenant_id, name=name, description="description", data_source_type=DataSourceType.UPLOAD_FILE, indexing_technique="high_quality", - created_by="account-1", maintainer="account-1", chunk_structure=chunk_structure, pipeline_id=pipeline.id, diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py index be5728f396e..43cfb85a828 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py @@ -7,14 +7,14 @@ from pytest_mock import MockerFixture from sqlalchemy import select from sqlalchemy.orm import Session -from extensions.storage.storage_type import StorageType from models.dataset import Dataset, Document, DocumentPipelineExecutionLog, Pipeline -from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom +from models.enums import DataSourceType, DocumentCreatedFrom from models.model import UploadFile from services.entities.knowledge_entities.rag_pipeline_entities import KnowledgeConfiguration from services.errors.rag_pipeline import RagPipelineResourceNotFoundError from services.rag_pipeline.rag_pipeline_transform_service import RagPipelineTransformService from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_upload_file def _dataset(**overrides: object) -> Dataset: @@ -60,21 +60,14 @@ def _pipeline(*, pipeline_id: str = "p-new", tenant_id: str = "t1") -> Pipeline: def _upload_file(*, file_id: str = "file-1", tenant_id: str = "tenant-1") -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id=tenant_id, - storage_type=StorageType.LOCAL, key="files/f.txt", name="f.txt", - size=10, - extension="txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, created_by="user-1", created_at=datetime.now(UTC).replace(tzinfo=None), - used=False, ) - upload_file.id = file_id - return upload_file @pytest.mark.parametrize( diff --git a/api/tests/unit_tests/services/test_account_change_email_service.py b/api/tests/unit_tests/services/test_account_change_email_service.py index 3fb3e8d6837..1556657038e 100644 --- a/api/tests/unit_tests/services/test_account_change_email_service.py +++ b/api/tests/unit_tests/services/test_account_change_email_service.py @@ -33,6 +33,7 @@ from services.entities.account_entities import ( AccountEmailResetStatus, AccountSnapshot, ) +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -45,21 +46,7 @@ def _context() -> RequestContext: def _account(*, email: str = "old@example.com") -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email=email, - avatar=None, - is_password_set=True, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=datetime(2026, 1, 1), - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot(email=email, is_password_set=True, initialized_at=datetime(2026, 1, 1)) def _service() -> tuple[AccountChangeEmailService, dict[str, Mock]]: diff --git a/api/tests/unit_tests/services/test_account_deletion_service.py b/api/tests/unit_tests/services/test_account_deletion_service.py index 0ba68d9d5f8..7d0f0e05ca6 100644 --- a/api/tests/unit_tests/services/test_account_deletion_service.py +++ b/api/tests/unit_tests/services/test_account_deletion_service.py @@ -17,6 +17,7 @@ from services.account_ports import ( AccountWorkspaceMembershipQuery, ) from services.entities.account_entities import AccountDeletionChallenge, AccountSnapshot +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -29,21 +30,7 @@ def _context() -> RequestContext: def _account() -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="account@example.com", - avatar=None, - is_password_set=True, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=datetime(2026, 1, 1), - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot(is_password_set=True, initialized_at=datetime(2026, 1, 1)) def _service(*, accounts: Mock | None = None) -> tuple[AccountDeletionService, dict[str, Mock]]: diff --git a/api/tests/unit_tests/services/test_account_education_service.py b/api/tests/unit_tests/services/test_account_education_service.py index 5f413eeee62..96194f44e31 100644 --- a/api/tests/unit_tests/services/test_account_education_service.py +++ b/api/tests/unit_tests/services/test_account_education_service.py @@ -20,6 +20,7 @@ from services.entities.account_entities import ( AccountEducationVerification, AccountSnapshot, ) +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -32,20 +33,11 @@ def _context() -> RequestContext: def _account() -> AccountSnapshot: - return AccountSnapshot( - id="account-1", + return make_account_snapshot( name="Student", email="student@example.edu", - avatar=None, is_password_set=True, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", initialized_at=datetime(2026, 1, 1), - created_at=datetime(2026, 1, 1), ) diff --git a/api/tests/unit_tests/services/test_account_email_registration_service.py b/api/tests/unit_tests/services/test_account_email_registration_service.py index b3091f51438..513b727e581 100644 --- a/api/tests/unit_tests/services/test_account_email_registration_service.py +++ b/api/tests/unit_tests/services/test_account_email_registration_service.py @@ -29,23 +29,15 @@ from services.entities.account_entities import ( AccountSessionTokens, AccountSnapshot, ) +from tests.unit_tests.model_factories import make_account_snapshot def _account(*, email: str = "stored@example.com") -> AccountSnapshot: - return AccountSnapshot( - id="account-1", + return make_account_snapshot( name="Stored Account", email=email, - avatar=None, is_password_set=True, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", initialized_at=datetime(2026, 1, 1), - created_at=datetime(2026, 1, 1), ) diff --git a/api/tests/unit_tests/services/test_account_initialization_service.py b/api/tests/unit_tests/services/test_account_initialization_service.py index e929e1ff11d..212fc61d149 100644 --- a/api/tests/unit_tests/services/test_account_initialization_service.py +++ b/api/tests/unit_tests/services/test_account_initialization_service.py @@ -19,6 +19,7 @@ from services.entities.account_entities import ( AccountInitializationStatus, AccountSnapshot, ) +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -31,21 +32,7 @@ def _context() -> RequestContext: def _account(*, status: str = "uninitialized") -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="account@example.com", - avatar=None, - is_password_set=False, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status=status, - initialized_at=None, - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot(status=status) def test_cloud_initialization_consumes_invitation_and_updates_account_atomically() -> None: diff --git a/api/tests/unit_tests/services/test_account_password_service.py b/api/tests/unit_tests/services/test_account_password_service.py index d4b9f7c2050..19ccad4125e 100644 --- a/api/tests/unit_tests/services/test_account_password_service.py +++ b/api/tests/unit_tests/services/test_account_password_service.py @@ -1,6 +1,5 @@ from __future__ import annotations -from datetime import datetime from unittest.mock import Mock import pytest @@ -10,6 +9,7 @@ from services.account_errors import AccountNotFoundError, CurrentAccountPassword from services.account_password_service import AccountPasswordService from services.account_ports import AccountPasswordHasher, AccountRepository from services.entities.account_entities import AccountCredentials, AccountPasswordDigest, AccountSnapshot +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -22,21 +22,7 @@ def _context() -> RequestContext: def _account() -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="account@example.com", - avatar=None, - is_password_set=True, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=None, - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot(is_password_set=True) def test_change_verifies_current_password_and_updates_digest() -> None: diff --git a/api/tests/unit_tests/services/test_account_profile_service.py b/api/tests/unit_tests/services/test_account_profile_service.py index 018cd45fc59..76837a88a3c 100644 --- a/api/tests/unit_tests/services/test_account_profile_service.py +++ b/api/tests/unit_tests/services/test_account_profile_service.py @@ -1,6 +1,5 @@ from __future__ import annotations -from datetime import datetime from unittest.mock import Mock import pytest @@ -10,6 +9,7 @@ from services.account_errors import AccountNotFoundError from services.account_ports import AccountRepository from services.account_profile_service import AccountProfileService from services.entities.account_entities import AccountProfileChanges, AccountSnapshot +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -22,21 +22,7 @@ def _context() -> RequestContext: def _account() -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="account@example.com", - avatar=None, - is_password_set=False, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=None, - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot() def test_get_returns_framework_neutral_account_snapshot() -> None: diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index 9bd00353a90..83e83929725 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -37,6 +37,7 @@ from services.errors.account import ( NoPermissionError, ) from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_tenant type _MockDependencies = dict[str, MagicMock] @@ -75,7 +76,7 @@ class TestAccountAssociatedDataFactory: def _tenant(session: Session | None = None) -> Tenant: - tenant = Tenant(name="Test Workspace") + tenant = make_tenant(tenant_id=None, name="Test Workspace") if session is not None: session.add(tenant) return tenant diff --git a/api/tests/unit_tests/services/test_app_dsl_service.py b/api/tests/unit_tests/services/test_app_dsl_service.py index b7d84895d04..e4c1554f62c 100644 --- a/api/tests/unit_tests/services/test_app_dsl_service.py +++ b/api/tests/unit_tests/services/test_app_dsl_service.py @@ -1,4 +1,3 @@ -import json from collections.abc import Callable from types import SimpleNamespace from typing import cast @@ -12,9 +11,9 @@ from sqlalchemy.orm import Session, sessionmaker from constants.dsl_version import CURRENT_APP_DSL_VERSION from core.rbac import RBACPermission, RBACResourceScope from core.workflow.llm_environment_variable import LLMEnvironmentVariable -from models import Account, App, AppMode, Tenant +from models import Account, App, AppMode from models.model import AppModelConfig, AppModelConfigDict, IconType -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from services.agent.dsl_entities import AgentPackage from services.app_dsl_service import AppDslService, Import, PendingData from services.enterprise.enterprise_service import EnterpriseService @@ -23,6 +22,7 @@ from services.errors.account import NoPermissionError from services.errors.app import WorkflowNotFoundError from services.system_feature_service import SystemFeatureService from tests.unit_tests.config_override import apply_config_overrides +from tests.unit_tests.model_factories import make_account, make_app, make_tenant, make_workflow _OVERWRITE_APP_ID = "11111111-1111-4111-8111-111111111111" _TENANT_ID = "22222222-2222-4222-8222-222222222222" @@ -60,12 +60,12 @@ def _persist_overwrite_target(session: Session, *, maintainer: str = _OTHER_ACCO def _account(*, account_id: str = "account-1", tenant_id: str = "tenant-1") -> Account: - account = Account(name="DSL author", email=f"{account_id}@example.com") - account.id = account_id - tenant = Tenant(name="DSL workspace") - tenant.id = tenant_id - account._current_tenant = tenant - return account + return make_account( + account_id=account_id, + name="DSL author", + email=f"{account_id}@example.com", + tenant=make_tenant(tenant_id=tenant_id, name="DSL workspace"), + ) def _app( @@ -75,38 +75,20 @@ def _app( mode: AppMode = AppMode.CHAT, app_model_config_id: str | None = None, ) -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, app_model_config_id=app_model_config_id, name="Existing app", - description="", mode=mode, - icon_type=IconType.EMOJI, - icon="robot", - icon_background="#FFFFFF", - enable_site=True, - enable_api=True, max_active_requests=0, - use_icon_as_answer_icon=False, ) def _workflow( *, graph: dict[str, object], environment_variables: list[LLMEnvironmentVariable] | None = None ) -> Workflow: - workflow = Workflow( - id="workflow-1", - tenant_id="tenant-1", - app_id="app-1", - type=WorkflowType.WORKFLOW, - version="draft", - graph=json.dumps(graph), - _features="{}", - created_by="account-1", - ) - workflow.environment_variables = environment_variables or [] - return workflow + return make_workflow(workflow_id="workflow-1", graph=graph, environment_variables=environment_variables or []) @pytest.mark.parametrize("status", [ImportStatus.FAILED, ImportStatus.PENDING]) diff --git a/api/tests/unit_tests/services/test_audio_service.py b/api/tests/unit_tests/services/test_audio_service.py index 82568916445..88c4fe20964 100644 --- a/api/tests/unit_tests/services/test_audio_service.py +++ b/api/tests/unit_tests/services/test_audio_service.py @@ -81,6 +81,7 @@ from services.errors.audio import ( SpeechToTextDisabledServiceError, UnsupportedAudioTypeServiceError, ) +from tests.unit_tests.model_factories import make_message APP_ID = "11111111-1111-1111-1111-111111111111" TENANT_ID = "22222222-2222-2222-2222-222222222222" @@ -92,10 +93,11 @@ OTHER_ID = "77777777-7777-7777-7777-777777777777" def _message(*, answer: str = "Message answer") -> Message: - message = Message( - id=MESSAGE_ID, + return make_message( + message_id=MESSAGE_ID, app_id=APP_ID, conversation_id=CONVERSATION_ID, + inputs={}, query="Question", message={"role": "user", "content": "Question"}, answer=answer, @@ -107,8 +109,6 @@ def _message(*, answer: str = "Message answer") -> Message: from_end_user_id=END_USER_ID, from_account_id=ACCOUNT_ID, ) - message._inputs = {} - return message class AudioServiceTestDataFactory: diff --git a/api/tests/unit_tests/services/test_billing_portal_service.py b/api/tests/unit_tests/services/test_billing_portal_service.py index 676d0686629..9af98ed8368 100644 --- a/api/tests/unit_tests/services/test_billing_portal_service.py +++ b/api/tests/unit_tests/services/test_billing_portal_service.py @@ -1,4 +1,3 @@ -from datetime import datetime from unittest.mock import MagicMock, Mock import pytest @@ -8,6 +7,7 @@ from services.account_errors import AccountNotFoundError from services.account_ports import AccountRepository from services.billing_portal_service import BillingPortalService from services.entities.account_entities import AccountSnapshot +from tests.unit_tests.model_factories import make_account_snapshot def _context() -> RequestContext: @@ -20,21 +20,7 @@ def _context() -> RequestContext: def _account() -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="owner@example.com", - avatar=None, - is_password_set=False, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=None, - created_at=datetime(2026, 1, 1), - ) + return make_account_snapshot(email="owner@example.com") @pytest.fixture diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index 0e10220635b..f2acba94932 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -22,7 +22,6 @@ import pytest from werkzeug.exceptions import InternalServerError from enums import CloudPlan -from models import Account, Tenant from services.billing_service import BillingService, _BillingHTTPStatusError from services.errors.billing import ( BillingUpstreamInvalidResponseError, @@ -33,15 +32,6 @@ TENANT_ID = "11111111-1111-1111-1111-111111111111" ACCOUNT_ID = "33333333-3333-3333-3333-333333333333" -def _account(*, account_id: str = ACCOUNT_ID, email: str = "user@example.com", tenant_id: str = TENANT_ID) -> Account: - tenant = Tenant(name="Test Tenant") - tenant.id = tenant_id - account = Account(name="Test User", email=email) - account.id = account_id - account._current_tenant = tenant - return account - - class TestBillingServiceSendRequest: """Unit tests for BillingService._send_request method. diff --git a/api/tests/unit_tests/services/test_dataset_service_dataset.py b/api/tests/unit_tests/services/test_dataset_service_dataset.py index f20946f4e91..18a833afb55 100644 --- a/api/tests/unit_tests/services/test_dataset_service_dataset.py +++ b/api/tests/unit_tests/services/test_dataset_service_dataset.py @@ -19,7 +19,6 @@ from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError from core.rag.index_processor.constant.index_type import IndexTechniqueType from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from models import Account -from models.account import Tenant from models.dataset import ( Dataset, DatasetCollectionBinding, @@ -36,6 +35,7 @@ from services.entities.knowledge_entities.rag_pipeline_entities import ( ) from services.errors.account import NoPermissionError from services.errors.dataset import DatasetNameDuplicateError +from tests.unit_tests.model_factories import make_account, make_dataset, make_tenant from .dataset_service_test_helpers import ( MagicMock, @@ -51,13 +51,13 @@ def _account( tenant_id: str = "tenant-1", role: TenantAccountRole = TenantAccountRole.OWNER, ) -> Account: - account = Account(name=f"User {account_id}", email=f"{account_id}@example.com") - account.id = account_id - account.role = role - tenant = Tenant(name=f"Tenant {tenant_id}") - tenant.id = tenant_id - account._current_tenant = tenant - return account + return make_account( + account_id=account_id, + name=f"User {account_id}", + email=f"{account_id}@example.com", + role=role, + tenant=make_tenant(tenant_id=tenant_id, name=f"Tenant {tenant_id}"), + ) def _dataset( @@ -71,8 +71,8 @@ def _dataset( indexing_technique: str = IndexTechniqueType.ECONOMY, chunk_structure: str | None = "text_model", ) -> Dataset: - return Dataset( - id=dataset_id, + return make_dataset( + dataset_id=dataset_id, tenant_id=tenant_id, name=name, description="", diff --git a/api/tests/unit_tests/services/test_dataset_service_document.py b/api/tests/unit_tests/services/test_dataset_service_document.py index c70468e5f65..a450560d231 100644 --- a/api/tests/unit_tests/services/test_dataset_service_document.py +++ b/api/tests/unit_tests/services/test_dataset_service_document.py @@ -7,13 +7,13 @@ from sqlalchemy import event, select from sqlalchemy.orm import Session from enums import DeploymentEdition -from models.account import Tenant from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus from models.model import UploadFile from models.source import DataSourceOauthBinding from services.dataset_ref_service import DatasetRefService from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_account, make_tenant, make_upload_file from .dataset_service_test_helpers import ( Account, @@ -53,12 +53,12 @@ from .dataset_service_test_helpers import ( def _account(*, account_id: str = "user-1", tenant_id: str = "tenant-1") -> Account: - account = Account(name="User", email=f"{account_id}@example.com") - account.id = account_id - tenant = Tenant(name="Tenant") - tenant.id = tenant_id - account._current_tenant = tenant - return account + return make_account( + account_id=account_id, + name="User", + email=f"{account_id}@example.com", + tenant=make_tenant(tenant_id=tenant_id, name="Tenant"), + ) def _dataset_row( @@ -121,21 +121,16 @@ def _document_row( def _upload_file(*, file_id: str, tenant_id: str = "tenant-1", name: str = "upload.txt") -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id=tenant_id, storage_type="opendal", key=f"key-{file_id}", name=name, size=1, - extension="txt", - mime_type="text/plain", - created_by_role="account", created_by="user-1", created_at=datetime(2026, 1, 1), - used=False, ) - upload_file.id = file_id - return upload_file def _process_rule( diff --git a/api/tests/unit_tests/services/test_file_service.py b/api/tests/unit_tests/services/test_file_service.py index a807bc6aa41..22106b85cc1 100644 --- a/api/tests/unit_tests/services/test_file_service.py +++ b/api/tests/unit_tests/services/test_file_service.py @@ -22,12 +22,11 @@ from services.errors.file import ( UnsupportedFileTypeError, ) from services.file_service import FileService +from tests.unit_tests.model_factories import make_account def _account() -> Account: - account = Account(name="Test Account", email="test@example.com") - account.id = "user_id" - return account + return make_account(account_id="user_id", name="Test Account", email="test@example.com") class TestFileService: diff --git a/api/tests/unit_tests/services/test_knowledge_fs_proxy.py b/api/tests/unit_tests/services/test_knowledge_fs_proxy.py index 51cd56b7bfd..1f7f2d6a2a0 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_proxy.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_proxy.py @@ -31,15 +31,13 @@ from services.knowledge_fs_proxy import ( from services.knowledge_fs_proxy import ( _forward_knowledge_fs_request as forward_knowledge_fs_request, ) +from tests.unit_tests.model_factories import make_account _JWT_SECRET = "production-secret-with-at-least-32-bytes" def _account(*, role: TenantAccountRole = TenantAccountRole.DATASET_OPERATOR) -> Account: - account = Account(name="Knowledge User", email="knowledge@example.com") - account.id = "account-1" - account.role = role - return account + return make_account(name="Knowledge User", email="knowledge@example.com", role=role) def _account_for_legacy_role(role: str) -> Account: diff --git a/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py b/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py index 4a5511396d7..66a289d24fa 100644 --- a/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py +++ b/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py @@ -7,7 +7,6 @@ from sqlalchemy.orm import Session from core.workflow.nodes.knowledge_retrieval.retrieval import Source, SourceMetadata from models.dataset import Dataset -from models.enums import AppStatus from models.model import App, AppMode from services.entities.knowledge_retrieval_inner import InnerKnowledgeRetrieveRequest from services.errors.knowledge_retrieval import ( @@ -17,6 +16,7 @@ from services.errors.knowledge_retrieval import ( InnerKnowledgeRetrieveDatasetTenantMismatchError, ) from services.knowledge_retrieval_inner_service import InnerKnowledgeRetrievalService +from tests.unit_tests.model_factories import make_app, make_dataset TENANT_ID = "11111111-1111-1111-1111-111111111111" OTHER_TENANT_ID = "22222222-2222-2222-2222-222222222222" @@ -27,22 +27,19 @@ DATASET_2_ID = "66666666-6666-6666-6666-666666666666" def _app(*, tenant_id: str = TENANT_ID) -> App: - return App( - id=APP_ID, + return make_app( + app_id=APP_ID, tenant_id=tenant_id, - name="Test App", - description="", mode=AppMode.WORKFLOW, - status=AppStatus.NORMAL, + icon_type=None, enable_site=False, enable_api=False, - max_active_requests=None, ) def _dataset(*, dataset_id: str, tenant_id: str = TENANT_ID, enable_api: bool = True) -> Dataset: - return Dataset( - id=dataset_id, + return make_dataset( + dataset_id=dataset_id, tenant_id=tenant_id, name=f"Dataset {dataset_id[-1]}", description="", diff --git a/api/tests/unit_tests/services/test_metadata_service_session_boundary.py b/api/tests/unit_tests/services/test_metadata_service_session_boundary.py index 3414a1b0d10..eea773da175 100644 --- a/api/tests/unit_tests/services/test_metadata_service_session_boundary.py +++ b/api/tests/unit_tests/services/test_metadata_service_session_boundary.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker from core.rag.index_processor.constant.built_in_field import BuiltInField from models import Account from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document -from models.enums import DataSourceType, DocumentCreatedFrom +from models.enums import DocumentCreatedFrom from services.dataset_service import DocumentService from services.entities.knowledge_entities.knowledge_entities import ( DocumentMetadataOperation, @@ -18,6 +18,7 @@ from services.entities.knowledge_entities.knowledge_entities import ( ) from services.errors.metadata import MetadataResourceNotFoundError from services.metadata_service import MetadataService +from tests.unit_tests.model_factories import make_account, make_dataset, make_document DOCUMENT_ID = "11111111-1111-1111-1111-111111111111" FOREIGN_DOCUMENT_ID = "22222222-2222-2222-2222-222222222222" @@ -26,9 +27,7 @@ FOREIGN_METADATA_ID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" def _account() -> Account: - account = Account(name="User", email="user@example.com") - account.id = "account-1" - return account + return make_account(name="User", email="user@example.com") def test_create_metadata_flushes_without_committing_caller_session(sqlite_session: Session) -> None: @@ -50,28 +49,17 @@ def test_create_metadata_flushes_without_committing_caller_session(sqlite_sessio def _dataset(*, built_in_field_enabled: bool) -> Dataset: - return Dataset( - id="dataset-1", - tenant_id="tenant-1", - name="Dataset", + return make_dataset( description="", provider="vendor", - created_by="account-1", built_in_field_enabled=built_in_field_enabled, ) def _document() -> Document: - return Document( - id=DOCUMENT_ID, - tenant_id="tenant-1", - dataset_id="dataset-1", - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch-1", - name="Document", + return make_document( + document_id=DOCUMENT_ID, created_from=DocumentCreatedFrom.API, - created_by="account-1", created_at=datetime(2026, 1, 1), updated_at=datetime(2026, 1, 2), doc_metadata={}, diff --git a/api/tests/unit_tests/services/test_schedule_service.py b/api/tests/unit_tests/services/test_schedule_service.py index 4f5df2f8c8a..40f1801c561 100644 --- a/api/tests/unit_tests/services/test_schedule_service.py +++ b/api/tests/unit_tests/services/test_schedule_service.py @@ -10,8 +10,9 @@ from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE from core.workflow.nodes.trigger_schedule.entities import VisualConfig from core.workflow.nodes.trigger_schedule.exc import ScheduleConfigError from libs.schedule_utils import calculate_next_run_at, convert_12h_to_24h -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from services.trigger.schedule_service import ScheduleService +from tests.unit_tests.model_factories import make_workflow class TestScheduleService(unittest.TestCase): @@ -507,18 +508,7 @@ class TestScheduleWithTimezone(unittest.TestCase): def _workflow(*, graph_dict: dict[str, Any]) -> Workflow: - return Workflow.new( - tenant_id="tenant-1", - app_id="app-1", - type=WorkflowType.WORKFLOW, - version="draft", - graph=json.dumps(graph_dict), - features="{}", - created_by="account-1", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], - ) + return make_workflow(graph=graph_dict) def test_to_schedule_config_should_build_from_cron_mode() -> None: diff --git a/api/tests/unit_tests/services/test_snippet_dsl_service.py b/api/tests/unit_tests/services/test_snippet_dsl_service.py index 900f321c43b..bd06fe98613 100644 --- a/api/tests/unit_tests/services/test_snippet_dsl_service.py +++ b/api/tests/unit_tests/services/test_snippet_dsl_service.py @@ -8,9 +8,9 @@ from sqlalchemy import event from sqlalchemy.orm import Session from graphon.nodes import BuiltinNodeTypes -from models import Account, Tenant +from models import Account from models.snippet import CustomizedSnippet, SnippetType -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow from services.snippet_dsl_service import ( ImportMode, ImportStatus, @@ -18,6 +18,7 @@ from services.snippet_dsl_service import ( SnippetPendingData, _check_version_compatibility, ) +from tests.unit_tests.model_factories import make_account, make_tenant, make_workflow SQLITE_MODELS = (CustomizedSnippet,) pytestmark = [ @@ -33,12 +34,12 @@ def service(sqlite_session: Session) -> SnippetDslService: def _account(*, account_id: str = "account-1", tenant_id: str = "tenant-1") -> Account: - account = Account(name="Snippet author", email=f"{account_id}@example.com") - account.id = account_id - tenant = Tenant(name="Snippet workspace") - tenant.id = tenant_id - account._current_tenant = tenant - return account + return make_account( + account_id=account_id, + name="Snippet author", + email=f"{account_id}@example.com", + tenant=make_tenant(tenant_id=tenant_id, name="Snippet workspace"), + ) def _snippet( @@ -64,16 +65,7 @@ def _snippet( def _workflow(*, graph: dict | None = None) -> Workflow: - return Workflow( - id="workflow-1", - tenant_id="tenant-1", - app_id="snippet-1", - type=WorkflowType.WORKFLOW, - version="draft", - graph=json.dumps(graph or {"nodes": [], "edges": []}), - _features="{}", - created_by="account-1", - ) + return make_workflow(workflow_id="workflow-1", app_id="snippet-1", graph=graph) @pytest.mark.parametrize( diff --git a/api/tests/unit_tests/services/test_snippet_generate_service.py b/api/tests/unit_tests/services/test_snippet_generate_service.py index d1b3f428c7b..cc2a81ecdc4 100644 --- a/api/tests/unit_tests/services/test_snippet_generate_service.py +++ b/api/tests/unit_tests/services/test_snippet_generate_service.py @@ -8,25 +8,13 @@ from sqlalchemy.orm import Session, sessionmaker from core.workflow.snippet_start import SNIPPET_VIRTUAL_START_NODE_ID from models.account import Account from models.snippet import CustomizedSnippet, SnippetType -from models.workflow import Workflow, WorkflowKind, WorkflowNodeExecutionModel, WorkflowType +from models.workflow import Workflow, WorkflowKind, WorkflowNodeExecutionModel from services.snippet_generate_service import SnippetGenerateService +from tests.unit_tests.model_factories import make_account, make_workflow def _workflow(graph: dict) -> Workflow: - return Workflow( - id="workflow-1", - tenant_id="tenant-1", - app_id="snippet-1", - type=WorkflowType.WORKFLOW, - kind=WorkflowKind.SNIPPET, - version=Workflow.VERSION_DRAFT, - graph=json.dumps(graph), - features="{}", - created_by="account-1", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], - ) + return make_workflow(workflow_id="workflow-1", app_id="snippet-1", kind=WorkflowKind.SNIPPET, graph=graph) def _snippet(*, input_fields: list[dict] | None = None) -> CustomizedSnippet: @@ -42,9 +30,7 @@ def _snippet(*, input_fields: list[dict] | None = None) -> CustomizedSnippet: def _account(account_id: str = "user-1") -> Account: - account = Account(name="Test User", email=f"{account_id}@example.com") - account.id = account_id - return account + return make_account(account_id=account_id, name="Test User", email=f"{account_id}@example.com") def test_filter_virtual_start_events_keeps_blocking_response_unchanged(): diff --git a/api/tests/unit_tests/services/test_snippet_service.py b/api/tests/unit_tests/services/test_snippet_service.py index ea2d83c810a..843c1857a81 100644 --- a/api/tests/unit_tests/services/test_snippet_service.py +++ b/api/tests/unit_tests/services/test_snippet_service.py @@ -37,6 +37,7 @@ from models.workflow import ( from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError from services.errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError from services.snippet_service import SnippetService +from tests.unit_tests.model_factories import make_account def _create_workflow(*, workflow_id: str, version: str, graph: dict, features: dict) -> Workflow: @@ -70,9 +71,7 @@ def _snippet(**overrides) -> CustomizedSnippet: def _account(account_id: str = "account-1") -> Account: - account = Account(name="Test User", email=f"{account_id}@example.com") - account.id = account_id - return account + return make_account(account_id=account_id, name="Test User", email=f"{account_id}@example.com") def test_create_snippet_allows_duplicate_names( diff --git a/api/tests/unit_tests/services/test_step_by_step_tour_service.py b/api/tests/unit_tests/services/test_step_by_step_tour_service.py index 1290987ffce..25413aa3449 100644 --- a/api/tests/unit_tests/services/test_step_by_step_tour_service.py +++ b/api/tests/unit_tests/services/test_step_by_step_tour_service.py @@ -12,6 +12,7 @@ from services.account_ports import AccountRepository from services.entities.account_entities import AccountSnapshot from services.entities.onboarding_entities import StepByStepTourPatch, StepByStepTourResult, StepByStepTourState from services.step_by_step_tour_service import StepByStepTourService +from tests.unit_tests.model_factories import make_account_snapshot def _context(*, workspace_id: str = "workspace-1") -> RequestContext: @@ -55,21 +56,7 @@ class StateRepositoryStub: def _account(*, started_at: datetime = datetime(2026, 6, 28)) -> AccountSnapshot: - return AccountSnapshot( - id="account-1", - name="Account", - email="account@example.com", - avatar=None, - is_password_set=False, - interface_language="en-US", - interface_theme="light", - timezone="UTC", - last_login_at=None, - last_login_ip=None, - status="active", - initialized_at=started_at, - created_at=started_at, - ) + return make_account_snapshot(initialized_at=started_at, created_at=started_at) def _accounts(account: AccountSnapshot | None) -> Mock: diff --git a/api/tests/unit_tests/services/test_vector_service.py b/api/tests/unit_tests/services/test_vector_service.py index 50b0d2c58c4..a8914f41f1c 100644 --- a/api/tests/unit_tests/services/test_vector_service.py +++ b/api/tests/unit_tests/services/test_vector_service.py @@ -14,12 +14,12 @@ from sqlalchemy.orm import Session import services.vector_service as vector_service_module from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType -from extensions.storage.storage_type import StorageType from models import UploadFile from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument -from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, ProcessRuleMode +from models.enums import DataSourceType, DocumentCreatedFrom, ProcessRuleMode from services.vector_service import VectorService +from tests.unit_tests.model_factories import make_upload_file @dataclass(frozen=True) @@ -107,21 +107,15 @@ def _make_segment( def _upload_file(*, file_id: str = "file-1", name: str = "img.png", tenant_id: str = "tenant-1") -> UploadFile: - upload_file = UploadFile( + return make_upload_file( + file_id=file_id, tenant_id=tenant_id, - storage_type=StorageType.LOCAL, key=f"uploads/{file_id}", name=name, - size=10, extension="png", mime_type="image/png", - created_by_role=CreatorUserRole.ACCOUNT, - created_by="account-1", created_at=datetime(2026, 1, 1), - used=False, ) - upload_file.id = file_id - return upload_file def _make_child_chunk( diff --git a/api/tests/unit_tests/services/test_vector_space_admission_service.py b/api/tests/unit_tests/services/test_vector_space_admission_service.py index 267d35f7757..db66f40ec66 100644 --- a/api/tests/unit_tests/services/test_vector_space_admission_service.py +++ b/api/tests/unit_tests/services/test_vector_space_admission_service.py @@ -26,6 +26,7 @@ from services.vector_space_admission_service import ( get_vector_space_admission_error_fields, parse_vector_space_estimate_limits, ) +from tests.unit_tests.model_factories import make_dataset _MEBIBYTE = 1024 * 1024 _ESTIMATE_LIMITS = "sandbox:60,professional:6400,team:25600" @@ -74,10 +75,8 @@ class _FakeRedis: def _dataset(session: Session) -> Dataset: - dataset = Dataset( - id=str(uuid4()), - tenant_id="tenant-1", - name="Dataset", + dataset = make_dataset( + dataset_id=str(uuid4()), created_by=str(uuid4()), indexing_technique=IndexTechniqueType.HIGH_QUALITY, embedding_model_provider="provider", diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index f52541d3506..301997252cf 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -10,7 +10,6 @@ from sqlalchemy.engine import Engine from sqlalchemy.orm import Session from werkzeug.datastructures import FileStorage -from graphon.enums import WorkflowType from models.enums import AppTriggerStatus, AppTriggerType, EndUserType from models.model import App, AppMode, EndUser from models.tools import ToolFile @@ -19,6 +18,7 @@ from models.workflow import Workflow from services.errors.app import QuotaExceededError from services.trigger import webhook_service as webhook_service_module from services.trigger.webhook_service import WebhookService +from tests.unit_tests.model_factories import make_app, make_end_user, make_workflow def _webhook_trigger( @@ -59,31 +59,22 @@ def _workflow( app_id: str = "app-123", version: str = Workflow.VERSION_DRAFT, ) -> Workflow: - workflow = Workflow.new( + return make_workflow( + workflow_id=workflow_id, tenant_id=tenant_id, app_id=app_id, - type=WorkflowType.WORKFLOW.value, version=version, - graph='{"nodes": [], "edges": []}', - features="{}", created_by="account-123", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) - workflow.id = workflow_id - return workflow def _app(*, tenant_id: str = "tenant-123", app_id: str = "app-123") -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, name="Webhook App", - description="", mode=AppMode.WORKFLOW, - enable_site=True, - enable_api=True, + icon_type=None, max_active_requests=0, ) @@ -100,11 +91,11 @@ def _app_trigger(*, status: AppTriggerStatus = AppTriggerStatus.ENABLED) -> AppT def _end_user() -> EndUser: - return EndUser( - id="end-user-123", + return make_end_user( + end_user_id="end-user-123", tenant_id="tenant-123", app_id="app-123", - type=EndUserType.TRIGGER, + end_user_type=EndUserType.TRIGGER, session_id="webhook-session", ) diff --git a/api/tests/unit_tests/services/test_workflow_collaboration_service.py b/api/tests/unit_tests/services/test_workflow_collaboration_service.py index 943677502f7..c1cbe203d63 100644 --- a/api/tests/unit_tests/services/test_workflow_collaboration_service.py +++ b/api/tests/unit_tests/services/test_workflow_collaboration_service.py @@ -11,10 +11,11 @@ from sqlalchemy.orm import Session from core.rbac import RBACPermission, RBACResourceScope from models.account import Account, Tenant from models.base import TypeBase -from models.model import App, AppMode, IconType +from models.model import App, AppMode from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository from services.workflow_collaboration_service import SYNC_REQUEST_TIMEOUT_SECONDS, WorkflowCollaborationService from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_app @pytest.fixture @@ -27,25 +28,15 @@ def db_session(sqlite_engine: Engine) -> Iterator[Session]: def _app(*, app_id: str, tenant_id: str, maintainer: str | None = None) -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, name="Workflow app", - description="", mode=AppMode.WORKFLOW, - icon_type=IconType.EMOJI, - icon="robot", icon_background="#ffffff", enable_site=False, enable_api=False, - api_rpm=0, - api_rph=0, - is_demo=False, - is_public=False, - is_universal=False, - max_active_requests=None, maintainer=maintainer, - use_icon_as_answer_icon=False, ) diff --git a/api/tests/unit_tests/services/test_workflow_comment_service.py b/api/tests/unit_tests/services/test_workflow_comment_service.py index f850169d09d..31b428ac1ab 100644 --- a/api/tests/unit_tests/services/test_workflow_comment_service.py +++ b/api/tests/unit_tests/services/test_workflow_comment_service.py @@ -19,6 +19,7 @@ from models.account import Account, TenantAccountRole from models.model import AppMode from services import workflow_comment_service as service_module from services.workflow_comment_service import WorkflowCommentService +from tests.unit_tests.model_factories import make_account, make_app TENANT_ID = "11111111-1111-1111-1111-111111111111" OTHER_TENANT_ID = "11111111-1111-1111-1111-111111111112" @@ -55,22 +56,20 @@ def _account( email: str = "user@example.com", interface_language: str | None = "en-US", ) -> Account: - account = Account(name=name, email=email, interface_language=interface_language) - account.id = account_id - return account + return make_account(account_id=account_id, name=name, email=email, interface_language=interface_language) def _app(*, app_id: str = APP_ID, tenant_id: str = TENANT_ID, name: str = "My App") -> App: - app = App( + return make_app( + app_id=app_id, tenant_id=tenant_id, name=name, mode=AppMode.WORKFLOW, + icon_type=None, enable_site=False, enable_api=False, created_by=OWNER_ID, ) - app.id = app_id - return app def _comment( diff --git a/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py b/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py index 62c4b456c63..1f75c576b78 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py @@ -17,11 +17,11 @@ from core.workflow.human_input_adapter import ( from core.workflow.nodes.human_input.entities import HumanInputNodeData from graphon.enums import BuiltinNodeTypes from models import Account -from models.enums import AppStatus from models.model import App, AppMode from models.workflow import Workflow, WorkflowType from services import workflow_service as workflow_service_module from services.workflow_service import WorkflowService +from tests.unit_tests.model_factories import make_account, make_app TENANT_ID = "11111111-1111-1111-1111-111111111111" APP_ID = "22222222-2222-2222-2222-222222222222" @@ -35,23 +35,18 @@ def _make_service(sqlite_session: Session) -> WorkflowService: def _app() -> App: - return App( - id=APP_ID, + return make_app( + app_id=APP_ID, tenant_id=TENANT_ID, - name="Test App", - description="", mode=AppMode.WORKFLOW, - status=AppStatus.NORMAL, + icon_type=None, enable_site=False, enable_api=False, - max_active_requests=None, ) def _account() -> Account: - account = Account(name="Test User", email="test@example.com") - account.id = ACCOUNT_ID - return account + return make_account(account_id=ACCOUNT_ID, name="Test User", email="test@example.com") def _build_node_config(delivery_methods: list[EmailDeliveryMethod]) -> dict[str, object]: diff --git a/api/tests/unit_tests/tasks/test_clean_document_task.py b/api/tests/unit_tests/tasks/test_clean_document_task.py index 4aef0581933..259dfd6e6a1 100644 --- a/api/tests/unit_tests/tasks/test_clean_document_task.py +++ b/api/tests/unit_tests/tasks/test_clean_document_task.py @@ -20,9 +20,10 @@ from models.dataset import ( DocumentSegment, SegmentAttachmentBinding, ) -from models.enums import DataSourceType, DocumentCreatedFrom +from models.enums import DataSourceType from models.model import UploadFile from tasks.clean_document_task import clean_document_task +from tests.unit_tests.model_factories import make_document SQLITE_MODELS = ( Dataset, @@ -87,15 +88,11 @@ def mock_index_processor_factory(): def _document(*, document_id: str, dataset_id: str, tenant_id: str, created_by: str) -> Document: - return Document( - id=document_id, + return make_document( + document_id=document_id, tenant_id=tenant_id, dataset_id=dataset_id, - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch-1", name=f"{document_id}.txt", - created_from=DocumentCreatedFrom.WEB, created_by=created_by, ) diff --git a/api/tests/unit_tests/tasks/test_delete_conversation_task.py b/api/tests/unit_tests/tasks/test_delete_conversation_task.py index e3e5d84488c..aa86ac156a0 100644 --- a/api/tests/unit_tests/tasks/test_delete_conversation_task.py +++ b/api/tests/unit_tests/tasks/test_delete_conversation_task.py @@ -12,7 +12,6 @@ from core.workflow.nodes.human_input.enums import HumanInputFormKind, HumanInput from graphon.file import FileTransferMethod, FileType from models import ( AgentDebugConversation, - AppMode, Conversation, ConversationVariable, HumanInputForm, @@ -39,6 +38,7 @@ from models.enums import ( from models.human_input import HumanInputDelivery, HumanInputFormRecipient, RecipientType from models.tools import ToolConversationVariables, ToolFile from tasks.delete_conversation_task import _cleanup_conversation_related_data, sweep_deleted_conversations +from tests.unit_tests.model_factories import make_conversation, make_message TENANT_ID = "11111111-1111-1111-1111-111111111111" APP_ID = "22222222-2222-2222-2222-222222222222" @@ -50,10 +50,9 @@ AGENT_ID = "77777777-7777-7777-7777-777777777777" def _conversation(conversation_id: str, *, deleted: bool) -> Conversation: - return Conversation( - id=conversation_id, + return make_conversation( + conversation_id=conversation_id, app_id=APP_ID, - mode=AppMode.CHAT, name="Test conversation", inputs={}, status=ConversationStatus.NORMAL, @@ -64,8 +63,8 @@ def _conversation(conversation_id: str, *, deleted: bool) -> Conversation: def _message() -> Message: - return Message( - id=MESSAGE_ID, + return make_message( + message_id=MESSAGE_ID, app_id=APP_ID, conversation_id=CONVERSATION_ID, inputs={}, diff --git a/api/tests/unit_tests/tasks/test_resume_agent_app_task.py b/api/tests/unit_tests/tasks/test_resume_agent_app_task.py index c9d20849af7..ce063d3a6bb 100644 --- a/api/tests/unit_tests/tasks/test_resume_agent_app_task.py +++ b/api/tests/unit_tests/tasks/test_resume_agent_app_task.py @@ -18,6 +18,7 @@ from models.enums import InvokeFrom as StoredInvokeFrom from models.human_input import HumanInputForm from models.model import App, AppMode, Conversation, EndUser from tasks.app_generate import resume_agent_app_task as mod +from tests.unit_tests.model_factories import make_app, make_conversation MODULE = "tasks.app_generate.resume_agent_app_task" @@ -33,15 +34,12 @@ def task_session(mocker: MockerFixture, sqlite_session_factory: sessionmaker[Ses def _app(*, app_id: str, tenant_id: str) -> App: - return App( - id=app_id, + return make_app( + app_id=app_id, tenant_id=tenant_id, name="Agent app", - description="", mode=AppMode.AGENT_CHAT, icon_type=None, - icon=None, - icon_background=None, enable_site=False, enable_api=False, ) @@ -55,8 +53,8 @@ def _conversation( end_user_id: str | None = None, invoke_from: StoredInvokeFrom = StoredInvokeFrom.WEB_APP, ) -> Conversation: - return Conversation( - id=conversation_id, + return make_conversation( + conversation_id=conversation_id, app_id=app_id, mode=AppMode.AGENT_CHAT, name="Agent conversation", diff --git a/api/tests/unit_tests/tasks/test_sync_website_document_indexing_task.py b/api/tests/unit_tests/tasks/test_sync_website_document_indexing_task.py index 041e9ba55e5..594c4295305 100644 --- a/api/tests/unit_tests/tasks/test_sync_website_document_indexing_task.py +++ b/api/tests/unit_tests/tasks/test_sync_website_document_indexing_task.py @@ -7,14 +7,15 @@ from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexStructureType from enums import DeploymentEdition from models.dataset import Dataset, Document, DocumentSegment -from models.enums import DataSourceType, DocumentCreatedFrom +from models.enums import DataSourceType from tasks.sync_website_document_indexing_task import sync_website_document_indexing_task from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.model_factories import make_dataset, make_document def _dataset(tenant_id: str) -> Dataset: - return Dataset( - id=str(uuid.uuid4()), + return make_dataset( + dataset_id=str(uuid.uuid4()), tenant_id=tenant_id, name="Website dataset", data_source_type=DataSourceType.WEBSITE_CRAWL, @@ -23,15 +24,12 @@ def _dataset(tenant_id: str) -> Dataset: def _document(dataset: Dataset) -> Document: - return Document( - id=str(uuid.uuid4()), + return make_document( + document_id=str(uuid.uuid4()), tenant_id=dataset.tenant_id, dataset_id=dataset.id, - position=1, data_source_type=DataSourceType.WEBSITE_CRAWL, - batch="batch-1", name="Website document", - created_from=DocumentCreatedFrom.WEB, created_by=str(uuid.uuid4()), doc_form=IndexStructureType.PARAGRAPH_INDEX, ) diff --git a/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py b/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py index 6609633a9cb..ecb320b5a9d 100644 --- a/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py +++ b/api/tests/unit_tests/tasks/test_trigger_processing_tasks.py @@ -1,52 +1,43 @@ -import json from unittest.mock import MagicMock, patch import pytest import tasks.trigger_processing_tasks as trigger_processing_tasks_module from core.plugin.entities.plugin_daemon import CredentialType -from graphon.enums import WorkflowType from models.enums import EndUserType from models.model import EndUser from models.trigger import TriggerSubscription, WorkflowPluginTrigger from models.workflow import Workflow from services.errors.app import QuotaExceededError from tasks.trigger_processing_tasks import dispatch_triggered_workflow +from tests.unit_tests.model_factories import make_end_user, make_workflow def _workflow(*, app_id: str = "app-123") -> Workflow: - workflow = Workflow.new( + return make_workflow( + workflow_id="workflow-123", tenant_id="tenant-123", app_id=app_id, - type=WorkflowType.WORKFLOW.value, version="published", - graph=json.dumps( - { - "nodes": [ - { - "id": "node-123", - "data": {"type": trigger_processing_tasks_module.TRIGGER_PLUGIN_NODE_TYPE}, - } - ], - "edges": [], - } - ), - features="{}", + graph={ + "nodes": [ + { + "id": "node-123", + "data": {"type": trigger_processing_tasks_module.TRIGGER_PLUGIN_NODE_TYPE}, + } + ], + "edges": [], + }, created_by="user-123", - environment_variables=[], - conversation_variables=[], - rag_pipeline_variables=[], ) - workflow.id = "workflow-123" - return workflow def _end_user() -> EndUser: - return EndUser( - id="end-user-123", + return make_end_user( + end_user_id="end-user-123", tenant_id="tenant-123", app_id="app-123", - type=EndUserType.TRIGGER, + end_user_type=EndUserType.TRIGGER, session_id="trigger-session", ) From 3d436a7e31e1332d30159282e3c48ea3f9a8efd7 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:27:49 +0000 Subject: [PATCH 16/21] =?UTF-8?q?refactor(openapi):=20delete=20the=20dead?= =?UTF-8?q?=20bearer=E2=86=92handler=20auth=20adapter=20(#41311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/openapi/_models.py | 68 +---- api/controllers/openapi/apps.py | 14 +- .../openapi/apps_permitted_external.py | 12 +- api/controllers/openapi/auth/data.py | 2 - api/controllers/openapi/auth/pipeline.py | 2 - api/controllers/openapi/auth/surface_gate.py | 90 ------- api/controllers/openapi/auth/verify.py | 9 +- api/controllers/openapi/workspaces.py | 5 +- api/libs/oauth_bearer.py | 236 +----------------- api/services/openapi/license_gate.py | 53 ---- .../controllers/openapi/test_auth.py | 128 ---------- .../controllers/openapi/conftest.py | 1 - .../openapi/auth/test_composition.py | 1 - .../openapi/auth/test_conditions.py | 12 +- .../controllers/openapi/auth/test_data.py | 18 -- .../controllers/openapi/auth/test_flow.py | 2 +- .../controllers/openapi/auth/test_pipeline.py | 4 - .../controllers/openapi/auth/test_prepare.py | 1 - .../controllers/openapi/auth/test_verify.py | 24 +- .../controllers/openapi/conftest.py | 1 - .../openapi/test_app_run_streaming.py | 1 - .../openapi/test_human_input_form.py | 1 - .../openapi/test_pagination_envelope.py | 134 ---------- .../openapi/test_workflow_events_openapi.py | 1 - .../openapi/test_workspaces_members.py | 5 +- .../libs/test_oauth_bearer_layer0_cache.py | 94 ------- .../libs/test_oauth_bearer_require_scope.py | 98 -------- .../libs/test_workspace_member_helper.py | 202 --------------- .../services/openapi/test_mint_policy.py | 48 ---- 29 files changed, 45 insertions(+), 1222 deletions(-) delete mode 100644 api/controllers/openapi/auth/surface_gate.py delete mode 100644 api/services/openapi/license_gate.py delete mode 100644 api/tests/integration_tests/controllers/openapi/test_auth.py delete mode 100644 api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py delete mode 100644 api/tests/unit_tests/libs/test_oauth_bearer_layer0_cache.py delete mode 100644 api/tests/unit_tests/libs/test_oauth_bearer_require_scope.py delete mode 100644 api/tests/unit_tests/libs/test_workspace_member_helper.py diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py index c5e8a22d466..530f2afeb0d 100644 --- a/api/controllers/openapi/_models.py +++ b/api/controllers/openapi/_models.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import StrEnum -from typing import Any, Final, Literal +from typing import Any, Final, Literal, Self from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -51,7 +51,7 @@ class MessageMetadata(BaseModel): class PaginationEnvelope[T](BaseModel): - """Canonical pagination envelope for `/openapi/v1/*` list endpoints.""" + """The one shape every paginated list on this surface answers with.""" page: int limit: int @@ -60,7 +60,7 @@ class PaginationEnvelope[T](BaseModel): data: list[T] @classmethod - def build(cls, *, page: int, limit: int, total: int, items: list[T]) -> PaginationEnvelope[T]: + def build(cls, *, page: int, limit: int, total: int, items: list[T]) -> Self: return cls(page=page, limit=limit, total=total, has_more=page * limit < total, data=items) @@ -74,20 +74,12 @@ class AppListRow(BaseModel): workspace_name: str | None = None -class AppListResponse(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[AppListRow] +class AppListResponse(PaginationEnvelope[AppListRow]): + pass -class PermittedExternalAppsListResponse(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[AppListRow] +class PermittedExternalAppsListResponse(PaginationEnvelope[AppListRow]): + pass class AppInfo(BaseModel): @@ -109,29 +101,6 @@ class AppDescribeResponse(BaseModel): input_schema: dict[str, Any] | None = Field(default=None) -class ChatMessageResponse(BaseModel): - event: str - task_id: str - id: str - message_id: str - conversation_id: str - mode: str - answer: str - metadata: MessageMetadata = Field(default_factory=MessageMetadata) - created_at: int - - -class CompletionMessageResponse(BaseModel): - event: str - task_id: str - id: str - message_id: str - mode: str - answer: str - metadata: MessageMetadata = Field(default_factory=MessageMetadata) - created_at: int - - class WorkflowRunData(BaseModel): id: str workflow_id: str @@ -145,13 +114,6 @@ class WorkflowRunData(BaseModel): finished_at: int | None = None -class WorkflowRunResponse(BaseModel): - workflow_run_id: str - task_id: str - mode: Literal["workflow"] = "workflow" - data: WorkflowRunData - - class AccountPayload(BaseModel): id: str email: str @@ -195,12 +157,8 @@ class SessionRow(BaseModel): expires_at: str | None = None -class SessionListResponse(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[SessionRow] +class SessionListResponse(PaginationEnvelope[SessionRow]): + pass class SessionListQuery(BaseModel): @@ -398,12 +356,8 @@ class MemberResponse(BaseModel): avatar: str | None = None -class MemberListResponse(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[MemberResponse] +class MemberListResponse(PaginationEnvelope[MemberResponse]): + pass class MemberListQuery(BaseModel): diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 5e1abe27b98..2675cac0400 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -37,8 +37,6 @@ from models.model import AppMode from services.account_service import TenantService from services.app_service import AppListParams, AppService -_ALLOWED_DESCRIBE_FIELDS: frozenset[str] = frozenset({"info", "parameters", "input_schema"}) - def _is_listable(app: App) -> bool: """Whether the openapi app face exposes this app (curated, listable types only).""" @@ -155,7 +153,7 @@ class AppListApi(Resource): def get(self, session: Session, *, auth_data: AuthData, query: AppListQuery): workspace_id = query.workspace_id - empty = AppListResponse(page=query.page, limit=query.limit, total=0, has_more=False, data=[]) + empty = AppListResponse.build(page=query.page, limit=query.limit, total=0, items=[]) if query.name: try: @@ -205,7 +203,7 @@ class AppListApi(Resource): workspace_id=str(workspace_id), workspace_name=tenant_name, ) - env = AppListResponse(page=1, limit=1, total=1, has_more=False, data=[item]) + env = AppListResponse.build(page=1, limit=1, total=1, items=[item]) return env params = AppListParams( @@ -244,11 +242,5 @@ class AppListApi(Resource): if _is_listable(r) ] - env = AppListResponse( - page=query.page, - limit=query.limit, - total=pagination.total, - has_more=query.page * query.limit < pagination.total, - data=items, - ) + env = AppListResponse.build(page=query.page, limit=query.limit, total=pagination.total, items=items) return env diff --git a/api/controllers/openapi/apps_permitted_external.py b/api/controllers/openapi/apps_permitted_external.py index a56ab4ae621..284ec0713d9 100644 --- a/api/controllers/openapi/apps_permitted_external.py +++ b/api/controllers/openapi/apps_permitted_external.py @@ -52,8 +52,8 @@ class PermittedExternalAppsListApi(Resource): ) if not page_result.app_ids: - env = PermittedExternalAppsListResponse( - page=query.page, limit=query.limit, total=page_result.total, has_more=False, data=[] + env = PermittedExternalAppsListResponse.build( + page=query.page, limit=query.limit, total=page_result.total, items=[] ) return env @@ -80,12 +80,8 @@ class PermittedExternalAppsListApi(Resource): workspace_name=tenant.name if tenant else None, ) ) - env = PermittedExternalAppsListResponse( - page=query.page, - limit=query.limit, - total=page_result.total, - has_more=query.page * query.limit < page_result.total, - data=items, + env = PermittedExternalAppsListResponse.build( + page=query.page, limit=query.limit, total=page_result.total, items=items ) return env diff --git a/api/controllers/openapi/auth/data.py b/api/controllers/openapi/auth/data.py index ab1d311fb06..de6f6eaf0fa 100644 --- a/api/controllers/openapi/auth/data.py +++ b/api/controllers/openapi/auth/data.py @@ -42,10 +42,8 @@ class AuthData(BaseModel): required_scope: Scope | None = None token_type: TokenType account_id: uuid.UUID | None = None - token_hash: str token_id: uuid.UUID | None = None scopes: frozenset[Scope] - tenants: dict[str, bool] = Field(default_factory=dict) external_identity: ExternalIdentity | None = None path_params: dict[str, str] = Field(default_factory=dict) diff --git a/api/controllers/openapi/auth/pipeline.py b/api/controllers/openapi/auth/pipeline.py index 12c3c9dce6c..bb32927725d 100644 --- a/api/controllers/openapi/auth/pipeline.py +++ b/api/controllers/openapi/auth/pipeline.py @@ -75,10 +75,8 @@ class AuthPipeline: data = AuthData( token_type=identity.token_type, account_id=identity.account_id, - token_hash=identity.token_hash, token_id=identity.token_id, scopes=frozenset(identity.scopes), - tenants=dict(identity.verified_tenants), required_scope=scope, allowed_roles=allowed_roles, rbac=rbac, diff --git a/api/controllers/openapi/auth/surface_gate.py b/api/controllers/openapi/auth/surface_gate.py deleted file mode 100644 index f3d2d0735bb..00000000000 --- a/api/controllers/openapi/auth/surface_gate.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Surface gate. - -`@accept_subjects(...)` is the route-level form. `SurfaceCheck` (pipeline -step) is the pipeline-level form. Both delegate to `check_surface` so the -audit emit + canonical-path message are single-sourced. - -Subjects come from `libs.oauth_bearer.SubjectType` directly — no parallel -vocabulary. Caller hits the wrong surface → 403 ``wrong_surface`` + audit -``openapi.wrong_surface_denied``. -""" - -from __future__ import annotations - -from collections.abc import Callable -from functools import wraps -from typing import TypeVar - -from flask import request -from werkzeug.exceptions import Forbidden - -from controllers.openapi._audit import emit_wrong_surface -from libs.oauth_bearer import SubjectType, try_get_auth_ctx - -_CANONICAL_PATH: dict[SubjectType, str] = { - SubjectType.ACCOUNT: "/openapi/v1/apps", - SubjectType.EXTERNAL_SSO: "/openapi/v1/permitted-external-apps", -} - -F = TypeVar("F", bound=Callable[..., object]) - - -def check_surface(accepted: frozenset[SubjectType]) -> None: - """Enforce that the resolved subject is in ``accepted``. - - Reads the openapi auth ContextVar via :func:`try_get_auth_ctx`. Raises - ``Forbidden`` with ``wrong_surface`` + canonical-path hint on miss; - emits ``openapi.wrong_surface_denied`` audit. If no auth context is - set the bearer layer didn't run — that's a wiring bug, not a - user-driven failure, so surface it as a ``RuntimeError`` instead of - a silent 403. - """ - ctx = try_get_auth_ctx() - if ctx is None: - raise RuntimeError( - "check_surface called without an auth context; stack validate_bearer or BearerCheck above the surface gate" - ) - - subject = _coerce_subject_type(getattr(ctx, "subject_type", None)) - if subject in accepted: - return - - canonical = _CANONICAL_PATH.get(subject, "/openapi/v1/") if subject else "/openapi/v1/" - emit_wrong_surface( - subject_type=subject.value if subject else None, - attempted_path=request.path, - client_id=getattr(ctx, "client_id", None), - token_id=_stringify(getattr(ctx, "token_id", None)), - ) - raise Forbidden(description=f"wrong_surface (canonical: {canonical})") - - -def accept_subjects(*accepted: SubjectType) -> Callable[[F], F]: - accepted_set: frozenset[SubjectType] = frozenset(accepted) - - def deco(fn: F) -> F: - @wraps(fn) - def wrapper(*args: object, **kwargs: object) -> object: - check_surface(accepted_set) - return fn(*args, **kwargs) - - return wrapper # type: ignore[return-value] - - return deco - - -def _coerce_subject_type(raw: object) -> SubjectType | None: - match raw: - case None: - return None - case SubjectType(): - return raw - case str(): - return SubjectType(raw) - return None - - -def _stringify(value: object) -> str | None: - if value is None: - return None - return str(value) diff --git a/api/controllers/openapi/auth/verify.py b/api/controllers/openapi/auth/verify.py index 7420841a8f3..c669bfaecd9 100644 --- a/api/controllers/openapi/auth/verify.py +++ b/api/controllers/openapi/auth/verify.py @@ -8,7 +8,7 @@ from controllers.common.rbac import enforce_rbac_checks from controllers.openapi.auth.data import AuthData, CallerKind from extensions.ext_database import db from libs.oauth_bearer import Scope, TokenType -from services.account_service import AccountService, TenantService +from services.account_service import AccountService from services.enterprise.enterprise_service import EnterpriseService, WebAppAccessMode @@ -77,13 +77,6 @@ def check_app_api_enabled(data: AuthData) -> None: raise Forbidden("service_api_disabled") -def check_app_access(data: AuthData) -> None: - if data.tenant is None: - return - if not TenantService.account_belongs_to_tenant(data.account_id, data.tenant.id, session=db.session()): - raise Forbidden("subject_no_app_access") - - _ALLOWED_MODES_BY_TOKEN_TYPE: dict[TokenType, frozenset[WebAppAccessMode]] = { TokenType.OAUTH_ACCOUNT: frozenset( { diff --git a/api/controllers/openapi/workspaces.py b/api/controllers/openapi/workspaces.py index 6bff0349f78..6053e09bf28 100644 --- a/api/controllers/openapi/workspaces.py +++ b/api/controllers/openapi/workspaces.py @@ -163,12 +163,11 @@ class WorkspaceMembersApi(Resource): total = len(members) start = (query.page - 1) * query.limit page_items = members[start : start + query.limit] - return MemberListResponse( + return MemberListResponse.build( page=query.page, limit=query.limit, total=total, - has_more=query.page * query.limit < total, - data=[_member_response(m) for m in page_items], + items=[_member_response(m) for m in page_items], ) @auth_router.guard_workspace( diff --git a/api/libs/oauth_bearer.py b/api/libs/oauth_bearer.py index 89ede4b8a12..bfd96650b8d 100644 --- a/api/libs/oauth_bearer.py +++ b/api/libs/oauth_bearer.py @@ -1,9 +1,4 @@ -"""OAuth bearer primitives. - -To add a token kind: write a Resolver, add a SubjectType + Accepts member, -append a TokenKind to build_registry, and update _SUBJECT_TO_ACCEPT. -Authenticator + validate_bearer stay untouched. -""" +"""OAuth bearer primitives.""" from __future__ import annotations @@ -13,23 +8,19 @@ import logging import uuid from collections.abc import Callable, Iterable from contextvars import ContextVar, Token -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import UTC, datetime from enum import StrEnum from functools import wraps -from typing import Literal, ParamSpec, Protocol, TypeVar +from typing import Literal, Protocol -from flask import request -from sqlalchemy import select, update +from sqlalchemy import update from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden, ServiceUnavailable, Unauthorized +from werkzeug.exceptions import ServiceUnavailable from configs import dify_config -from enums import DeploymentEdition -from extensions.ext_database import db -from extensions.ext_redis import redis_client from libs.rate_limit import enforce_bearer_rate_limit -from models import Account, OAuthAccessToken, TenantAccountJoin +from models import OAuthAccessToken logger = logging.getLogger(__name__) @@ -53,8 +44,8 @@ class Scope(StrEnum): """Catalog of bearer scopes recognised by the openapi surface. `FULL` is the catch-all carried by `dfoa_` account tokens — it satisfies - any per-route `require_scope`. `dfoe_` tokens carry the per-feature scopes - (`APPS_RUN`, `APPS_READ_PERMITTED_EXTERNAL`). + any per-route scope requirement. `dfoe_` tokens carry the per-feature + scopes (`APPS_RUN`, `APPS_READ_PERMITTED_EXTERNAL`). """ FULL = "full" @@ -65,32 +56,12 @@ class Scope(StrEnum): WORKSPACE_WRITE = "workspace:write" -class Accepts(StrEnum): - """Subject types a route is willing to accept as caller.""" - - USER_ACCOUNT = "user_account" - USER_EXT_SSO = "user_ext_sso" - - -ACCEPT_USER_ANY: frozenset[Accepts] = frozenset({Accepts.USER_ACCOUNT, Accepts.USER_EXT_SSO}) -ACCEPT_USER_EXT_SSO: frozenset[Accepts] = frozenset({Accepts.USER_EXT_SSO}) - -_SUBJECT_TO_ACCEPT: dict[SubjectType, Accepts] = { - SubjectType.ACCOUNT: Accepts.USER_ACCOUNT, - SubjectType.EXTERNAL_SSO: Accepts.USER_EXT_SSO, -} - - @dataclass(frozen=True, slots=True) class AuthContext: """Per-request identity published via :data:`_auth_ctx_var` (see :func:`set_auth_ctx` / :func:`get_auth_ctx`). ``scopes`` / ``subject_type`` / ``token_type`` come from the TokenKind, not the DB — corrupt rows can't elevate scope. - - `verified_tenants` is a snapshot of the Layer-0 verdict cache at - authenticate time. Per-request mutations write through to Redis via - `record_layer0_verdict`; this snapshot is not updated in place (frozen). """ subject_type: SubjectType @@ -102,8 +73,6 @@ class AuthContext: token_id: uuid.UUID token_type: TokenType expires_at: datetime | None - token_hash: str - verified_tenants: dict[str, bool] = field(default_factory=dict) _auth_ctx_var: ContextVar[AuthContext] = ContextVar("openapi_auth_ctx") @@ -133,7 +102,6 @@ class ResolvedRow: client_id: str | None token_id: uuid.UUID expires_at: datetime | None - verified_tenants: dict[str, bool] = field(default_factory=dict) def to_cache(self) -> dict: return { @@ -143,7 +111,6 @@ class ResolvedRow: "client_id": self.client_id, "token_id": str(self.token_id), "expires_at": self.expires_at.isoformat() if self.expires_at else None, - "verified_tenants": dict(self.verified_tenants), } @classmethod @@ -155,29 +122,9 @@ class ResolvedRow: client_id=data.get("client_id"), token_id=uuid.UUID(data["token_id"]), expires_at=datetime.fromisoformat(data["expires_at"]) if data["expires_at"] else None, - verified_tenants=_coerce_verified_tenants(data.get("verified_tenants")), ) -def _coerce_verified_tenants(raw: object) -> dict[str, bool]: - """Tolerate legacy entries that stored 'ok'/'denied' string verdicts. - - TODO(post-v1.0): remove once the AuthContext cache TTL has fully cycled - on all live deployments (60s TTL → safe to drop one release after rollout). - """ - if not isinstance(raw, dict): - return {} - out: dict[str, bool] = {} - for k, v in raw.items(): - if isinstance(v, bool): - out[k] = v - elif v == "ok": - out[k] = True - elif v == "denied": - out[k] = False - return out - - class Resolver(Protocol): def resolve(self, token_hash: str) -> ResolvedRow | None: # pragma: no cover - contract ... @@ -279,9 +226,8 @@ class BearerAuthenticator: def authenticate(self, token: str) -> AuthContext: """Identity + per-token rate limit (single source). - Both the openapi pipeline (`BearerCheck`) and the decorator - (`validate_bearer`) call this — rate-limit fires exactly once per - request regardless of which path hosts the route. + The openapi auth pipeline is the only caller, so the rate limit fires + exactly once per request. """ kind = self._registry.find(token) if kind is None: @@ -301,8 +247,6 @@ class BearerAuthenticator: token_id=row.token_id, token_type=kind.token_type, expires_at=row.expires_at, - token_hash=token_hash, - verified_tenants=dict(row.verified_tenants), ) @@ -466,92 +410,6 @@ class _VariantResolver: ) -# ============================================================================ -# Layer 0 — workspace membership cache + helper -# ============================================================================ - - -def record_layer0_verdict(token_hash: str, tenant_id: str, verdict: bool) -> None: - """Merge a Layer-0 membership verdict into the AuthContext cache entry at - `auth:token:{hash}`. No-op if entry missing/expired/invalid — next request - rebuilds via authenticate() and re-runs Layer 0. - """ - cache_key = TOKEN_CACHE_KEY_FMT.format(hash=token_hash) - raw = redis_client.get(cache_key) - if raw is None: - return - text = raw.decode() if isinstance(raw, (bytes, bytearray)) else raw - if text == "invalid": - return - try: - data = json.loads(text) - except (ValueError, KeyError): - return - ttl = redis_client.ttl(cache_key) - if ttl <= 0: - return - data.setdefault("verified_tenants", {})[tenant_id] = verdict - redis_client.setex(cache_key, ttl, json.dumps(data)) - - -def check_workspace_membership( - *, - account_id: uuid.UUID | str, - tenant_id: str, - token_hash: str, - membership_cache: dict[str, bool] | None = None, - cached_verdicts: dict[str, bool] | None = None, -) -> None: - """Layer-0 enforcement core. Raises `Forbidden` on deny, returns on allow. - - Shared by the pipeline step (`WorkspaceMembershipCheck`) and the - inline helper (`require_workspace_member`). Caller is responsible for - short-circuiting on EE / SSO subjects before invoking — this function - runs the membership + active-status checks unconditionally. - """ - cache = membership_cache if membership_cache is not None else cached_verdicts or {} - cached = cache.get(tenant_id) - if cached is True: - return - if cached is False: - raise Forbidden("workspace_membership_revoked") - - join = db.session.execute( - select(TenantAccountJoin.id).where( - TenantAccountJoin.account_id == account_id, - TenantAccountJoin.tenant_id == tenant_id, - ) - ).scalar_one_or_none() - if join is None: - record_layer0_verdict(token_hash, tenant_id, False) - raise Forbidden("workspace_membership_revoked") - - status = db.session.execute(select(Account.status).where(Account.id == account_id)).scalar_one_or_none() - if status != "active": - record_layer0_verdict(token_hash, tenant_id, False) - raise Forbidden("workspace_membership_revoked") - - record_layer0_verdict(token_hash, tenant_id, True) - - -def require_workspace_member(ctx: AuthContext, tenant_id: str) -> None: - """AuthContext-flavoured wrapper around `check_workspace_membership`. - - No-op on EE (gateway RBAC owns tenant isolation) and for SSO subjects - (no `tenant_account_joins` row by definition). - """ - if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE: - return - if ctx.subject_type != SubjectType.ACCOUNT or ctx.account_id is None: - return - check_workspace_membership( - account_id=ctx.account_id, - tenant_id=tenant_id, - token_hash=ctx.token_hash, - membership_cache=ctx.verified_tenants, - ) - - # ============================================================================ # Decorator — route-level bearer gate # ============================================================================ @@ -574,10 +432,9 @@ def get_authenticator() -> BearerAuthenticator: def extract_bearer(req) -> str | None: """Pull the bearer token out of an HTTP request's Authorization header. - Used by both attachment paths (the ``validate_bearer`` decorator and the - openapi ``Pipeline.guard``) so the parsing rule lives in one place. Pipeline - callers extract once at the boundary and pass the token through ``Context`` - so steps stay independent of the request object. + Used by the openapi auth pipeline, which extracts once at the request + boundary so the parsing rule lives in one place and later steps stay + independent of the request object. """ header = req.headers.get("Authorization", "") scheme, _, value = header.partition(" ") @@ -586,50 +443,6 @@ def extract_bearer(req) -> str | None: return value.strip() -_DP = ParamSpec("_DP") -_DR = TypeVar("_DR") - - -def validate_bearer(*, accept: frozenset[Accepts]) -> Callable[[Callable[_DP, _DR]], Callable[_DP, _DR]]: - """Opt-in: omitting it leaves the route unauthenticated. - - Resolves user-level OAuth bearers (``dfoa_`` / ``dfoe_``). Legacy - ``app-`` keys belong to ``service_api/wraps.py:validate_app_token`` - and are rejected here as the wrong auth scheme for this surface. - """ - - def wrap(fn: Callable[_DP, _DR]) -> Callable[_DP, _DR]: - @wraps(fn) - def inner(*args: _DP.args, **kwargs: _DP.kwargs) -> _DR: - token = extract_bearer(request) - if token is None: - raise Unauthorized("missing bearer token") - - if _authenticator is None: - raise ServiceUnavailable("bearer_auth_disabled: set ENABLE_OAUTH_BEARER=true to enable") - - try: - ctx = get_authenticator().authenticate(token) - except InvalidBearerError as e: - raise Unauthorized(str(e)) - - if _SUBJECT_TO_ACCEPT[ctx.subject_type] not in accept: - raise Forbidden("token subject type not accepted here") - - # Try/finally pairing — the WSGI worker thread is reused - # across requests, so a leaked ContextVar would publish the - # previous caller's identity to the next request. - reset_token = set_auth_ctx(ctx) - try: - return fn(*args, **kwargs) - finally: - reset_auth_ctx(reset_token) - - return inner - - return wrap - - def bearer_feature_required[**P, R](fn: Callable[P, R]) -> Callable[P, R]: """503 if ENABLE_OAUTH_BEARER is off — minted tokens would be unusable without the authenticator, so fail fast instead of approving silently. @@ -644,29 +457,6 @@ def bearer_feature_required[**P, R](fn: Callable[P, R]) -> Callable[P, R]: return inner -def require_scope(scope: Scope) -> Callable: - """Route-level scope gate — must run AFTER validate_bearer so that - the auth ContextVar is set. Raises ``Forbidden('insufficient_scope: ')`` - when the bearer lacks both the requested scope and ``Scope.FULL``. - """ - - def wrap(fn: Callable) -> Callable: - @wraps(fn) - def inner(*args, **kwargs): - ctx = try_get_auth_ctx() - if ctx is None: - raise RuntimeError( - "require_scope used without validate_bearer; stack @validate_bearer above @require_scope" - ) - if Scope.FULL not in ctx.scopes and scope not in ctx.scopes: - raise Forbidden(f"insufficient_scope: {scope}") - return fn(*args, **kwargs) - - return inner - - return wrap - - # ============================================================================ # Wiring — called once from the app factory # ============================================================================ diff --git a/api/services/openapi/license_gate.py b/api/services/openapi/license_gate.py deleted file mode 100644 index 8df92eb344b..00000000000 --- a/api/services/openapi/license_gate.py +++ /dev/null @@ -1,53 +0,0 @@ -"""License gate for the /openapi/v1/permitted-external-apps* surface. - -Enterprise-edition only. Community and Cloud deployments skip the gate entirely — -the EE blueprint chain is what gives CE deploys no callers on this surface -in practice, but the explicit short-circuit avoids any test/fixture that -flips the surface on without flipping the license. - -Uses the narrow system license policy shared with Console admission. - -Companion to ``controllers.console.wraps.enterprise_license_required`` — -that one is for console (cookie-authed, force-logout 401). This one is -for bearer surface (token-authed, 403 ``license_required``). -""" - -from __future__ import annotations - -import logging -from collections.abc import Callable -from functools import wraps - -from werkzeug.exceptions import Forbidden - -from configs import dify_config -from enums import DeploymentEdition -from services.entities.feature_entities import LicenseStatus -from services.system_feature_service import SystemFeatureService - -logger = logging.getLogger(__name__) - -_VALID_LICENSE_STATUSES: frozenset[LicenseStatus] = frozenset({LicenseStatus.ACTIVE, LicenseStatus.EXPIRING}) - - -def license_required[**P, R](view: Callable[P, R]) -> Callable[P, R]: - """Decorator form. Raises ``Forbidden('license_required')`` when the EE - deployment has no valid license. No-op outside the Enterprise edition. - """ - - @wraps(view) - def decorated(*args: P.args, **kwargs: P.kwargs) -> R: - if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE and not _is_license_valid(): - raise Forbidden(description="license_required") - return view(*args, **kwargs) - - return decorated - - -def _is_license_valid() -> bool: - try: - license_status = SystemFeatureService.get_license_status() - except Exception: - logger.exception("license_gate: SystemFeatureService.get_license_status failed") - return False - return license_status in _VALID_LICENSE_STATUSES diff --git a/api/tests/integration_tests/controllers/openapi/test_auth.py b/api/tests/integration_tests/controllers/openapi/test_auth.py deleted file mode 100644 index 956a0904dab..00000000000 --- a/api/tests/integration_tests/controllers/openapi/test_auth.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Integration tests for the /openapi/v1 bearer auth surface. - -Layer 0 (workspace membership), per-token rate limit, and read-scope (`apps:read`) -acceptance/rejection on app-scoped routes. -""" - -from __future__ import annotations - -from collections.abc import Generator - -import pytest -from flask import Flask -from flask.testing import FlaskClient - -from enums import DeploymentEdition -from extensions.ext_database import db -from models import App, Tenant - - -def test_info_accepts_account_bearer_with_apps_read_scope( - test_client: FlaskClient, - app_in_workspace: App, - account_token: str, -) -> None: - res = test_client.get( - f"/openapi/v1/apps/{app_in_workspace.id}/info", - headers={"Authorization": f"Bearer {account_token}"}, - ) - assert res.status_code == 200 - assert res.json["id"] == app_in_workspace.id - - -@pytest.fixture -def other_workspace_app(flask_app: Flask) -> Generator[App, None, None]: - """A fresh app under a *different* tenant — caller has no membership row.""" - with flask_app.app_context(): - other_tenant = Tenant(name="other", status="normal") - db.session.add(other_tenant) - db.session.commit() - app = App( - tenant_id=other_tenant.id, - name="b", - mode="chat", - status="normal", - enable_site=True, - enable_api=True, - ) - db.session.add(app) - db.session.commit() - yield app - db.session.delete(app) - db.session.delete(other_tenant) - db.session.commit() - - -def test_layer0_denies_account_bearer_without_membership( - test_client: FlaskClient, - account_token: str, - other_workspace_app: App, -) -> None: - """Account A bearer hitting an app under tenant B — Layer 0 denies on CE.""" - res = test_client.get( - f"/openapi/v1/apps/{other_workspace_app.id}/info", - headers={"Authorization": f"Bearer {account_token}"}, - ) - assert res.status_code == 403 - assert res.json.get("message") == "workspace_membership_revoked" - - -def test_layer0_skipped_for_enterprise_edition( - test_client: FlaskClient, - account_token: str, - other_workspace_app: App, - monkeypatch, -) -> None: - """On EE, Layer 0 short-circuits — gateway RBAC owns tenant isolation. - - /info uses validate_bearer + require_workspace_member inline (no - AppAuthzCheck), so a cross-tenant bearer reaches the app lookup and - gets 200 — gateway is expected to enforce isolation upstream. - """ - from configs import dify_config - - # Override the conftest autouse default for this test only. - monkeypatch.setattr(dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE) - - res = test_client.get( - f"/openapi/v1/apps/{other_workspace_app.id}/info", - headers={"Authorization": f"Bearer {account_token}"}, - ) - assert res.status_code == 200 - assert res.json.get("message") != "workspace_membership_revoked" - - -def test_rate_limit_returns_429_after_60_requests( - test_client: FlaskClient, - account_token: str, -) -> None: - """61st sequential GET to /account on the same bearer → 429 with Retry-After.""" - headers = {"Authorization": f"Bearer {account_token}"} - for i in range(60): - r = test_client.get("/openapi/v1/account", headers=headers) - assert r.status_code == 200, f"unexpected fail at i={i}" - - r = test_client.get("/openapi/v1/account", headers=headers) - assert r.status_code == 429 - assert r.headers.get("Retry-After"), "Retry-After header missing" - assert int(r.headers["Retry-After"]) >= 1 - body = r.json or {} - assert body.get("error") == "rate_limited" - assert isinstance(body.get("retry_after_ms"), int) - assert body["retry_after_ms"] >= 1000 - - -def test_rate_limit_bucket_shared_across_surfaces( - test_client: FlaskClient, - app_in_workspace: App, - account_token: str, -) -> None: - """30 calls to /account + 30 calls to /apps//info on same token → 61st 429s.""" - headers = {"Authorization": f"Bearer {account_token}"} - for _ in range(30): - assert test_client.get("/openapi/v1/account", headers=headers).status_code == 200 - for _ in range(30): - assert test_client.get(f"/openapi/v1/apps/{app_in_workspace.id}/info", headers=headers).status_code == 200 - - r = test_client.get("/openapi/v1/account", headers=headers) - assert r.status_code == 429 diff --git a/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py b/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py index 2047ee765d1..1b7894bcb8a 100644 --- a/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py +++ b/api/tests/test_containers_integration_tests/controllers/openapi/conftest.py @@ -82,7 +82,6 @@ def auth_for( return AuthData( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.UUID(str(account.id)), - token_hash="integration-test", token_id=token_id, scopes=frozenset({Scope.FULL}), caller=account, diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py index d61a38848f0..e77bb15c9cc 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py @@ -157,7 +157,6 @@ def _selected_webapp_steps(*, scope, app_access_mode): ) data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="x", scopes=frozenset({scope}) if scope is not None else frozenset(), app_access_mode=app_access_mode, ) diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py index 59776892903..8c3f1bc20ed 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py @@ -1,3 +1,4 @@ +import uuid from unittest.mock import patch from controllers.common.rbac import PlainApp, RBACCheck @@ -27,6 +28,9 @@ from models.account import TenantAccountRole from services.enterprise.enterprise_service import WebAppAccessMode from tests.unit_tests.config_override import config_overrides_context +PROBE_ID = uuid.uuid4() +OTHER_ID = uuid.uuid4() + def _ctx(token_type=TokenType.OAUTH_ACCOUNT, path_params=None, **kwargs): return RequestContext( @@ -37,7 +41,7 @@ def _ctx(token_type=TokenType.OAUTH_ACCOUNT, path_params=None, **kwargs): def _data(**kwargs): - defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "token_hash": "x", "scopes": frozenset()} + defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "scopes": frozenset()} defaults.update(kwargs) return AuthData(**defaults) @@ -89,9 +93,9 @@ def test_data_cond_returns_false_when_data_none(): def test_data_cond_evaluates_when_data_present(): - c = data_cond(lambda data: data.token_hash == "secret") - assert c(_ctx(), _data(token_hash="secret")) is True - assert c(_ctx(), _data(token_hash="other")) is False + c = data_cond(lambda data: data.token_id == PROBE_ID) + assert c(_ctx(), _data(token_id=PROBE_ID)) is True + assert c(_ctx(), _data(token_id=OTHER_ID)) is False def test_config_cond_ignores_ctx_and_data(): diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_data.py b/api/tests/unit_tests/controllers/openapi/auth/test_data.py index e5171e64303..73aed7bd615 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_data.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_data.py @@ -39,7 +39,6 @@ def test_request_context_scope_optional(): def test_auth_data_is_mutable(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset({Scope.FULL}), ) data.token_type = TokenType.OAUTH_EXTERNAL_SSO @@ -49,7 +48,6 @@ def test_auth_data_is_mutable(): def test_auth_data_path_params_defaults_empty(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), ) assert data.path_params == {} @@ -58,7 +56,6 @@ def test_auth_data_path_params_defaults_empty(): def test_auth_data_account_id_optional(): data = AuthData( token_type=TokenType.OAUTH_EXTERNAL_SSO, - token_hash="abc", scopes=frozenset({Scope.APPS_RUN}), external_identity=ExternalIdentity(email="u@sso.com"), ) @@ -69,25 +66,14 @@ def test_auth_data_external_identity_none_for_account(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.uuid4(), - token_hash="abc", scopes=frozenset({Scope.FULL}), ) assert data.external_identity is None -def test_auth_data_tenants_default_empty(): - data = AuthData( - token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", - scopes=frozenset(), - ) - assert data.tenants == {} - - def test_auth_data_token_id_optional(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), ) assert data.token_id is None @@ -119,7 +105,6 @@ def test_request_context_allowed_roles_set(): def test_auth_data_allowed_roles_default_none(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), ) assert data.allowed_roles is None @@ -131,7 +116,6 @@ def test_auth_data_allowed_roles_set(): roles = frozenset({TenantAccountRole.ADMIN}) data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), allowed_roles=roles, ) @@ -141,7 +125,6 @@ def test_auth_data_allowed_roles_set(): def test_auth_data_tenant_role_default_none(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), ) assert data.tenant_role is None @@ -152,7 +135,6 @@ def test_auth_data_tenant_role_set(): data = AuthData( token_type=TokenType.OAUTH_ACCOUNT, - token_hash="abc", scopes=frozenset(), tenant_role=TenantAccountRole.ADMIN, ) diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_flow.py b/api/tests/unit_tests/controllers/openapi/auth/test_flow.py index 3ea7ac2b125..1c87c3b6450 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_flow.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_flow.py @@ -11,7 +11,7 @@ def _ctx(): def _data(): - return AuthData(token_type=TokenType.OAUTH_ACCOUNT, token_hash="x", scopes=frozenset()) + return AuthData(token_type=TokenType.OAUTH_ACCOUNT, scopes=frozenset()) def test_applies_returns_true_when_condition_true(): diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py b/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py index f483c80eb38..fc215fa4dd1 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_pipeline.py @@ -16,20 +16,16 @@ def _make_identity( token_type=TokenType.OAUTH_ACCOUNT, account_id=None, scopes=None, - token_hash="testhash", subject_email=None, subject_issuer=None, - verified_tenants=None, token_id=None, ): identity = MagicMock() identity.token_type = token_type identity.account_id = account_id or uuid.uuid4() identity.scopes = scopes or frozenset({Scope.FULL}) - identity.token_hash = token_hash identity.subject_email = subject_email identity.subject_issuer = subject_issuer - identity.verified_tenants = verified_tenants or {} identity.token_id = token_id or uuid.uuid4() return identity diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py index a9ae90a3463..fa844520e3e 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py @@ -33,7 +33,6 @@ ACCOUNT_ID = "00000000-0000-0000-0000-000000000003" def _make_auth_data(**kwargs: object) -> AuthData: return AuthData( token_type=kwargs.pop("token_type", TokenType.OAUTH_ACCOUNT), - token_hash=kwargs.pop("token_hash", "testhash"), scopes=kwargs.pop("scopes", frozenset()), **kwargs, ) diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_verify.py b/api/tests/unit_tests/controllers/openapi/auth/test_verify.py index 89c38c59e4d..baad1264abf 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_verify.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_verify.py @@ -10,7 +10,6 @@ from controllers.common.rbac import PlainApp, RBACCheck from controllers.openapi.auth.data import AuthData from controllers.openapi.auth.verify import ( check_acl, - check_app_access, check_app_api_enabled, check_private_app_permission, check_rbac_permission, @@ -32,7 +31,7 @@ def _rbac_config(config_overrides: Callable[..., None]) -> None: def _data(**kwargs) -> AuthData: - defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "token_hash": "hash", "scopes": frozenset({Scope.FULL})} + defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "scopes": frozenset({Scope.FULL})} defaults.update(kwargs) return AuthData(**defaults) @@ -63,27 +62,6 @@ def test_check_workspace_member_passes_when_role_present(): check_workspace_member(_data(tenant_role=TenantAccountRole.NORMAL)) -def test_check_app_access_passes_when_tenant_none(): - check_app_access(_data(tenant=None)) - - -def test_check_app_access_passes_when_member(): - tenant = Tenant(name="Test Tenant") - tenant.id = "t1" - data = _data(account_id=uuid.uuid4(), tenant=tenant) - with patch("controllers.openapi.auth.verify.TenantService.account_belongs_to_tenant", return_value=True): - check_app_access(data) - - -def test_check_app_access_raises_when_not_member(): - tenant = Tenant(name="Test Tenant") - tenant.id = "t1" - data = _data(account_id=uuid.uuid4(), tenant=tenant) - with patch("controllers.openapi.auth.verify.TenantService.account_belongs_to_tenant", return_value=False): - with pytest.raises(Forbidden, match="subject_no_app_access"): - check_app_access(data) - - # --- check_rbac_permission --- _RBAC_REQ = RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()) diff --git a/api/tests/unit_tests/controllers/openapi/conftest.py b/api/tests/unit_tests/controllers/openapi/conftest.py index 6d17dcaa39c..f77e35a3b7f 100644 --- a/api/tests/unit_tests/controllers/openapi/conftest.py +++ b/api/tests/unit_tests/controllers/openapi/conftest.py @@ -28,7 +28,6 @@ def _stub_execute( kwargs["auth_data"] = AuthData( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.uuid4(), - token_hash="test", token_id=uuid.uuid4(), scopes=frozenset({Scope.FULL}), required_scope=scope, diff --git a/api/tests/unit_tests/controllers/openapi/test_app_run_streaming.py b/api/tests/unit_tests/controllers/openapi/test_app_run_streaming.py index 7672e6414fd..88af3d3ff8f 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_run_streaming.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_run_streaming.py @@ -109,7 +109,6 @@ def test_stop_task_calls_queue_manager_and_graph_engine(app: Flask, bypass_pipel auth_data = AuthData.model_construct( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.UUID(_TEST_ACCOUNT_ID), - token_hash="test", scopes=frozenset({Scope.FULL}), app=_make_app(), caller=_make_account(), diff --git a/api/tests/unit_tests/controllers/openapi/test_human_input_form.py b/api/tests/unit_tests/controllers/openapi/test_human_input_form.py index 28947c69b35..97590ad6dfe 100644 --- a/api/tests/unit_tests/controllers/openapi/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/openapi/test_human_input_form.py @@ -26,7 +26,6 @@ def _make_auth_data(app_model, caller, caller_kind): return AuthData.model_construct( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.uuid4(), - token_hash="test", scopes=frozenset({Scope.FULL}), app=app_model, caller=caller, diff --git a/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py b/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py deleted file mode 100644 index 7b84911d788..00000000000 --- a/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Unit tests for PaginationEnvelope generic Pydantic model.""" - -from __future__ import annotations - -from pydantic import BaseModel - -from controllers.openapi._models import PaginationEnvelope - - -class _Row(BaseModel): - id: str - name: str - - -def test_envelope_basic_fields(): - env = PaginationEnvelope[_Row](page=1, limit=20, total=42, has_more=True, data=[_Row(id="a", name="A")]) - dumped = env.model_dump(mode="json") - assert dumped == { - "page": 1, - "limit": 20, - "total": 42, - "has_more": True, - "data": [{"id": "a", "name": "A"}], - } - - -def test_envelope_empty_data_no_more(): - env = PaginationEnvelope[_Row](page=1, limit=20, total=0, has_more=False, data=[]) - assert env.model_dump(mode="json")["data"] == [] - assert env.model_dump(mode="json")["has_more"] is False - - -def test_envelope_has_more_true_when_total_exceeds_page_window(): - env = PaginationEnvelope[_Row].build(page=1, limit=20, total=42, items=[_Row(id="a", name="A")]) - assert env.has_more is True - - -def test_envelope_has_more_false_when_total_within_page_window(): - env = PaginationEnvelope[_Row].build(page=2, limit=20, total=22, items=[_Row(id="a", name="A")]) - assert env.has_more is False - - -def test_envelope_has_more_false_for_last_page(): - env = PaginationEnvelope[_Row].build(page=3, limit=20, total=42, items=[_Row(id="a", name="A")]) - assert env.has_more is False - - -def test_max_page_limit_is_200(): - from controllers.openapi._models import MAX_PAGE_LIMIT - - assert MAX_PAGE_LIMIT == 200 - - -def test_envelope_uses_pep695_generics(): - """Verify the class uses PEP 695 native generic syntax (not legacy Generic[T]).""" - from controllers.openapi._models import PaginationEnvelope - - # PEP 695 syntax populates __type_params__; the legacy Generic[T] form does not. - assert PaginationEnvelope.__type_params__, "expected PEP 695 native generic syntax" - - fields = PaginationEnvelope.model_fields - assert {"page", "limit", "total", "has_more", "data"} <= set(fields) - - -def test_app_info_response_dump_matches_spec(): - from controllers.openapi._models import AppInfo - - obj = AppInfo( - id="app1", - name="X", - description="d", - mode="chat", - ) - assert obj.model_dump(mode="json") == { - "id": "app1", - "name": "X", - "description": "d", - "mode": "chat", - } - - -def test_app_describe_response_nests_info_and_parameters(): - from controllers.openapi._models import AppDescribeInfo, AppDescribeResponse - - info = AppDescribeInfo( - id="app1", - name="X", - mode="chat", - description=None, - updated_at="2026-05-05T00:00:00+00:00", - service_api_enabled=True, - ) - obj = AppDescribeResponse(info=info, parameters={"opening_statement": None}) - dumped = obj.model_dump(mode="json") - assert dumped["info"]["service_api_enabled"] is True - assert dumped["parameters"]["opening_statement"] is None - - -def test_response_models_dump_per_mode(): - from controllers.openapi._models import ( - ChatMessageResponse, - CompletionMessageResponse, - WorkflowRunData, - WorkflowRunResponse, - ) - - chat = ChatMessageResponse( - event="message", - task_id="t1", - id="m1", - message_id="m1", - conversation_id="c1", - mode="chat", - answer="hi", - created_at=0, - ) - assert chat.model_dump(mode="json")["mode"] == "chat" - wf = WorkflowRunResponse( - workflow_run_id="r1", - task_id="t1", - data=WorkflowRunData(id="r1", workflow_id="w1", status="succeeded"), - ) - assert wf.model_dump(mode="json")["data"]["status"] == "succeeded" - assert wf.model_dump(mode="json")["mode"] == "workflow" - comp = CompletionMessageResponse( - event="message", - task_id="t2", - id="m2", - message_id="m2", - mode="completion", - answer="ok", - created_at=0, - ) - assert comp.model_dump(mode="json")["mode"] == "completion" diff --git a/api/tests/unit_tests/controllers/openapi/test_workflow_events_openapi.py b/api/tests/unit_tests/controllers/openapi/test_workflow_events_openapi.py index 2e6af990003..7f906826403 100644 --- a/api/tests/unit_tests/controllers/openapi/test_workflow_events_openapi.py +++ b/api/tests/unit_tests/controllers/openapi/test_workflow_events_openapi.py @@ -31,7 +31,6 @@ def _make_auth_data(app_model, caller, caller_kind): return AuthData.model_construct( token_type=TokenType.OAUTH_ACCOUNT, account_id=uuid.uuid4(), - token_hash="test", scopes=frozenset({Scope.FULL}), app=app_model, caller=caller, diff --git a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py index 02398cbd2ad..9b998edff57 100644 --- a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py +++ b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py @@ -9,7 +9,7 @@ Coverage: Auth-pipeline plumbing is bypassed via the `bypass_pipeline` fixture from conftest.py; the bearer identity is seeded into the openapi auth ContextVar -via `_seed` (the slot `validate_bearer` publishes). Tests that exercise +via `_seed`. Tests that exercise endpoint *bodies* skip the single `guard_workspace` decorator via ``__wrapped__`` — membership and role enforcement live in the auth pipeline and are covered in `auth/test_prepare.py` and `auth/test_verify.py`. @@ -113,8 +113,6 @@ def _auth_ctx(account_id: uuid.UUID | None = None) -> AuthContext: token_id=uuid.uuid4(), token_type=TokenType.OAUTH_ACCOUNT, expires_at=datetime.now(UTC), - token_hash="h", - verified_tenants={}, ) @@ -125,7 +123,6 @@ def _auth_data(account_id: uuid.UUID) -> AuthData: return AuthData( token_type=TokenType.OAUTH_ACCOUNT, account_id=account_id, - token_hash="testhash", scopes=frozenset({Scope.FULL}), ) diff --git a/api/tests/unit_tests/libs/test_oauth_bearer_layer0_cache.py b/api/tests/unit_tests/libs/test_oauth_bearer_layer0_cache.py deleted file mode 100644 index 0023f17119e..00000000000 --- a/api/tests/unit_tests/libs/test_oauth_bearer_layer0_cache.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Unit tests for record_layer0_verdict — merge L0 verdict into AuthContext cache.""" - -from __future__ import annotations - -import json -from unittest.mock import MagicMock, patch - -import pytest - -from libs.oauth_bearer import record_layer0_verdict - - -@pytest.fixture -def mock_redis(): - return MagicMock() - - -@patch("libs.oauth_bearer.redis_client") -def test_no_op_when_cache_entry_missing(mock_redis): - mock_redis.get.return_value = None - record_layer0_verdict("h1", "t1", True) - mock_redis.setex.assert_not_called() - - -@patch("libs.oauth_bearer.redis_client") -def test_no_op_when_cache_entry_invalid_marker(mock_redis): - mock_redis.get.return_value = b"invalid" - record_layer0_verdict("h1", "t1", True) - mock_redis.setex.assert_not_called() - - -@patch("libs.oauth_bearer.redis_client") -def test_no_op_when_json_malformed(mock_redis): - mock_redis.get.return_value = b"not json" - record_layer0_verdict("h1", "t1", True) - mock_redis.setex.assert_not_called() - - -@patch("libs.oauth_bearer.redis_client") -def test_no_op_when_ttl_expired(mock_redis): - mock_redis.get.return_value = json.dumps( - { - "subject_email": "e", - "subject_issuer": None, - "account_id": None, - "token_id": "tid", - "expires_at": None, - } - ).encode() - mock_redis.ttl.return_value = -1 - record_layer0_verdict("h1", "t1", True) - mock_redis.setex.assert_not_called() - - -@patch("libs.oauth_bearer.redis_client") -def test_merges_new_tenant_verdict(mock_redis): - mock_redis.get.return_value = json.dumps( - { - "subject_email": "e", - "subject_issuer": None, - "account_id": None, - "token_id": "tid", - "expires_at": None, - "verified_tenants": {"t0": True}, - } - ).encode() - mock_redis.ttl.return_value = 42 - - record_layer0_verdict("h1", "t1", False) - - mock_redis.setex.assert_called_once() - args = mock_redis.setex.call_args - assert args.args[0] == "auth:token:h1" - assert args.args[1] == 42 # remaining TTL preserved - written = json.loads(args.args[2]) - assert written["verified_tenants"] == {"t0": True, "t1": False} - - -@patch("libs.oauth_bearer.redis_client") -def test_merges_when_field_absent_from_legacy_entry(mock_redis): - """Backward compat: legacy cache entry without verified_tenants field.""" - mock_redis.get.return_value = json.dumps( - { - "subject_email": "e", - "subject_issuer": None, - "account_id": None, - "token_id": "tid", - "expires_at": None, - } - ).encode() - mock_redis.ttl.return_value = 42 - record_layer0_verdict("h1", "t1", True) - written = json.loads(mock_redis.setex.call_args.args[2]) - assert written["verified_tenants"] == {"t1": True} diff --git a/api/tests/unit_tests/libs/test_oauth_bearer_require_scope.py b/api/tests/unit_tests/libs/test_oauth_bearer_require_scope.py deleted file mode 100644 index e8204a6e2e0..00000000000 --- a/api/tests/unit_tests/libs/test_oauth_bearer_require_scope.py +++ /dev/null @@ -1,98 +0,0 @@ -"""require_scope is a route-level gate run after validate_bearer. -Tests use a fake auth_ctx published via the openapi auth ContextVar (no -authenticator wiring needed). The `_publish_auth_ctx` helper guarantees -the ContextVar is reset between tests so worker-thread reuse can't leak -identity into the next test. -""" - -from __future__ import annotations - -import uuid -from collections.abc import Iterator -from contextlib import contextmanager - -import pytest -from flask import Flask -from werkzeug.exceptions import Forbidden - -from libs.oauth_bearer import ( - AuthContext, - Scope, - SubjectType, - TokenType, - require_scope, - reset_auth_ctx, - set_auth_ctx, -) - - -@contextmanager -def _publish_auth_ctx(ctx: AuthContext) -> Iterator[None]: - token = set_auth_ctx(ctx) - try: - yield - finally: - reset_auth_ctx(token) - - -@pytest.fixture -def app() -> Flask: - app = Flask(__name__) - app.config["TESTING"] = True - return app - - -def _ctx(scopes) -> AuthContext: - return AuthContext( - subject_type=SubjectType.ACCOUNT, - subject_email="user@example.com", - subject_issuer="dify:account", - account_id=uuid.uuid4(), - client_id="difyctl", - scopes=scopes, - token_id=uuid.uuid4(), - token_type=TokenType.OAUTH_ACCOUNT, - expires_at=None, - token_hash="h1", - verified_tenants={}, - ) - - -def test_require_scope_allows_when_scope_present(app: Flask): - @require_scope("apps:read") - def view(): - return "ok" - - with app.test_request_context(), _publish_auth_ctx(_ctx(frozenset({"apps:read"}))): - assert view() == "ok" - - -def test_require_scope_rejects_when_scope_missing(app: Flask): - @require_scope("apps:write") - def view(): - return "ok" - - with app.test_request_context(), _publish_auth_ctx(_ctx(frozenset({"apps:read"}))): - with pytest.raises(Forbidden) as exc: - view() - assert "insufficient_scope: apps:write" in str(exc.value.description) - - -def test_require_scope_full_passes_any_check(app: Flask): - @require_scope("apps:write") - def view(): - return "ok" - - with app.test_request_context(), _publish_auth_ctx(_ctx(frozenset({Scope.FULL}))): - assert view() == "ok" - - -def test_require_scope_without_validate_bearer_raises_runtime_error(app: Flask): - @require_scope("apps:read") - def view(): - return "ok" - - with app.test_request_context(): - # No auth ContextVar published — validate_bearer was forgotten. - with pytest.raises(RuntimeError, match="stack @validate_bearer above @require_scope"): - view() diff --git a/api/tests/unit_tests/libs/test_workspace_member_helper.py b/api/tests/unit_tests/libs/test_workspace_member_helper.py deleted file mode 100644 index af9acd9cf62..00000000000 --- a/api/tests/unit_tests/libs/test_workspace_member_helper.py +++ /dev/null @@ -1,202 +0,0 @@ -"""SQLite-backed unit tests for workspace membership enforcement.""" - -from __future__ import annotations - -import uuid -from collections.abc import Iterator -from dataclasses import dataclass -from unittest.mock import Mock - -import pytest -from sqlalchemy import event -from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden - -from enums import DeploymentEdition -from libs import oauth_bearer -from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, require_workspace_member -from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole -from tests.unit_tests.config_override import apply_config_overrides -from tests.unit_tests.model_factories import make_account - -pytestmark = pytest.mark.usefixtures("community_edition") - - -@dataclass(frozen=True) -class Database: - """Real ORM binding and executed-statement log for one isolated test.""" - - session: Session - statements: list[str] - - -@pytest.fixture -def database(sqlite_session: Session, monkeypatch: pytest.MonkeyPatch) -> Iterator[Database]: - statements: list[str] = [] - - def record_statement(_connection, _cursor, statement, _parameters, _context, _executemany) -> None: - statements.append(statement) - - engine = sqlite_session.get_bind() - event.listen(engine, "before_cursor_execute", record_statement) - binding = Database(session=sqlite_session, statements=statements) - monkeypatch.setattr(oauth_bearer, "db", binding) - try: - yield binding - finally: - event.remove(engine, "before_cursor_execute", record_statement) - - -@pytest.fixture -def community_edition(monkeypatch: pytest.MonkeyPatch) -> None: - apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY) - - -def _ctx( - verified: dict[str, bool] | None = None, - *, - account_id: uuid.UUID | None = None, - account: bool = True, -) -> AuthContext: - return AuthContext( - subject_type=SubjectType.ACCOUNT if account else SubjectType.EXTERNAL_SSO, - subject_email="e@example.com", - subject_issuer=None, - account_id=account_id or (uuid.uuid4() if account else None), - client_id="difyctl", - scopes=frozenset({Scope.FULL}), - token_id=uuid.uuid4(), - token_type=TokenType.OAUTH_ACCOUNT if account else TokenType.OAUTH_EXTERNAL_SSO, - expires_at=None, - token_hash="h1", - verified_tenants=dict(verified or {}), - ) - - -def _persist_membership( - session: Session, - *, - account_id: uuid.UUID, - tenant_id: str, - status: AccountStatus = AccountStatus.ACTIVE, -) -> None: - account = _account(account_id, status=status) - tenant = Tenant(name=f"Tenant {tenant_id}") - tenant.id = tenant_id - membership = TenantAccountJoin( - tenant_id=tenant_id, - account_id=account_id.hex, - role=TenantAccountRole.NORMAL, - ) - session.add_all([account, tenant, membership]) - session.commit() - - -def _account(account_id: uuid.UUID, *, status: AccountStatus = AccountStatus.ACTIVE) -> Account: - # SQLite's StringUUID adapter binds UUID objects as compact hex, while - # PostgreSQL binds their dashed string form. Persist the SQLite-bound form - # so the production query can keep accepting the AuthContext UUID object. - return make_account( - account_id=account_id.hex, name="Workspace member", email=f"{account_id}@example.com", status=status - ) - - -def test_skips_for_enterprise_edition(database: Database, monkeypatch: pytest.MonkeyPatch) -> None: - apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE) - before = len(database.statements) - - require_workspace_member(_ctx(), "tenant-1") - - assert len(database.statements) == before - - -def test_skips_for_external_sso(database: Database) -> None: - before = len(database.statements) - - require_workspace_member(_ctx(account=False), "tenant-1") - - assert len(database.statements) == before - - -def test_uses_cached_allow_without_database_access(database: Database) -> None: - before = len(database.statements) - - require_workspace_member(_ctx({"tenant-1": True}), "tenant-1") - - assert len(database.statements) == before - - -def test_uses_cached_denial_without_database_access(database: Database) -> None: - before = len(database.statements) - - with pytest.raises(Forbidden, match="workspace_membership_revoked"): - require_workspace_member(_ctx({"tenant-1": False}), "tenant-1") - - assert len(database.statements) == before - - -@pytest.mark.usefixtures("database") -def test_denies_when_membership_is_absent( - monkeypatch: pytest.MonkeyPatch, -) -> None: - record_verdict = Mock() - monkeypatch.setattr(oauth_bearer, "record_layer0_verdict", record_verdict) - - with pytest.raises(Forbidden, match="workspace_membership_revoked"): - require_workspace_member(_ctx(), "tenant-1") - - record_verdict.assert_called_once_with("h1", "tenant-1", False) - - -def test_denies_membership_from_another_tenant( - database: Database, - monkeypatch: pytest.MonkeyPatch, -) -> None: - account_id = uuid.uuid4() - requested_tenant_member_id = uuid.uuid4() - status_decoy_id = uuid.uuid4() - _persist_membership(database.session, account_id=account_id, tenant_id="tenant-2") - _persist_membership(database.session, account_id=requested_tenant_member_id, tenant_id="tenant-1") - database.session.add(_account(status_decoy_id, status=AccountStatus.BANNED)) - database.session.commit() - record_verdict = Mock() - monkeypatch.setattr(oauth_bearer, "record_layer0_verdict", record_verdict) - - with pytest.raises(Forbidden, match="workspace_membership_revoked"): - require_workspace_member(_ctx(account_id=account_id), "tenant-1") - - record_verdict.assert_called_once_with("h1", "tenant-1", False) - - -def test_denies_when_account_is_inactive( - database: Database, - monkeypatch: pytest.MonkeyPatch, -) -> None: - account_id = uuid.uuid4() - _persist_membership( - database.session, - account_id=account_id, - tenant_id="tenant-1", - status=AccountStatus.BANNED, - ) - record_verdict = Mock() - monkeypatch.setattr(oauth_bearer, "record_layer0_verdict", record_verdict) - - with pytest.raises(Forbidden, match="workspace_membership_revoked"): - require_workspace_member(_ctx(account_id=account_id), "tenant-1") - - record_verdict.assert_called_once_with("h1", "tenant-1", False) - - -def test_allows_active_member_and_records_verdict( - database: Database, - monkeypatch: pytest.MonkeyPatch, -) -> None: - account_id = uuid.uuid4() - _persist_membership(database.session, account_id=account_id, tenant_id="tenant-1") - record_verdict = Mock() - monkeypatch.setattr(oauth_bearer, "record_layer0_verdict", record_verdict) - - require_workspace_member(_ctx(account_id=account_id), "tenant-1") - - record_verdict.assert_called_once_with("h1", "tenant-1", True) diff --git a/api/tests/unit_tests/services/openapi/test_mint_policy.py b/api/tests/unit_tests/services/openapi/test_mint_policy.py index e3bf6cd9d0e..c3df657ed4c 100644 --- a/api/tests/unit_tests/services/openapi/test_mint_policy.py +++ b/api/tests/unit_tests/services/openapi/test_mint_policy.py @@ -10,7 +10,6 @@ from __future__ import annotations import pytest -from enums import DeploymentEdition from libs.oauth_bearer import MINTABLE_PROFILES, Scope, SubjectType from services.openapi.mint_policy import MintPolicyViolation, validate_mint_policy @@ -73,50 +72,3 @@ def test_message_carries_both_drift_reasons(): msg = str(exc.value) assert "prefix" in msg assert "scopes" in msg - - -def test_license_required_decorator_skips_on_ce(config_overrides): - from services.openapi.license_gate import license_required - - @license_required - def view(): - return "ok" - - config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY) - assert view() == "ok" - - -def test_license_required_decorator_403_on_invalid_ee_license(config_overrides): - from unittest.mock import patch - - from werkzeug.exceptions import Forbidden - - from services.openapi.license_gate import license_required - - @license_required - def view(): - return "ok" - - config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE) - with ( - patch("services.openapi.license_gate._is_license_valid", return_value=False), - ): - with pytest.raises(Forbidden) as exc: - view() - assert "license_required" in exc.value.description - - -def test_license_required_decorator_passes_on_valid_ee_license(config_overrides): - from unittest.mock import patch - - from services.openapi.license_gate import license_required - - @license_required - def view(): - return "ok" - - config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE) - with ( - patch("services.openapi.license_gate._is_license_valid", return_value=True), - ): - assert view() == "ok" From 41bc173636a7f29949dbf18e3896042453a01e73 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Fri, 18 Sep 2026 06:50:39 +0000 Subject: [PATCH 17/21] chore: remove skill export dialog (#42491) --- web/features/skills/__tests__/page.spec.tsx | 36 ++------------------ web/features/skills/page.tsx | 37 +++------------------ web/i18n/locales/ar-TN/skill.json | 2 -- web/i18n/locales/de-DE/skill.json | 2 -- web/i18n/locales/en-US/skill.json | 2 -- web/i18n/locales/es-ES/skill.json | 2 -- web/i18n/locales/fa-IR/skill.json | 2 -- web/i18n/locales/fr-FR/skill.json | 2 -- web/i18n/locales/hi-IN/skill.json | 2 -- web/i18n/locales/id-ID/skill.json | 2 -- web/i18n/locales/it-IT/skill.json | 2 -- web/i18n/locales/ja-JP/skill.json | 2 -- web/i18n/locales/ko-KR/skill.json | 2 -- web/i18n/locales/lo-LA/skill.json | 2 -- web/i18n/locales/nl-NL/skill.json | 2 -- web/i18n/locales/pl-PL/skill.json | 2 -- web/i18n/locales/pt-BR/skill.json | 2 -- web/i18n/locales/ro-RO/skill.json | 2 -- web/i18n/locales/ru-RU/skill.json | 2 -- web/i18n/locales/sl-SI/skill.json | 2 -- web/i18n/locales/th-TH/skill.json | 2 -- web/i18n/locales/tr-TR/skill.json | 2 -- web/i18n/locales/uk-UA/skill.json | 2 -- web/i18n/locales/vi-VN/skill.json | 2 -- web/i18n/locales/zh-Hans/skill.json | 2 -- web/i18n/locales/zh-Hant/skill.json | 2 -- 26 files changed, 7 insertions(+), 114 deletions(-) diff --git a/web/features/skills/__tests__/page.spec.tsx b/web/features/skills/__tests__/page.spec.tsx index 8149615e037..16e581165a2 100644 --- a/web/features/skills/__tests__/page.spec.tsx +++ b/web/features/skills/__tests__/page.spec.tsx @@ -893,14 +893,7 @@ describe('SkillsPage', () => { }), ) await user.click(await screen.findByText('common.operation.export')) - const dialog = await screen.findByRole('alertdialog', { - name: 'skill.skillManagement.exportDialog.title', - }) - expect( - within(dialog).getByText('skill.skillManagement.exportDialog.description'), - ).toBeInTheDocument() - expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled() - await user.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' })) + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() await waitFor(() => { expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1') @@ -924,37 +917,12 @@ describe('SkillsPage', () => { ) await user.click(await screen.findByText('common.operation.export')) - const dialog = await screen.findByRole('alertdialog', { - name: 'skill.skillManagement.exportDialog.title', - }) - expect( - within(dialog).getByText('skill.skillManagement.exportDialog.description'), - ).toBeInTheDocument() - expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled() - await user.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' })) + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() await waitFor(() => { expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1') }) }) - it('cancels export without downloading the skill', async () => { - const user = userEvent.setup() - renderSkillsPage() - - await user.click( - await screen.findByRole('button', { - name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}', - }), - ) - await user.click(await screen.findByText('common.operation.export')) - const dialog = await screen.findByRole('alertdialog') - await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' })) - - await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()) - expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled() - expect(mocks.downloadBlob).not.toHaveBeenCalled() - }) - it('confirms deletion with the skill name and refreshes list data', async () => { const user = userEvent.setup() renderSkillsPage() diff --git a/web/features/skills/page.tsx b/web/features/skills/page.tsx index a60e8f24a8a..1e256f976e4 100644 --- a/web/features/skills/page.tsx +++ b/web/features/skills/page.tsx @@ -417,7 +417,6 @@ function SkillCard({ const descriptionId = useId() const draftStatusId = useId() const [isDeleteOpen, setIsDeleteOpen] = useState(false) - const [isExportOpen, setIsExportOpen] = useState(false) const duplicateMutation = useMutation( consoleQuery.workspaces.current.skills.bySkillId.duplicate.post.mutationOptions(), ) @@ -425,7 +424,6 @@ function SkillCard({ mutationFn: () => fetchSkillArchiveBlob(skill.id), onSuccess: (blob) => { downloadBlob({ data: blob, fileName: `${skill.name}.zip` }) - setIsExportOpen(false) }, onError: () => { toast.error(tCommon(($) => $['operation.downloadFailed'])) @@ -554,7 +552,11 @@ function SkillCard({ {tCommon(($) => $['operation.duplicate'])} )} - setIsExportOpen(true)}> +
- { - if (!exportMutation.isPending) setIsExportOpen(open) - }} - > - -
- - {t(($) => $['skillManagement.exportDialog.title'])} - - - {t(($) => $['skillManagement.exportDialog.description'])} - -
- - - {tCommon(($) => $['operation.cancel'])} - - - {tCommon(($) => $['operation.confirm'])} - - -
-
) diff --git a/web/i18n/locales/ar-TN/skill.json b/web/i18n/locales/ar-TN/skill.json index ac5c3967c86..b4e12ee82bb 100644 --- a/web/i18n/locales/ar-TN/skill.json +++ b/web/i18n/locales/ar-TN/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "لا تحتوي هذه الحزمة على SKILL.md. أضف الملف وقم باستيراده مرة أخرى.", "skillManagement.errors.nameConflict": "توجد بالفعل مهارة اسمها \"{{name}}\". أعد تسميته إلى SKILL.md وقم باستيراده مرة أخرى.", "skillManagement.errors.workspaceLimit": "وصلت مساحة العمل الخاصة بك إلى الحد الأقصى وهو 500 مهارة. احذف مهارة لإضافة مهارة جديدة.", - "skillManagement.exportDialog.description": "سيتم تصدير المسودة المحفوظة الحالية لمهارتك. إذا كانت لديك تغييرات غير محفوظة في المحرر، يُرجى حفظها قبل التصدير.", - "skillManagement.exportDialog.title": "تصدير المسودة المحفوظة الحالية", "skillManagement.import": "استيراد", "skillManagement.importFailed": "فشل استيراد المهارة.", "skillManagement.importSuccess": "Skill مستورد.", diff --git a/web/i18n/locales/de-DE/skill.json b/web/i18n/locales/de-DE/skill.json index 76b778876cd..afacee20835 100644 --- a/web/i18n/locales/de-DE/skill.json +++ b/web/i18n/locales/de-DE/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Dieses Paket enthält kein SKILL.md. Fügen Sie die Datei hinzu und importieren Sie sie erneut.", "skillManagement.errors.nameConflict": "Ein Skill mit dem Namen „{{name}}“ ist bereits vorhanden. Benennen Sie es in SKILL.md um und importieren Sie es erneut.", "skillManagement.errors.workspaceLimit": "Ihr Arbeitsbereich hat die Grenze von 500 Fertigkeiten erreicht. Löschen Sie eine Fertigkeit, um eine neue hinzuzufügen.", - "skillManagement.exportDialog.description": "Dadurch wird der aktuell gespeicherte Entwurf Ihres Skills exportiert. Speichern Sie nicht gespeicherte Änderungen im Editor vor dem Export.", - "skillManagement.exportDialog.title": "Aktuell gespeicherten Entwurf exportieren", "skillManagement.import": "Importieren", "skillManagement.importFailed": "Der Skill konnte nicht importiert werden.", "skillManagement.importSuccess": "Skill importiert.", diff --git a/web/i18n/locales/en-US/skill.json b/web/i18n/locales/en-US/skill.json index d08d0ccb0ee..883139cc51f 100644 --- a/web/i18n/locales/en-US/skill.json +++ b/web/i18n/locales/en-US/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "This package has no SKILL.md. Add the file and import it again.", "skillManagement.errors.nameConflict": "A skill named \"{{name}}\" already exists. Rename it in SKILL.md and import again.", "skillManagement.errors.workspaceLimit": "Your workspace has reached the limit of 500 skills. Delete a skill to add a new one.", - "skillManagement.exportDialog.description": "This will export the current saved draft of your skill. If you have unsaved changes in the editor, please save them before exporting.", - "skillManagement.exportDialog.title": "Export Current Saved Draft", "skillManagement.import": "Import", "skillManagement.importFailed": "Failed to import skill.", "skillManagement.importSuccess": "Skill imported.", diff --git a/web/i18n/locales/es-ES/skill.json b/web/i18n/locales/es-ES/skill.json index e5c63859a95..11c8ffec02b 100644 --- a/web/i18n/locales/es-ES/skill.json +++ b/web/i18n/locales/es-ES/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Este paquete no tiene SKILL.md. Agregue el archivo e impórtelo nuevamente.", "skillManagement.errors.nameConflict": "Ya existe una habilidad denominada \"{{name}}\". Cambie el nombre a SKILL.md e impórtelo nuevamente.", "skillManagement.errors.workspaceLimit": "Tu espacio de trabajo ha alcanzado el límite de 500 habilidades. Elimina una habilidad para agregar una nueva.", - "skillManagement.exportDialog.description": "Se exportará el borrador guardado actual de tu habilidad. Si tienes cambios sin guardar en el editor, guárdalos antes de exportar.", - "skillManagement.exportDialog.title": "Exportar el borrador guardado actual", "skillManagement.import": "Importar", "skillManagement.importFailed": "No se pudo importar la habilidad.", "skillManagement.importSuccess": "Skill importado.", diff --git a/web/i18n/locales/fa-IR/skill.json b/web/i18n/locales/fa-IR/skill.json index e4ee9eaf838..7d340c2285b 100644 --- a/web/i18n/locales/fa-IR/skill.json +++ b/web/i18n/locales/fa-IR/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "این بسته فاقد SKILL.md است. فایل را اضافه کنید و دوباره آن را وارد کنید.", "skillManagement.errors.nameConflict": "مهارتی به نام \"{{name}}\" از قبل وجود دارد. نام آن را در SKILL.md تغییر دهید و دوباره وارد کنید.", "skillManagement.errors.workspaceLimit": "فضای کاری شما به حداکثر 500 مهارت رسیده است. برای افزودن مهارت جدید، مهارت را حذف کنید.", - "skillManagement.exportDialog.description": "با این کار از پیش‌نویس ذخیره‌شدهٔ فعلی مهارت شما خروجی گرفته می‌شود. اگر در ویرایشگر تغییرات ذخیره‌نشده‌ای دارید، پیش از خروجی گرفتن آن‌ها را ذخیره کنید.", - "skillManagement.exportDialog.title": "خروجی گرفتن از پیش‌نویس ذخیره‌شدهٔ فعلی", "skillManagement.import": "وارد کردن", "skillManagement.importFailed": "وارد کردن مهارت انجام نشد.", "skillManagement.importSuccess": "Skill وارد شده است.", diff --git a/web/i18n/locales/fr-FR/skill.json b/web/i18n/locales/fr-FR/skill.json index bef2f785aa6..3ecc767e05f 100644 --- a/web/i18n/locales/fr-FR/skill.json +++ b/web/i18n/locales/fr-FR/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Ce package ne contient pas de SKILL.md. Ajoutez le fichier et importez-le à nouveau.", "skillManagement.errors.nameConflict": "Une compétence nommée \"{{name}}\" existe déjà. Renommez-le en SKILL.md et importez-le à nouveau.", "skillManagement.errors.workspaceLimit": "Votre espace de travail a atteint la limite de 500 compétences. Supprimez une compétence pour en ajouter une nouvelle.", - "skillManagement.exportDialog.description": "Le brouillon actuellement enregistré de votre compétence sera exporté. Si des modifications ne sont pas enregistrées dans l’éditeur, enregistrez-les avant l’exportation.", - "skillManagement.exportDialog.title": "Exporter le brouillon actuellement enregistré", "skillManagement.import": "Importer", "skillManagement.importFailed": "Échec de l'importation de la compétence.", "skillManagement.importSuccess": "Skill importé.", diff --git a/web/i18n/locales/hi-IN/skill.json b/web/i18n/locales/hi-IN/skill.json index ea41c068917..32696bf5ec4 100644 --- a/web/i18n/locales/hi-IN/skill.json +++ b/web/i18n/locales/hi-IN/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "इस पैकेज में कोई SKILL.md नहीं है। फ़ाइल जोड़ें और इसे फिर से आयात करें।", "skillManagement.errors.nameConflict": "\"{{name}}\" नामक कौशल पहले से मौजूद है। इसे SKILL.md में नाम बदलें और फिर से आयात करें।", "skillManagement.errors.workspaceLimit": "आपका कार्यक्षेत्र 500 कौशल की सीमा तक पहुंच गया है। नया जोड़ने के लिए एक कौशल हटाएँ।", - "skillManagement.exportDialog.description": "इससे आपके कौशल का वर्तमान सहेजा गया ड्राफ़्ट निर्यात होगा। यदि संपादक में कोई बदलाव सहेजे नहीं गए हैं, तो निर्यात करने से पहले उन्हें सहेज लें।", - "skillManagement.exportDialog.title": "वर्तमान सहेजा गया ड्राफ़्ट निर्यात करें", "skillManagement.import": "आयात करें", "skillManagement.importFailed": "कौशल आयात करने में विफल.", "skillManagement.importSuccess": "Skill आयातित.", diff --git a/web/i18n/locales/id-ID/skill.json b/web/i18n/locales/id-ID/skill.json index 0453a6f1d38..17b93d404ca 100644 --- a/web/i18n/locales/id-ID/skill.json +++ b/web/i18n/locales/id-ID/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Paket ini tidak memiliki SKILL.md. Tambahkan file dan impor lagi.", "skillManagement.errors.nameConflict": "Skill bernama \"{{name}}\" sudah ada. Ubah namanya menjadi SKILL.md dan impor lagi.", "skillManagement.errors.workspaceLimit": "Ruang kerja Anda telah mencapai batas 500 keterampilan. Hapus keterampilan untuk menambahkan keterampilan baru.", - "skillManagement.exportDialog.description": "Ini akan mengekspor draf keterampilan Anda yang tersimpan saat ini. Jika ada perubahan yang belum disimpan di editor, simpan terlebih dahulu sebelum mengekspor.", - "skillManagement.exportDialog.title": "Ekspor Draf Tersimpan Saat Ini", "skillManagement.import": "Impor", "skillManagement.importFailed": "Gagal mengimpor keterampilan.", "skillManagement.importSuccess": "Skill diimpor.", diff --git a/web/i18n/locales/it-IT/skill.json b/web/i18n/locales/it-IT/skill.json index 8d97c77210a..19b818ec852 100644 --- a/web/i18n/locales/it-IT/skill.json +++ b/web/i18n/locales/it-IT/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Questo pacchetto non ha SKILL.md. Aggiungi il file e importalo di nuovo.", "skillManagement.errors.nameConflict": "Esiste già una competenza denominata \"{{name}}\". Rinominalo in SKILL.md e importalo di nuovo.", "skillManagement.errors.workspaceLimit": "La tua area di lavoro ha raggiunto il limite di 500 competenze. Elimina una competenza per aggiungerne una nuova.", - "skillManagement.exportDialog.description": "Verrà esportata la bozza attualmente salvata della tua competenza. Se ci sono modifiche non salvate nell’editor, salvale prima di esportare.", - "skillManagement.exportDialog.title": "Esporta la bozza attualmente salvata", "skillManagement.import": "Importa", "skillManagement.importFailed": "Impossibile importare la competenza.", "skillManagement.importSuccess": "Skill importato.", diff --git a/web/i18n/locales/ja-JP/skill.json b/web/i18n/locales/ja-JP/skill.json index 5a7eccf4b93..ddd892f6696 100644 --- a/web/i18n/locales/ja-JP/skill.json +++ b/web/i18n/locales/ja-JP/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "このスキルパッケージには SKILL.md がありません。SKILL.md を追加してから、もう一度インポートしてください。", "skillManagement.errors.nameConflict": "「{{name}}」という名前のスキルはすでに存在します。SKILL.md で名前を変更してから、もう一度インポートしてください。", "skillManagement.errors.workspaceLimit": "ワークスペースに追加できるスキルは 500 個までです。スキルを 1 つ削除してから、新しいスキルを追加してください。", - "skillManagement.exportDialog.description": "このスキルの保存済みの現在の下書きをエクスポートします。エディターに未保存の変更がある場合は、エクスポートする前に保存してください。", - "skillManagement.exportDialog.title": "保存済みの現在の下書きをエクスポート", "skillManagement.import": "インポート", "skillManagement.importFailed": "スキルのインポートに失敗しました。", "skillManagement.importSuccess": "Skill がインポートされました。", diff --git a/web/i18n/locales/ko-KR/skill.json b/web/i18n/locales/ko-KR/skill.json index ba560742dfb..816757fd74b 100644 --- a/web/i18n/locales/ko-KR/skill.json +++ b/web/i18n/locales/ko-KR/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "이 패키지에는 SKILL.md가 없습니다. 파일을 추가하고 다시 가져오세요.", "skillManagement.errors.nameConflict": "\"{{name}}\"라는 스킬이 이미 존재합니다. SKILL.md에서 이름을 바꾸고 다시 가져옵니다.", "skillManagement.errors.workspaceLimit": "작업 영역이 스킬 한도인 500개에 도달했습니다. 새로운 스킬을 추가하려면 스킬을 삭제하세요.", - "skillManagement.exportDialog.description": "이 스킬의 현재 저장된 초안을 내보냅니다. 편집기에 저장하지 않은 변경 사항이 있으면 내보내기 전에 저장해 주세요.", - "skillManagement.exportDialog.title": "현재 저장된 초안 내보내기", "skillManagement.import": "가져오기", "skillManagement.importFailed": "스킬을 가져오지 못했습니다.", "skillManagement.importSuccess": "Skill 가져왔습니다.", diff --git a/web/i18n/locales/lo-LA/skill.json b/web/i18n/locales/lo-LA/skill.json index 341b9d233d2..da4e872b5a1 100644 --- a/web/i18n/locales/lo-LA/skill.json +++ b/web/i18n/locales/lo-LA/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "ຊຸດນີ້ບໍ່ມີ SKILL.md. ເພີ່ມໄຟລ໌ແລະນໍາເຂົ້າມັນອີກເທື່ອຫນຶ່ງ.", "skillManagement.errors.nameConflict": "ທັກສະທີ່ມີຊື່ວ່າ \"{{name}}\" ມີຢູ່ແລ້ວ. ປ່ຽນຊື່ມັນຢູ່ໃນ SKILL.md ແລະນໍາເຂົ້າອີກເທື່ອຫນຶ່ງ.", "skillManagement.errors.workspaceLimit": "ພື້ນທີ່ເຮັດວຽກຂອງທ່ານຮອດຂີດຈຳກັດຂອງ 500 ທັກສະແລ້ວ. ລຶບທັກສະເພື່ອເພີ່ມອັນໃໝ່.", - "skillManagement.exportDialog.description": "ການດຳເນີນການນີ້ຈະສົ່ງອອກສະບັບຮ່າງຂອງທັກສະທີ່ບັນທຶກໄວ້ໃນປັດຈຸບັນ. ຖ້າມີການປ່ຽນແປງທີ່ຍັງບໍ່ໄດ້ບັນທຶກໃນຕົວແກ້ໄຂ, ກະລຸນາບັນທຶກກ່ອນສົ່ງອອກ.", - "skillManagement.exportDialog.title": "ສົ່ງອອກສະບັບຮ່າງທີ່ບັນທຶກໄວ້ໃນປັດຈຸບັນ", "skillManagement.import": "ນຳເຂົ້າ", "skillManagement.importFailed": "ລົ້ມເຫລວໃນການນໍາເຂົ້າທັກສະ.", "skillManagement.importSuccess": "Skill ນໍາເຂົ້າ.", diff --git a/web/i18n/locales/nl-NL/skill.json b/web/i18n/locales/nl-NL/skill.json index 2153bd2894d..dbf592bef27 100644 --- a/web/i18n/locales/nl-NL/skill.json +++ b/web/i18n/locales/nl-NL/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Dit pakket heeft geen SKILL.md. Voeg het bestand toe en importeer het opnieuw.", "skillManagement.errors.nameConflict": "Er bestaat al een vaardigheid met de naam \"{{name}}\". Hernoem het in SKILL.md en importeer opnieuw.", "skillManagement.errors.workspaceLimit": "Uw werkruimte heeft de limiet van 500 vaardigheden bereikt. Verwijder een vaardigheid om een ​​nieuwe toe te voegen.", - "skillManagement.exportDialog.description": "Hiermee exporteer je het huidige opgeslagen concept van je vaardigheid. Sla eventuele niet-opgeslagen wijzigingen in de editor op voordat je exporteert.", - "skillManagement.exportDialog.title": "Huidige opgeslagen concept exporteren", "skillManagement.import": "Importeren", "skillManagement.importFailed": "Kan vaardigheid niet importeren.", "skillManagement.importSuccess": "Skill geïmporteerd.", diff --git a/web/i18n/locales/pl-PL/skill.json b/web/i18n/locales/pl-PL/skill.json index 9bb73d1188e..7f8d1337292 100644 --- a/web/i18n/locales/pl-PL/skill.json +++ b/web/i18n/locales/pl-PL/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Ten pakiet nie zawiera SKILL.md. Dodaj plik i zaimportuj go ponownie.", "skillManagement.errors.nameConflict": "Umiejętność o nazwie „{{name}}” już istnieje. Zmień jego nazwę na SKILL.md i zaimportuj ponownie.", "skillManagement.errors.workspaceLimit": "Twój obszar roboczy osiągnął limit 500 umiejętności. Usuń umiejętność, aby dodać nową.", - "skillManagement.exportDialog.description": "Zostanie wyeksportowany aktualnie zapisany szkic umiejętności. Jeśli w edytorze są niezapisane zmiany, zapisz je przed eksportem.", - "skillManagement.exportDialog.title": "Eksportuj aktualnie zapisany szkic", "skillManagement.import": "Importuj", "skillManagement.importFailed": "Nie udało się zaimportować umiejętności.", "skillManagement.importSuccess": "Skill zaimportowano.", diff --git a/web/i18n/locales/pt-BR/skill.json b/web/i18n/locales/pt-BR/skill.json index d4ef0483808..9e8d9075739 100644 --- a/web/i18n/locales/pt-BR/skill.json +++ b/web/i18n/locales/pt-BR/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Este pacote não possui SKILL.md. Adicione o arquivo e importe-o novamente.", "skillManagement.errors.nameConflict": "Uma habilidade chamada \"{{name}}\" já existe. Renomeie-o para SKILL.md e importe novamente.", "skillManagement.errors.workspaceLimit": "Seu espaço de trabalho atingiu o limite de 500 habilidades. Exclua uma habilidade para adicionar uma nova.", - "skillManagement.exportDialog.description": "Isso exportará o rascunho salvo atual da sua habilidade. Se houver alterações não salvas no editor, salve-as antes de exportar.", - "skillManagement.exportDialog.title": "Exportar o rascunho salvo atual", "skillManagement.import": "Importar", "skillManagement.importFailed": "Falha ao importar habilidade.", "skillManagement.importSuccess": "Skill importado.", diff --git a/web/i18n/locales/ro-RO/skill.json b/web/i18n/locales/ro-RO/skill.json index 749b70e8efa..230e0dbe548 100644 --- a/web/i18n/locales/ro-RO/skill.json +++ b/web/i18n/locales/ro-RO/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Acest pachet nu are SKILL.md. Adăugați fișierul și importați-l din nou.", "skillManagement.errors.nameConflict": "O abilitate numită „{{name}}” există deja. Redenumiți-l în SKILL.md și importați din nou.", "skillManagement.errors.workspaceLimit": "Spațiul tău de lucru a atins limita de 500 de aptitudini. Ștergeți o abilitate pentru a adăuga una nouă.", - "skillManagement.exportDialog.description": "Se va exporta schița salvată curentă a abilității. Dacă există modificări nesalvate în editor, salvează-le înainte de export.", - "skillManagement.exportDialog.title": "Exportă schița salvată curentă", "skillManagement.import": "Importați", "skillManagement.importFailed": "Nu s-a putut importa abilitate.", "skillManagement.importSuccess": "Skill importat.", diff --git a/web/i18n/locales/ru-RU/skill.json b/web/i18n/locales/ru-RU/skill.json index 27f402e2433..eb3f70eef19 100644 --- a/web/i18n/locales/ru-RU/skill.json +++ b/web/i18n/locales/ru-RU/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "В этом пакете нет SKILL.md. Добавьте файл и импортируйте его еще раз.", "skillManagement.errors.nameConflict": "Навык под названием «{{name}}» уже существует. Переименуйте его в SKILL.md и импортируйте снова.", "skillManagement.errors.workspaceLimit": "Ваше рабочее пространство достигло предела в 500 навыков. Удалите навык, чтобы добавить новый.", - "skillManagement.exportDialog.description": "Будет экспортирован текущий сохранённый черновик навыка. Если в редакторе есть несохранённые изменения, сохраните их перед экспортом.", - "skillManagement.exportDialog.title": "Экспорт текущего сохранённого черновика", "skillManagement.import": "Импортировать", "skillManagement.importFailed": "Не удалось импортировать навык.", "skillManagement.importSuccess": "Skill импортирован.", diff --git a/web/i18n/locales/sl-SI/skill.json b/web/i18n/locales/sl-SI/skill.json index d55fc2e3a3c..9896fd83f8d 100644 --- a/web/i18n/locales/sl-SI/skill.json +++ b/web/i18n/locales/sl-SI/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Ta paket nima SKILL.md. Dodajte datoteko in jo znova uvozite.", "skillManagement.errors.nameConflict": "Spretnost z imenom \"{{name}}\" že obstaja. Preimenujte ga v SKILL.md in znova uvozite.", "skillManagement.errors.workspaceLimit": "Vaš delovni prostor je dosegel omejitev 500 veščin. Izbrišite veščino, da dodate novo.", - "skillManagement.exportDialog.description": "Izvožen bo trenutno shranjeni osnutek vaše veščine. Če imate v urejevalniku neshranjene spremembe, jih pred izvozom shranite.", - "skillManagement.exportDialog.title": "Izvozi trenutno shranjeni osnutek", "skillManagement.import": "Uvozi", "skillManagement.importFailed": "Spretnosti ni bilo mogoče uvoziti.", "skillManagement.importSuccess": "Skill uvoženo.", diff --git a/web/i18n/locales/th-TH/skill.json b/web/i18n/locales/th-TH/skill.json index 2c0b10caeba..bf1c6c6d2c4 100644 --- a/web/i18n/locales/th-TH/skill.json +++ b/web/i18n/locales/th-TH/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "แพ็คเกจนี้ไม่มี SKILL.md เพิ่มไฟล์และนำเข้าอีกครั้ง", "skillManagement.errors.nameConflict": "มีทักษะชื่อ \"{{name}}\" อยู่แล้ว เปลี่ยนชื่อใน SKILL.md และนำเข้าอีกครั้ง", "skillManagement.errors.workspaceLimit": "พื้นที่ทำงานของคุณถึงขีดจำกัด 500 ทักษะแล้ว ลบทักษะเพื่อเพิ่มทักษะใหม่", - "skillManagement.exportDialog.description": "การดำเนินการนี้จะส่งออกฉบับร่างของทักษะที่บันทึกไว้ปัจจุบัน หากมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในตัวแก้ไข โปรดบันทึกก่อนส่งออก", - "skillManagement.exportDialog.title": "ส่งออกฉบับร่างที่บันทึกไว้ปัจจุบัน", "skillManagement.import": "นำเข้า", "skillManagement.importFailed": "ไม่สามารถนำเข้าทักษะได้", "skillManagement.importSuccess": "Skill นำเข้า", diff --git a/web/i18n/locales/tr-TR/skill.json b/web/i18n/locales/tr-TR/skill.json index 0890ca19c46..ac6af9541bd 100644 --- a/web/i18n/locales/tr-TR/skill.json +++ b/web/i18n/locales/tr-TR/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Bu pakette SKILL.md yok. Dosyayı ekleyin ve tekrar içe aktarın.", "skillManagement.errors.nameConflict": "\"{{name}}\" adlı bir beceri zaten mevcut. SKILL.md'de yeniden adlandırın ve tekrar içe aktarın.", "skillManagement.errors.workspaceLimit": "Çalışma alanınız 500 beceri sınırına ulaştı. Yeni bir beceri eklemek için bir beceriyi silin.", - "skillManagement.exportDialog.description": "Becerinizin geçerli kayıtlı taslağı dışa aktarılacaktır. Düzenleyicide kaydedilmemiş değişiklikler varsa dışa aktarmadan önce kaydedin.", - "skillManagement.exportDialog.title": "Geçerli Kayıtlı Taslağı Dışa Aktar", "skillManagement.import": "İçe Aktar", "skillManagement.importFailed": "Beceri içe aktarılamadı.", "skillManagement.importSuccess": "Skill içe aktarıldı.", diff --git a/web/i18n/locales/uk-UA/skill.json b/web/i18n/locales/uk-UA/skill.json index 9b9690aab96..26468cfe4f6 100644 --- a/web/i18n/locales/uk-UA/skill.json +++ b/web/i18n/locales/uk-UA/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Цей пакет не має SKILL.md. Додайте файл і імпортуйте його знову.", "skillManagement.errors.nameConflict": "Уміння під назвою \"{{name}}\" уже існує. Перейменуйте його в SKILL.md і імпортуйте знову.", "skillManagement.errors.workspaceLimit": "Ваша робоча область досягла ліміту в 500 навичок. Видаліть навик, щоб додати новий.", - "skillManagement.exportDialog.description": "Буде експортовано поточну збережену чернетку навички. Якщо в редакторі є незбережені зміни, збережіть їх перед експортом.", - "skillManagement.exportDialog.title": "Експорт поточної збереженої чернетки", "skillManagement.import": "Імпортувати", "skillManagement.importFailed": "Не вдалося імпортувати навички.", "skillManagement.importSuccess": "Skill імпортовано.", diff --git a/web/i18n/locales/vi-VN/skill.json b/web/i18n/locales/vi-VN/skill.json index e68cb5b83a1..221e406b000 100644 --- a/web/i18n/locales/vi-VN/skill.json +++ b/web/i18n/locales/vi-VN/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "Gói này không có SKILL.md. Thêm tệp và nhập lại.", "skillManagement.errors.nameConflict": "Một kỹ năng có tên \"{{name}}\" đã tồn tại. Đổi tên nó thành SKILL.md và nhập lại.", "skillManagement.errors.workspaceLimit": "Không gian làm việc của bạn đã đạt đến giới hạn 500 kỹ năng. Xóa một kỹ năng để thêm một kỹ năng mới.", - "skillManagement.exportDialog.description": "Thao tác này sẽ xuất bản nháp hiện đã lưu của kỹ năng. Nếu có thay đổi chưa lưu trong trình chỉnh sửa, hãy lưu trước khi xuất.", - "skillManagement.exportDialog.title": "Xuất bản nháp hiện đã lưu", "skillManagement.import": "Nhập", "skillManagement.importFailed": "Không thể nhập kỹ năng.", "skillManagement.importSuccess": "Skill đã nhập.", diff --git a/web/i18n/locales/zh-Hans/skill.json b/web/i18n/locales/zh-Hans/skill.json index 68673f15e70..b238a2ff3a3 100644 --- a/web/i18n/locales/zh-Hans/skill.json +++ b/web/i18n/locales/zh-Hans/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "此 Skill 包中没有 SKILL.md。请添加该文件后重新导入。", "skillManagement.errors.nameConflict": "名为「{{name}}」的 Skill 已存在。请在 SKILL.md 中修改名称后重新导入。", "skillManagement.errors.workspaceLimit": "工作空间最多可添加 500 个 Skill。请先删除一个 Skill,再添加新 Skill。", - "skillManagement.exportDialog.description": "此操作将导出此 Skill 当前已保存的草稿。如果编辑器中有未保存的更改,请先保存后再导出。", - "skillManagement.exportDialog.title": "导出当前已保存的草稿", "skillManagement.import": "导入", "skillManagement.importFailed": "Skill 导入失败。", "skillManagement.importSuccess": "Skill 已导入。", diff --git a/web/i18n/locales/zh-Hant/skill.json b/web/i18n/locales/zh-Hant/skill.json index 4547d7ca4e2..6680ec3e5b5 100644 --- a/web/i18n/locales/zh-Hant/skill.json +++ b/web/i18n/locales/zh-Hant/skill.json @@ -188,8 +188,6 @@ "skillManagement.errors.missingSkillMd": "這個包沒有SKILL.md。新增檔案並再次匯入。", "skillManagement.errors.nameConflict": "名為「{{name}}」的技能已存在。將其重新命名為SKILL.md並再次匯入。", "skillManagement.errors.workspaceLimit": "您的工作區已達到 500 項技能的限制。刪除一項技能以新增技能。", - "skillManagement.exportDialog.description": "此操作將匯出此 Skill 目前已儲存的草稿。如果編輯器中有尚未儲存的變更,請先儲存再匯出。", - "skillManagement.exportDialog.title": "匯出目前已儲存的草稿", "skillManagement.import": "匯入", "skillManagement.importFailed": "Skill 匯入失敗。", "skillManagement.importSuccess": "Skill 已匯入。", From a60167b39038960d67c381e45b952e6361889a80 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 18 Sep 2026 07:02:22 +0000 Subject: [PATCH 18/21] feat(web): add copy action to error toasts (#42480) Co-authored-by: yyh --- .../src/toast/__tests__/index.spec.tsx | 79 ++- packages/dify-ui/src/toast/index.stories.tsx | 43 +- packages/dify-ui/src/toast/index.tsx | 63 +- web/.storybook/main.ts | 1 + web/.storybook/preview.tsx | 4 +- .../billing/cloud-plan-payment-flow.test.tsx | 5 +- .../overview/tracing/__tests__/panel.spec.tsx | 2 +- .../__tests__/provider-config-modal.spec.tsx | 4 +- .../[appId]/overview/tracing/panel.tsx | 2 +- .../tracing/provider-config-modal.tsx | 2 +- .../webapp-reset-password/check-code/page.tsx | 2 +- .../webapp-reset-password/page.tsx | 2 +- .../set-password/page.tsx | 2 +- .../webapp-signin/check-code/page.tsx | 2 +- .../components/external-member-sso-auth.tsx | 2 +- .../components/mail-and-code-auth.tsx | 2 +- .../components/mail-and-password-auth.tsx | 2 +- .../webapp-signin/components/sso-auth.tsx | 2 +- .../account-page/AvatarWithEdit.tsx | 2 +- .../account-page/email-change-modal.tsx | 2 +- .../(commonLayout)/account-page/index.tsx | 2 +- .../components/__tests__/feed-back.spec.tsx | 2 +- .../delete-account/components/feed-back.tsx | 2 +- web/app/account/oauth/authorize/page.tsx | 2 +- .../oauth/authorize/use-silent-authorize.ts | 2 +- .../__tests__/use-app-info-actions.spec.ts | 2 +- .../app-info/use-app-info-actions.ts | 2 +- .../__tests__/dropdown-callbacks.spec.tsx | 2 +- .../app-sidebar/dataset-info/dropdown.tsx | 2 +- .../snippet-info/__tests__/dropdown.spec.tsx | 2 +- .../app-sidebar/snippet-info/dropdown.tsx | 2 +- .../app/__tests__/use-export-app-dsl.spec.tsx | 2 +- .../environment-access-control.spec.tsx | 2 +- .../environment-access-point-cards.spec.tsx | 4 +- .../access-point/__tests__/mcp-card.spec.tsx | 4 +- .../__tests__/service-api-card.spec.tsx | 4 +- .../__tests__/trigger-card.spec.tsx | 4 +- .../use-access-point-actions.spec.ts | 2 +- .../__tests__/web-app-card.spec.tsx | 4 +- .../built-in-access-points/mcp-card.tsx | 2 +- .../service-api-card.tsx | 2 +- .../built-in-access-points/trigger-card.tsx | 2 +- .../built-in-access-points/web-app-card.tsx | 2 +- .../environment-access-control.tsx | 2 +- .../environment-service-api-card.tsx | 2 +- .../environment-web-app-card.tsx | 2 +- .../shared/use-access-point-actions.ts | 2 +- .../app/annotation/__tests__/index.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../annotation/add-annotation-modal/index.tsx | 2 +- .../__tests__/csv-uploader.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../csv-uploader.tsx | 2 +- .../batch-add-annotation-modal/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../edit-annotation-modal/index.tsx | 2 +- web/app/components/app/annotation/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../app/app-access-control/index.tsx | 2 +- .../app-publisher/__tests__/index.spec.tsx | 2 +- .../__tests__/version-info-modal.spec.tsx | 4 +- .../use-marketplace-publish.ts | 2 +- .../publisher-content/use-version-info.ts | 2 +- .../app/app-publisher/version-info-modal.tsx | 2 +- .../configuration/__tests__/index.spec.tsx | 4 +- .../components/app/configuration/index.tsx | 4 +- web/app/components/app/configuration/toast.ts | 6 +- .../app-list/__tests__/index.spec.tsx | 2 +- .../app/create-app-dialog/app-list/index.tsx | 2 +- .../create-app-modal/__tests__/index.spec.tsx | 2 +- .../components/app/create-app-modal/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../__tests__/uploader.spec.tsx | 4 +- .../app/create-from-dsl-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/uploader.tsx | 2 +- .../app/deploy/__tests__/index.spec.tsx | 4 +- .../deploy/environment-table/row-actions.tsx | 2 +- .../app/deploy/hooks/use-undeploy-workflow.ts | 2 +- .../use-deployment-configuration-form.ts | 2 +- .../duplicate-modal/__tests__/index.spec.tsx | 2 +- .../components/app/duplicate-modal/index.tsx | 2 +- web/app/components/app/log/list.tsx | 2 +- .../settings/__tests__/index.spec.tsx | 2 +- .../app/overview/settings/index.tsx | 2 +- .../switch-app-modal/__tests__/index.spec.tsx | 2 +- .../components/app/switch-app-modal/index.tsx | 2 +- .../item/__tests__/action-groups.spec.tsx | 2 +- .../item/__tests__/index.spec.tsx | 2 +- .../app/text-generate/item/action-groups.tsx | 2 +- .../app/text-generate/item/index.tsx | 2 +- .../saved-items/__tests__/index.spec.tsx | 2 +- .../app/text-generate/saved-items/index.tsx | 2 +- web/app/components/app/use-export-app-dsl.ts | 2 +- .../apps/__tests__/app-card.spec.tsx | 2 +- web/app/components/apps/app-card/index.tsx | 2 +- .../components/apps/app-card/interactions.tsx | 2 +- ...import-from-marketplace-template-modal.tsx | 2 +- web/app/components/apps/starred-app-card.tsx | 2 +- .../agent-log-modal/__tests__/detail.spec.tsx | 2 +- .../agent-log-modal/__tests__/index.spec.tsx | 2 +- .../base/agent-log-modal/detail.tsx | 2 +- .../base/agent-log-modal/index.stories.tsx | 4 +- .../base/audio-gallery/AudioPlayer.tsx | 2 +- .../__tests__/AudioPlayer.spec.tsx | 2 +- .../__tests__/hooks.spec.tsx | 4 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- .../check-input-forms-hooks.spec.tsx | 2 +- .../base/chat/chat/__tests__/hooks.spec.tsx | 2 +- .../chat/chat/__tests__/question.spec.tsx | 2 +- .../chat/answer/__tests__/operation.spec.tsx | 2 +- .../base/chat/chat/answer/operation.tsx | 2 +- .../chat-input-area/__tests__/index.spec.tsx | 2 +- .../base/chat/chat/chat-input-area/index.tsx | 2 +- .../base/chat/chat/check-input-forms-hooks.ts | 2 +- web/app/components/base/chat/chat/hooks.ts | 2 +- .../components/base/chat/chat/question.tsx | 2 +- .../embedded-chatbot/__tests__/hooks.spec.tsx | 4 +- .../base/chat/embedded-chatbot/hooks.tsx | 2 +- .../inputs-form/__tests__/content.spec.tsx | 2 +- .../__tests__/annotation-ctrl-button.spec.tsx | 2 +- .../__tests__/config-param-modal.spec.tsx | 2 +- .../annotation-ctrl-button.tsx | 2 +- .../annotation-reply/config-param-modal.tsx | 2 +- .../moderation-setting-modal.spec.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- .../__tests__/voice-settings.spec.tsx | 2 +- .../file-uploader/__tests__/hooks.spec.ts | 2 +- .../index.stories.tsx | 4 +- .../index.stories.tsx | 4 +- .../components/base/file-uploader/hooks.ts | 2 +- .../validation-accessibility.spec.tsx | 2 +- .../__tests__/use-check-validated.spec.ts | 2 +- .../base/form/hooks/use-check-validated.ts | 2 +- .../image-uploader/__tests__/hooks.spec.ts | 2 +- .../__tests__/image-preview.spec.tsx | 2 +- .../components/base/image-uploader/hooks.ts | 2 +- .../base/image-uploader/image-preview.tsx | 2 +- .../base/tag-input/__tests__/index.spec.tsx | 2 +- web/app/components/base/tag-input/index.tsx | 2 +- .../text-generation/__tests__/hooks.spec.ts | 2 +- .../components/base/text-generation/hooks.ts | 2 +- .../billing/hooks/use-education-discount.ts | 2 +- .../pricing/plans/cloud-plan-item/index.tsx | 2 +- .../custom-page/__tests__/index.spec.tsx | 2 +- .../__tests__/use-web-app-brand.spec.tsx | 2 +- .../hooks/use-web-app-brand.ts | 2 +- .../__tests__/auto-disabled-document.spec.tsx | 4 +- .../auto-disabled-document.tsx | 2 +- .../hooks/__tests__/use-upload.spec.tsx | 4 +- .../common/image-uploader/hooks/use-upload.ts | 2 +- .../__tests__/index.spec.tsx | 2 +- .../common/retrieval-param-config/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../__tests__/uploader.spec.tsx | 2 +- .../hooks/__tests__/use-dsl-import.spec.tsx | 2 +- .../hooks/use-dsl-import.ts | 2 +- .../create-from-dsl-modal/uploader.tsx | 2 +- .../list/__tests__/create-card.spec.tsx | 4 +- .../create-from-pipeline/list/create-card.tsx | 2 +- .../__tests__/edit-pipeline-info.spec.tsx | 4 +- .../template-card/__tests__/index.spec.tsx | 4 +- .../list/template-card/edit-pipeline-info.tsx | 2 +- .../list/template-card/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../empty-dataset-creation-modal/index.tsx | 2 +- .../hooks/__tests__/use-file-upload.spec.tsx | 2 +- .../file-uploader/hooks/use-file-upload.ts | 2 +- .../__tests__/use-document-creation.spec.ts | 2 +- .../step-two/hooks/use-document-creation.ts | 2 +- .../datasets/create/step-two/index.tsx | 2 +- .../create/website/firecrawl/index.tsx | 2 +- .../create/website/jina-reader/index.tsx | 2 +- .../create/website/watercrawl/index.tsx | 2 +- .../components/__tests__/operations.spec.tsx | 2 +- .../__tests__/rename-modal.spec.tsx | 2 +- .../__tests__/use-document-actions.spec.ts | 2 +- .../hooks/use-document-actions.ts | 2 +- .../documents/components/operations.tsx | 2 +- .../documents/components/rename-modal.tsx | 2 +- .../__tests__/use-local-file-upload.spec.tsx | 2 +- .../online-documents/__tests__/index.spec.tsx | 4 +- .../data-source/online-documents/index.tsx | 2 +- .../online-drive/__tests__/index.spec.tsx | 4 +- .../data-source/online-drive/index.tsx | 2 +- .../base/options/__tests__/index.spec.tsx | 4 +- .../website-crawl/base/options/index.tsx | 2 +- .../online-document-preview.spec.tsx | 4 +- .../preview/online-document-preview.tsx | 2 +- .../__tests__/components.spec.tsx | 4 +- .../process-documents/__tests__/form.spec.tsx | 4 +- .../process-documents/form.tsx | 2 +- .../documents/detail/__tests__/index.spec.tsx | 2 +- .../detail/__tests__/new-segment.spec.tsx | 5 +- .../__tests__/csv-uploader.spec.tsx | 2 +- .../detail/batch-modal/csv-uploader.tsx | 2 +- .../detail/completed/__tests__/index.spec.tsx | 2 +- .../__tests__/new-child-segment.spec.tsx | 5 +- .../__tests__/use-child-segment-data.spec.ts | 2 +- .../__tests__/use-segment-list-data.spec.ts | 2 +- .../completed/hooks/use-child-segment-data.ts | 2 +- .../completed/hooks/use-segment-list-data.ts | 2 +- .../detail/completed/new-child-segment.tsx | 2 +- .../detail/embedding/__tests__/index.spec.tsx | 2 +- .../documents/detail/embedding/index.tsx | 2 +- .../datasets/documents/detail/index.tsx | 2 +- .../datasets/documents/detail/new-segment.tsx | 2 +- .../status-item/__tests__/index.spec.tsx | 2 +- .../datasets/documents/status-item/index.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../external-api/external-api-modal/index.tsx | 2 +- .../connector/__tests__/index.spec.tsx | 4 +- .../connector/index.tsx | 2 +- .../__tests__/modify-retrieval-modal.spec.tsx | 2 +- .../hit-testing/modify-retrieval-modal.tsx | 2 +- .../dataset-card/__tests__/index.spec.tsx | 2 +- .../hooks/use-dataset-card-state.ts | 2 +- .../datasets/list/dataset-card/index.tsx | 2 +- .../__tests__/modal.spec.tsx | 2 +- .../metadata/edit-metadata-batch/modal.tsx | 2 +- .../use-batch-edit-document-metadata.spec.ts | 2 +- .../use-edit-dataset-metadata.spec.ts | 2 +- .../__tests__/use-metadata-document.spec.ts | 2 +- .../hooks/use-batch-edit-document-metadata.ts | 2 +- .../hooks/use-edit-dataset-metadata.ts | 2 +- .../metadata/hooks/use-metadata-document.ts | 2 +- .../dataset-metadata-drawer.spec.tsx | 2 +- .../dataset-metadata-drawer.tsx | 2 +- .../rename-modal/__tests__/index.spec.tsx | 2 +- .../datasets/rename-modal/index.tsx | 2 +- .../settings/form/__tests__/index.spec.tsx | 4 +- .../hooks/__tests__/use-form-state.spec.ts | 10 +- .../settings/form/hooks/use-form-state.ts | 2 +- .../explore/create-app-modal/index.tsx | 2 +- .../__tests__/compliance.spec.tsx | 2 +- .../header/account-dropdown/compliance.tsx | 2 +- .../access-rule-row-menu.tsx | 2 +- .../resource-access-rule-section.tsx | 2 +- .../__tests__/modal.spec.tsx | 2 +- .../api-based-extension-page/modal.tsx | 2 +- .../__tests__/member-menu.spec.tsx | 4 +- .../__tests__/index.spec.tsx | 2 +- .../edit-workspace-modal/index.tsx | 2 +- .../account-setting/members-page/index.tsx | 2 +- .../members-page/member-menu.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../transfer-ownership-modal/index.tsx | 2 +- .../hooks/__tests__/use-auth.spec.tsx | 4 +- .../model-auth/hooks/use-auth.ts | 2 +- .../__tests__/credential-panel.spec.tsx | 2 +- .../model-list.dynamic-import.spec.tsx | 2 +- .../__tests__/model-list.spec.tsx | 2 +- .../model-load-balancing-modal.spec.tsx | 4 +- .../use-change-provider-priority.spec.ts | 2 +- .../use-activate-credential.spec.tsx | 2 +- .../use-activate-credential.ts | 2 +- .../provider-added-card/model-list.tsx | 2 +- .../model-load-balancing-modal.tsx | 2 +- .../use-change-provider-priority.ts | 2 +- .../__tests__/index.spec.tsx | 4 +- .../system-model-selector/index.tsx | 2 +- .../permissions-page/__tests__/index.spec.tsx | 4 +- .../permissions-page/index.tsx | 2 +- .../role-list/__tests__/row-menu.spec.tsx | 2 +- .../permissions-page/role-list/row-menu.tsx | 2 +- .../preference-page/__tests__/index.spec.tsx | 4 +- .../account-setting/preference-page/index.tsx | 2 +- .../account-setting/update-setting-dialog.tsx | 2 +- .../workflow-log-archives-page/index.tsx | 2 +- .../main-nav/__tests__/index.spec.tsx | 4 +- .../__tests__/support-menu.spec.tsx | 2 +- .../main-nav/components/support-menu.tsx | 2 +- .../main-nav/components/web-apps-section.tsx | 2 +- .../main-nav/components/workspace-card.tsx | 2 +- .../install-plugin/__tests__/hooks.spec.ts | 2 +- .../plugins/install-plugin/hooks.ts | 2 +- .../__tests__/index.spec.tsx | 2 +- .../install-from-github/index.tsx | 2 +- .../__tests__/api-key-modal.spec.tsx | 2 +- .../__tests__/authorize-components.spec.tsx | 2 +- .../__tests__/oauth-client-settings.spec.tsx | 2 +- .../plugin-auth/authorize/api-key-modal.tsx | 2 +- .../authorize/oauth-client-settings.tsx | 2 +- .../authorized/__tests__/index.spec.tsx | 2 +- .../plugins/plugin-auth/authorized/index.tsx | 2 +- .../__tests__/use-plugin-auth-action.spec.ts | 2 +- .../hooks/use-plugin-auth-action.ts | 2 +- .../__tests__/endpoint-card.spec.tsx | 2 +- .../__tests__/endpoint-modal.spec.tsx | 2 +- .../__tests__/use-plugin-operations.spec.ts | 2 +- .../hooks/use-plugin-operations.ts | 2 +- .../plugin-detail-panel/endpoint-card.tsx | 2 +- .../plugin-detail-panel/endpoint-list.tsx | 2 +- .../plugin-detail-panel/endpoint-modal.tsx | 2 +- .../model-selector/__tests__/index.spec.tsx | 2 +- .../model-selector/index.tsx | 2 +- .../__tests__/delete-confirm.spec.tsx | 4 +- .../__tests__/log-viewer.spec.tsx | 2 +- .../__tests__/selector-entry.spec.tsx | 2 +- .../__tests__/selector-view.spec.tsx | 2 +- .../__tests__/subscription-card.spec.tsx | 2 +- .../create/__tests__/common-modal.spec.tsx | 2 +- .../create/__tests__/index.spec.tsx | 2 +- .../create/__tests__/oauth-client.spec.tsx | 2 +- .../use-common-modal-state.helpers.spec.ts | 2 +- .../__tests__/use-common-modal-state.spec.ts | 2 +- .../__tests__/use-oauth-client-state.spec.ts | 2 +- .../hooks/use-common-modal-state.helpers.ts | 2 +- .../create/hooks/use-common-modal-state.ts | 2 +- .../create/hooks/use-oauth-client-state.ts | 2 +- .../subscription-list/create/index.tsx | 2 +- .../subscription-list/create/oauth-client.tsx | 2 +- .../subscription-list/delete-confirm.tsx | 2 +- .../edit/__tests__/index.spec.tsx | 2 +- .../edit/apikey-edit-modal.tsx | 2 +- .../edit/manual-edit-modal.tsx | 2 +- .../edit/oauth-edit-modal.tsx | 2 +- .../subscription-list/log-viewer.tsx | 2 +- .../plugin-item/__tests__/action.spec.tsx | 2 +- .../components/plugins/plugin-item/action.tsx | 2 +- .../__tests__/use-reference-setting.spec.ts | 2 +- .../plugin-page/use-reference-setting.ts | 2 +- .../__tests__/from-market-place.spec.tsx | 2 +- .../update-plugin/__tests__/index.spec.tsx | 2 +- .../update-plugin/from-market-place.tsx | 2 +- .../components/__tests__/conversion.spec.tsx | 2 +- .../__tests__/update-dsl-modal.spec.tsx | 2 +- .../rag-pipeline/components/conversion.tsx | 2 +- .../editor/form/__tests__/index.spec.tsx | 2 +- .../panel/input-field/editor/form/index.tsx | 2 +- .../field-list/__tests__/hooks.spec.ts | 2 +- .../panel/input-field/field-list/hooks.ts | 2 +- .../__tests__/index.spec.tsx | 2 +- .../__tests__/options.spec.tsx | 2 +- .../document-processing/options.tsx | 2 +- .../publisher/__tests__/index.spec.tsx | 2 +- .../publisher/__tests__/popup.spec.tsx | 2 +- .../rag-pipeline-header/publisher/index.tsx | 2 +- .../rag-pipeline-header/publisher/popup.tsx | 2 +- .../hooks/__tests__/use-DSL.spec.ts | 2 +- .../__tests__/use-update-dsl-modal.spec.ts | 2 +- .../components/rag-pipeline/hooks/use-DSL.ts | 2 +- .../hooks/use-update-dsl-modal.ts | 2 +- .../use-text-generation-app-state.spec.ts | 2 +- .../hooks/use-text-generation-app-state.ts | 2 +- .../share/text-generation/index.tsx | 2 +- .../result/__tests__/index.spec.tsx | 2 +- .../share/text-generation/result/index.tsx | 2 +- .../__tests__/snippet-card.spec.tsx | 2 +- .../__tests__/snippet-create-button.spec.tsx | 2 +- .../snippet-list/components/snippet-card.tsx | 2 +- .../import-snippet-dsl-dialog.spec.tsx | 2 +- .../__tests__/snippet-main.spec.tsx | 4 +- .../__tests__/snippet-run-panel.spec.tsx | 2 +- .../__tests__/snippet-sidebar.spec.tsx | 4 +- .../hooks/__tests__/use-snippet-dsl.spec.ts | 4 +- .../__tests__/use-snippet-publish.spec.ts | 4 +- .../components/hooks/use-snippet-dsl.ts | 2 +- .../components/hooks/use-snippet-publish.ts | 2 +- .../snippets/components/snippet-main.tsx | 2 +- .../snippets/components/snippet-run-panel.tsx | 2 +- .../snippets/components/snippet-sidebar.tsx | 2 +- ...use-create-snippet-from-selection.spec.tsx | 4 +- .../__tests__/use-create-snippet.spec.tsx | 2 +- .../use-create-snippet-from-selection.tsx | 2 +- .../snippets/hooks/use-create-snippet.ts | 2 +- .../snippets/import-snippet-dsl-dialog.tsx | 2 +- .../__tests__/get-schema.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../get-schema.tsx | 2 +- .../edit-custom-collection-modal/index.tsx | 2 +- .../tools/mcp/__tests__/modal.spec.tsx | 2 +- .../tools/mcp/hooks/use-mcp-modal-form.ts | 2 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../__tests__/custom-create-card.spec.tsx | 2 +- .../tools/provider/__tests__/detail.spec.tsx | 2 +- .../tools/provider/custom-create-card.tsx | 2 +- web/app/components/tools/provider/detail.tsx | 2 +- .../__tests__/config-credentials.spec.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 2 +- .../workflow-tool/__tests__/index.spec.tsx | 2 +- .../__tests__/use-configure-button.spec.ts | 2 +- .../hooks/use-configure-button.ts | 2 +- .../components/tools/workflow-tool/index.tsx | 2 +- .../__tests__/features-trigger.spec.tsx | 2 +- .../workflow-header/features-trigger.tsx | 2 +- .../hooks/__tests__/use-DSL.spec.ts | 2 +- .../__tests__/use-workflow-run-utils.spec.ts | 2 +- .../components/workflow-app/hooks/use-DSL.ts | 2 +- .../hooks/use-workflow-run-utils.ts | 2 +- .../__tests__/update-dsl-modal.spec.tsx | 4 +- .../__tests__/workflow-edge-events.spec.tsx | 2 +- .../block-selector/__tests__/blocks.spec.tsx | 2 +- .../__tests__/tool-picker.spec.tsx | 2 +- .../block-selector/agent-selector.tsx | 2 +- .../__tests__/use-insert-snippet.spec.tsx | 2 +- .../snippets/use-insert-snippet.ts | 2 +- .../workflow/block-selector/tool-picker.tsx | 2 +- .../header/__tests__/header-layouts.spec.tsx | 2 +- .../header/__tests__/run-mode.spec.tsx | 2 +- .../header/__tests__/view-history.spec.tsx | 2 +- .../workflow/header/header-in-restoring.tsx | 2 +- .../components/workflow/header/run-mode.tsx | 2 +- .../hooks/__tests__/use-checklist.spec.ts | 2 +- .../hooks/__tests__/use-locate-node.spec.tsx | 2 +- .../workflow/hooks/use-checklist.ts | 2 +- .../workflow/hooks/use-locate-node.ts | 2 +- .../workflow/hooks/use-nodes-interactions.ts | 2 +- web/app/components/workflow/index.tsx | 2 +- .../llm-environment-variable-value-field.tsx | 2 +- .../__tests__/file-support.spec.tsx | 2 +- .../before-run-form/__tests__/index.spec.tsx | 4 +- .../components/before-run-form/index.tsx | 2 +- .../__tests__/output-var-list.spec.tsx | 2 +- .../components/variable/output-var-list.tsx | 2 +- .../_base/components/variable/var-list.tsx | 2 +- .../workflow-panel/last-run/use-last-run.ts | 2 +- .../hooks/__tests__/use-one-step-run.spec.ts | 2 +- .../nodes/_base/hooks/use-one-step-run.ts | 2 +- .../nodes/agent-v2/__tests__/hooks.spec.tsx | 2 +- ...ave-inline-agent-to-roster-dialog.spec.tsx | 2 +- .../agent-orchestrate-panel-content.tsx | 2 +- .../workflow/nodes/agent-v2/hooks.ts | 2 +- .../components/__tests__/curl-panel.spec.tsx | 4 +- .../nodes/http/components/curl-panel.tsx | 2 +- .../human-input/__tests__/panel.spec.tsx | 4 +- .../components/__tests__/user-action.spec.tsx | 2 +- .../__tests__/email-configure-modal.spec.tsx | 2 +- .../__tests__/test-email-sender.spec.tsx | 4 +- .../delivery-method/email-configure-modal.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 2 +- .../human-input/components/user-action.tsx | 2 +- .../workflow/nodes/human-input/panel.tsx | 2 +- .../iteration/__tests__/integration.spec.tsx | 4 +- .../workflow/nodes/iteration/node.tsx | 2 +- .../json-schema-config.tsx | 2 +- .../json-schema-generator/index.tsx | 2 +- .../visual-editor/hooks.ts | 2 +- .../components/workflow/nodes/llm/panel.tsx | 2 +- .../loop/components/loop-variables/item.tsx | 2 +- .../__tests__/integration.spec.tsx | 4 +- .../__tests__/update.spec.tsx | 4 +- .../components/extract-parameter/update.tsx | 2 +- .../nodes/start/__tests__/use-config.spec.ts | 2 +- .../nodes/start/components/var-list.tsx | 2 +- .../workflow/nodes/start/use-config.ts | 2 +- .../workflow/nodes/tool/hooks/use-config.ts | 2 +- .../__tests__/use-config.spec.tsx | 4 +- .../nodes/trigger-webhook/use-config.ts | 2 +- .../__tests__/var-group-item.spec.tsx | 4 +- .../components/var-group-item.tsx | 2 +- .../__tests__/hooks.spec.tsx | 2 +- .../plugins/link-editor-plugin/hooks.ts | 2 +- .../panel/__tests__/inputs-panel.spec.tsx | 2 +- .../panel/__tests__/workflow-preview.spec.tsx | 4 +- .../__tests__/object-value-item.spec.tsx | 2 +- .../__tests__/variable-modal.spec.tsx | 4 +- .../components/object-value-item.tsx | 2 +- .../components/variable-modal.tsx | 2 +- .../debug-and-preview/__tests__/hooks.spec.ts | 2 +- .../__tests__/hooks/handle-resume.spec.ts | 2 +- .../__tests__/hooks/handle-send.spec.ts | 2 +- .../hooks/handle-stop-restart.spec.ts | 2 +- .../__tests__/hooks/misc.spec.ts | 2 +- .../__tests__/hooks/opening-statement.spec.ts | 2 +- .../__tests__/hooks/sse-callbacks.spec.ts | 2 +- .../workflow/panel/debug-and-preview/hooks.ts | 2 +- .../__tests__/variable-modal.spec.tsx | 4 +- .../workflow/panel/env-panel/index.tsx | 2 +- .../panel/env-panel/variable-modal.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../panel/version-history-panel/index.tsx | 2 +- .../workflow/panel/workflow-preview.tsx | 2 +- .../workflow/run/__tests__/index.spec.tsx | 2 +- web/app/components/workflow/run/index.tsx | 2 +- .../components/workflow/update-dsl-modal.tsx | 2 +- .../__tests__/value-content-sections.spec.tsx | 2 +- .../workflow/workflow-generator/index.tsx | 2 +- .../education/apply/__tests__/page.spec.tsx | 2 +- web/app/education/apply/application-form.tsx | 2 +- web/app/init/InitPasswordPopup.tsx | 2 +- web/app/layout.tsx | 4 +- .../__tests__/copy.browser.spec.tsx | 204 ++++++ .../notifications/__tests__/toast.spec.tsx | 204 ++++++ web/app/notifications/copy-error-action.tsx | 50 ++ web/app/notifications/host.tsx | 51 ++ web/app/notifications/index.tsx | 6 + .../notifications/notifications.stories.tsx | 599 ++++++++++++++++++ web/app/reset-password/check-code/page.tsx | 2 +- web/app/reset-password/page.tsx | 2 +- web/app/signin/check-code/page.tsx | 2 +- .../components/mail-and-password-auth.tsx | 2 +- web/app/signin/components/sso-auth.tsx | 2 +- web/app/signin/normal-form.tsx | 2 +- web/app/signin/one-more-step.tsx | 2 +- web/app/signup/check-code/page.tsx | 2 +- web/app/signup/set-password/page.tsx | 2 +- .../__tests__/access-surface-cards.spec.tsx | 4 +- .../access/components/agent-api-key-modal.tsx | 2 +- .../components/service-api-access-card.tsx | 2 +- .../access/components/web-app-access-card.tsx | 2 +- .../configure/__tests__/page.spec.tsx | 2 +- .../use-agent-configure-sync.spec.tsx | 2 +- .../configure/components/composer-session.tsx | 2 +- .../__tests__/publish-bar.spec.tsx | 2 +- .../advanced/__tests__/env.spec.tsx | 4 +- .../components/orchestrate/advanced/env.tsx | 2 +- .../files/__tests__/index.spec.tsx | 4 +- .../orchestrate/files/upload-dialog.tsx | 2 +- .../orchestrate/prompt-editor/index.tsx | 2 +- .../orchestrate/publish-bar/index.tsx | 2 +- .../skills/__tests__/index.spec.tsx | 4 +- .../components/orchestrate/skills/index.tsx | 2 +- .../orchestrate/skills/upload-dialog.tsx | 2 +- .../orchestrate/skills/use-skill-detail.ts | 2 +- .../tools/cli-tool/__tests__/dialog.spec.tsx | 4 +- .../orchestrate/tools/cli-tool/dialog.tsx | 2 +- .../preview/__tests__/chat.spec.tsx | 4 +- .../working-directory-panel.spec.tsx | 4 +- .../components/preview/chat-conversation.tsx | 2 +- .../preview/working-directory-panel.tsx | 2 +- .../use-agent-configure-build-draft.ts | 2 +- .../configure/use-agent-configure-sync.ts | 2 +- .../logs/__tests__/log-detail-panel.spec.tsx | 2 +- .../logs/components/log-detail-panel.tsx | 2 +- .../agent-v2/agent-detail/sidebar-actions.tsx | 2 +- .../__tests__/agent-roster-list.spec.tsx | 2 +- .../__tests__/create-agent-dialog.spec.tsx | 2 +- .../__tests__/edit-agent-dialog.spec.tsx | 2 +- .../roster/components/agent-roster-list.tsx | 2 +- .../roster/components/delete-agent-dialog.tsx | 2 +- .../components/duplicate-agent-dialog.tsx | 2 +- .../continue-work/__tests__/item.spec.tsx | 2 +- web/features/home/continue-work/item.tsx | 2 +- .../__tests__/home-content.spec.tsx | 2 +- .../__tests__/add-source-page.spec.tsx | 2 +- .../__tests__/document-detail-page.spec.tsx | 2 +- .../new-rag/__tests__/documents-page.spec.tsx | 2 +- .../__tests__/new-knowledge-list.spec.tsx | 2 +- .../new-rag/__tests__/sources-page.spec.tsx | 2 +- .../new-rag/create-knowledge-page.tsx | 2 +- .../new-rag/document-chunk-detail.tsx | 2 +- web/features/new-rag/document-list.tsx | 2 +- web/features/new-rag/documents-page.tsx | 2 +- .../new-rag/knowledge-space-shell.tsx | 2 +- web/features/new-rag/new-knowledge-list.tsx | 2 +- web/features/new-rag/sources-page.tsx | 2 +- web/features/new-rag/use-document-reindex.ts | 2 +- .../skills/__tests__/detail-page.fixture.tsx | 2 +- web/features/skills/__tests__/page.spec.tsx | 4 +- web/features/skills/detail/builder-panel.tsx | 2 +- web/features/skills/detail/file-editor.tsx | 2 +- web/features/skills/detail/file-tree.tsx | 2 +- web/features/skills/detail/page.tsx | 2 +- web/features/skills/detail/shared.tsx | 2 +- .../skills/detail/sidebar-actions.tsx | 2 +- .../detail/skill-display-name-editor.tsx | 2 +- web/features/skills/detail/skill-metadata.tsx | 2 +- web/features/skills/detail/version-panel.tsx | 2 +- web/features/skills/page.tsx | 2 +- .../__tests__/tag-item-editor.spec.tsx | 2 +- .../__tests__/tag-management-modal.spec.tsx | 2 +- .../__tests__/tag-selector.spec.tsx | 2 +- .../components/tag-item-editor.tsx | 2 +- .../components/tag-management-modal.tsx | 2 +- .../components/tag-selector.tsx | 2 +- web/hooks/use-import-dsl.spec.tsx | 2 +- web/hooks/use-import-dsl.ts | 2 +- web/i18n/locales/ar-TN/common.json | 2 + web/i18n/locales/az-AZ/common.json | 2 + web/i18n/locales/de-DE/common.json | 2 + web/i18n/locales/en-US/common.json | 2 + web/i18n/locales/es-ES/common.json | 2 + web/i18n/locales/fa-IR/common.json | 2 + web/i18n/locales/fr-FR/common.json | 2 + web/i18n/locales/hi-IN/common.json | 2 + web/i18n/locales/id-ID/common.json | 2 + web/i18n/locales/it-IT/common.json | 2 + web/i18n/locales/ja-JP/common.json | 2 + web/i18n/locales/ko-KR/common.json | 2 + web/i18n/locales/lo-LA/common.json | 2 + web/i18n/locales/nl-NL/common.json | 2 + web/i18n/locales/pl-PL/common.json | 2 + web/i18n/locales/pt-BR/common.json | 2 + web/i18n/locales/ro-RO/common.json | 2 + web/i18n/locales/ru-RU/common.json | 2 + web/i18n/locales/sl-SI/common.json | 2 + web/i18n/locales/th-TH/common.json | 2 + web/i18n/locales/tr-TR/common.json | 2 + web/i18n/locales/uk-UA/common.json | 2 + web/i18n/locales/vi-VN/common.json | 2 + web/i18n/locales/zh-Hans/common.json | 2 + web/i18n/locales/zh-Hant/common.json | 2 + .../__tests__/request-error-toast.spec.ts | 4 +- web/service/base.spec.ts | 4 +- web/service/base.ts | 2 +- web/service/fetch.spec.ts | 4 +- web/service/request-error-toast.ts | 2 +- 597 files changed, 1942 insertions(+), 689 deletions(-) create mode 100644 web/app/notifications/__tests__/copy.browser.spec.tsx create mode 100644 web/app/notifications/__tests__/toast.spec.tsx create mode 100644 web/app/notifications/copy-error-action.tsx create mode 100644 web/app/notifications/host.tsx create mode 100644 web/app/notifications/index.tsx create mode 100644 web/app/notifications/notifications.stories.tsx diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx index 8c690c134dd..5428a446bc0 100644 --- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx @@ -1,6 +1,45 @@ +import type { ToastManager, ToastViewportProps } from '../index' import { userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' -import { createToast, createToastManager, toast, ToastHost } from '../index' +import { + createToast, + createToastManager, + ToastCard, + ToastPortal, + ToastProvider, + ToastViewport, + useToastManager, +} from '../index' + +function ToastCards() { + const { toasts } = useToastManager>() + return toasts.map((item) => ) +} + +function ExampleToastHost({ + manager, + timeout, + limit, + offset, +}: { + manager: ToastManager + timeout?: number + limit?: number + offset?: ToastViewportProps['offset'] +}) { + return ( + + + + + + + + ) +} + +const manager = createToastManager() +const toast = createToast(manager) const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement @@ -22,7 +61,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should render a success toast when called through the typed shortcut', async () => { - const screen = await render() + const screen = await render() toast.success('Saved', { description: 'Your changes are available now.', @@ -35,7 +74,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should keep multiple toast roots mounted in a collapsed stack', async () => { - const screen = await render() + const screen = await render() toast('First toast') await expect.element(screen.getByText('First toast')).toBeInTheDocument() @@ -63,7 +102,7 @@ describe('@langgenius/dify-ui/toast', () => { > Underlying action - + , ) @@ -130,7 +169,7 @@ describe('@langgenius/dify-ui/toast', () => { height: 20, }} /> - + , ) @@ -190,7 +229,7 @@ describe('@langgenius/dify-ui/toast', () => { height: 20, }} /> - + , ) @@ -220,7 +259,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should render a neutral toast when called directly', async () => { - const screen = await render() + const screen = await render() toast('Neutral toast') @@ -232,8 +271,8 @@ describe('@langgenius/dify-ui/toast', () => { const localToast = createToast(localManager) const screen = await render( <> - - + + , ) @@ -254,7 +293,7 @@ describe('@langgenius/dify-ui/toast', () => { it('should apply custom positioning to the viewport', async () => { const localManager = createToastManager() const localToast = createToast(localManager) - const screen = await render() + const screen = await render() localToast('Positioned viewport') @@ -268,7 +307,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should mark overflow toasts as limited when the stack exceeds the configured limit', async () => { - const screen = await render() + const screen = await render() toast('First toast') toast('Second toast') @@ -278,7 +317,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should dismiss a toast when dismiss(id) is called', async () => { - const screen = await render() + const screen = await render() const toastId = toast('Closable', { description: 'This toast can be removed.', @@ -295,7 +334,7 @@ describe('@langgenius/dify-ui/toast', () => { it('should close a toast when the dismiss button is clicked', async () => { const onClose = vi.fn() - const screen = await render() + const screen = await render() toast('Dismiss me', { description: 'Manual dismissal path.', @@ -352,7 +391,7 @@ describe('@langgenius/dify-ui/toast', () => { > Underlying action - + , ) @@ -397,7 +436,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should pass the host timeout to added toasts', async () => { - const screen = await render() + const screen = await render() toast('Auto dismiss') await expect.element(screen.getByText('Auto dismiss')).toBeInTheDocument() @@ -413,7 +452,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should keep a toast persistent when its timeout is zero', async () => { - const screen = await render() + const screen = await render() toast('Persistent', { timeout: 0, @@ -426,7 +465,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should update an existing toast', async () => { - const screen = await render() + const screen = await render() const toastId = toast.info('Loading', { description: 'Preparing your data…', @@ -445,7 +484,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should upsert an existing toast when add is called with the same id', async () => { - const screen = await render() + const screen = await render() toast('Draft saving', { id: 'draft-save-status', @@ -468,7 +507,7 @@ describe('@langgenius/dify-ui/toast', () => { it('should render and invoke toast action props', async () => { const onAction = vi.fn() - const screen = await render() + const screen = await render() toast('Action toast', { actionProps: { @@ -484,7 +523,7 @@ describe('@langgenius/dify-ui/toast', () => { }) it('should transition a promise toast from loading to success', async () => { - const screen = await render() + const screen = await render() let resolvePromise: ((value: string) => void) | undefined const promise = new Promise((resolve) => { diff --git a/packages/dify-ui/src/toast/index.stories.tsx b/packages/dify-ui/src/toast/index.stories.tsx index 2a5e480e0cb..331d05d550d 100644 --- a/packages/dify-ui/src/toast/index.stories.tsx +++ b/packages/dify-ui/src/toast/index.stories.tsx @@ -1,9 +1,48 @@ import type { Meta, StoryObj } from '@storybook/react-vite' +import type { ToastManager, ToastViewportProps } from '.' import * as React from 'react' import { expect, within } from 'storybook/test' -import { toast, ToastHost } from '.' +import { + createToast, + createToastManager, + ToastCard, + ToastPortal, + ToastProvider, + ToastViewport, + useToastManager, +} from '.' import { Button } from '../button' +function ToastCards() { + const { toasts } = useToastManager>() + return toasts.map((item) => ) +} + +function ExampleToastHost({ + manager, + timeout, + limit, + offset, +}: { + manager: ToastManager + timeout?: number + limit?: number + offset?: ToastViewportProps['offset'] +}) { + return ( + + + + + + + + ) +} + +const manager = createToastManager() +const toast = createToast(manager) + const longToastTitle = 'operation error S3: PutObject, exceeded maximum number of attempts, 3, StatusCode: 0, RequestID: , HostID: , request send failed' const longToastDescription = @@ -329,7 +368,7 @@ const UpdateExamples = () => { const ToastDocsDemo = () => { return ( - +
diff --git a/packages/dify-ui/src/toast/index.tsx b/packages/dify-ui/src/toast/index.tsx index c020fbd607a..b0b11616889 100644 --- a/packages/dify-ui/src/toast/index.tsx +++ b/packages/dify-ui/src/toast/index.tsx @@ -79,13 +79,23 @@ type ToastPromiseOptions = { error: ToastPromiseResultOption } -type ToastHostProps = { - timeout?: number - limit?: number - manager?: ToastManager - offset?: ToastHostOffset +type ToastCardProps = { + toast: ToastObject + /** Additional content composed inside the card. */ + children?: React.ReactNode } +type ToastViewportProps = { + offset?: ToastHostOffset + children: React.ReactNode +} + +const ToastProvider = BaseToast.Provider +type ToastProviderProps = BaseToast.Provider.Props +const ToastPortal = BaseToast.Portal +type ToastPortalProps = BaseToast.Portal.Props +const useToastManager = BaseToast.useToastManager + type ToastHostOffset = Pick type ToastDismiss = (toastId?: string) => void @@ -161,9 +171,6 @@ function createToastManager(): ToastManager { return BaseToast.createToastManager() } -const defaultToastManager = createToastManager() -const toast = createToast(defaultToastManager) - function ToastIcon({ type }: { type?: ToastRenderType }) { return type ? (
)}
+ {children}
}) { ) } -function ToastViewport({ offset }: { offset?: ToastHostOffset }) { - const { toasts } = BaseToast.useToastManager() - +function ToastViewport({ offset, children }: ToastViewportProps) { return ( - {toasts.map((toastItem) => ( - - ))} + {children} ) } -function ToastHost({ timeout, limit, manager = defaultToastManager, offset }: ToastHostProps) { - return ( - - - - - - ) +export { + createToast, + createToastManager, + ToastCard, + ToastPortal, + ToastProvider, + ToastViewport, + useToastManager, } -export { createToast, createToastManager, toast, ToastHost } - -export type { ToastHostProps } +export type { + ToastApi, + ToastCardProps, + ToastManager, + ToastPortalProps, + ToastProviderProps, + ToastViewportProps, +} diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index e5bf0ee65e4..d493da8194b 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -4,6 +4,7 @@ const config: StorybookConfig = { stories: [ '../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)', '../features/**/*.stories.@(js|jsx|mjs|ts|tsx)', + '../app/notifications/**/*.stories.@(js|jsx|mjs|ts|tsx)', ], addons: [ // Not working with Storybook Vite framework diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index e1a1638437d..f91a9f14ba1 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -1,8 +1,8 @@ import type { Decorator, Preview } from '@storybook/react' import type { Resource } from 'i18next' -import { ToastHost } from '@langgenius/dify-ui/toast' import { withThemeByDataAttribute } from '@storybook/addon-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { AppToastHost } from '@/app/notifications/host' import { I18nClientProvider as I18N } from '../app/components/provider/i18n' import commonEnUS from '../i18n/locales/en-US/common.json' import '../app/styles/markdown.css' @@ -38,7 +38,7 @@ export const decorators: Decorator[] = [ <> - + diff --git a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx index 6b2971465a2..66b2e54465f 100644 --- a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx +++ b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx @@ -9,12 +9,13 @@ */ import type { GetBillingSubscriptionData } from '@dify/contracts/api/console/billing/types.gen' import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' -import { toast, ToastHost } from '@langgenius/dify-ui/toast' import { cleanup, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { ALL_PLANS } from '@/app/components/billing/config' import { CloudPlanItem } from '@/app/components/billing/pricing/plans/cloud-plan-item' +import { toast } from '@/app/notifications' +import { AppToastHost } from '@/app/notifications/host' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' @@ -86,7 +87,7 @@ const renderCloudPlanItem = ({ const { wrapper } = createConsoleQueryWrapper() return render( <> - + ({ updateTracingStatus: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: vi.fn(), })) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/provider-config-modal.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/provider-config-modal.spec.tsx index 47880f0e3fd..c9adb3a699f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/provider-config-modal.spec.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/provider-config-modal.spec.tsx @@ -10,9 +10,9 @@ import type { TencentConfig, WeaveConfig, } from '../type' -import { toast } from '@langgenius/dify-ui/toast' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import ConfigBtn from '../config-button' import ProviderConfigModal from '../provider-config-modal' @@ -24,7 +24,7 @@ vi.mock('@/service/apps', () => ({ updateTracingConfig: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: vi.fn(), })) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 7873d05e3a0..254eb41736c 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -16,7 +16,6 @@ import type { import type { TracingStatus } from '@/models/app' import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useAtomValue } from 'jotai' @@ -38,6 +37,7 @@ import { WeaveIcon, } from '@/app/components/base/icons/src/public/tracing' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { usePathname } from '@/next/navigation' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index d2d05f8d898..a97de487bfc 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -23,7 +23,6 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useState } from 'react' @@ -31,6 +30,7 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import { toast } from '@/app/notifications' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import { docURL } from './config' import Field from './field' diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx index 9f7d89ca924..e8435f51d9d 100644 --- a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx @@ -1,12 +1,12 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Countdown from '@/app/components/signin/countdown' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common' diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx index 99449d9d568..7f98a682429 100644 --- a/web/app/(shareLayout)/webapp-reset-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -1,13 +1,13 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage' +import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx index 96ce4c1335e..03ee325dcfe 100644 --- a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -5,11 +5,11 @@ import { Field, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { RiCheckboxCircleFill } from '@remixicon/react' import { useCountDown } from 'ahooks' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { validPassword } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index e6a953db618..5d113736890 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -2,7 +2,6 @@ import type { FormEvent } from 'react' import { Button } from '@langgenius/dify-ui/button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' @@ -11,6 +10,7 @@ import { resolveWebAppLoginRedirect, } from '@/app/(shareLayout)/webapp-signin/login-redirect' import Countdown from '@/app/components/signin/countdown' +import { toast } from '@/app/notifications' import { useWebAppStore } from '@/context/web-app-context' import { useRouter, useSearchParams } from '@/next/navigation' import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common' diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx index b72d3fb3fcb..8e76fcd9d71 100644 --- a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx @@ -1,12 +1,12 @@ 'use client' import { zSsoProtocol } from '@dify/contracts/api/console/system-features/zod.gen' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useEffect } from 'react' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import AppUnavailable from '@/app/components/base/app-unavailable' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter, useSearchParams } from '@/next/navigation' import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx index bb67ca8e891..ddbed90e7aa 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -1,11 +1,11 @@ import { Button } from '@langgenius/dify-ui/button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage' +import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import { useRouter, useSearchParams } from '@/next/navigation' import { sendWebAppEMailLoginCode } from '@/service/common' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index 2fc180f80bb..1d3548a58c4 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -5,13 +5,13 @@ import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { navigateAfterWebAppLogin, resolveWebAppLoginRedirect, } from '@/app/(shareLayout)/webapp-signin/login-redirect' +import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import { useWebAppStore } from '@/context/web-app-context' import Link from '@/next/link' diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx index 73cdffa360f..210a7077e31 100644 --- a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx @@ -2,11 +2,11 @@ import type { SsoProtocol } from '@dify/contracts/api/console/system-features/types.gen' import { zSsoProtocol } from '@dify/contracts/api/console/system-features/zod.gen' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import { toast } from '@/app/notifications' import { useRouter, useSearchParams } from '@/next/navigation' import { fetchMembersOAuth2SSOUrl, diff --git a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx index bab0d50d9e4..243a7cd6293 100644 --- a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx +++ b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx @@ -7,13 +7,13 @@ import type { ImageFile } from '@/types/app' import { Avatar } from '@langgenius/dify-ui/avatar' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import ImageInput from '@/app/components/base/app-icon-picker/ImageInput' import Divider from '@/app/components/base/divider' import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks' +import { toast } from '@/app/notifications' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { updateUserProfile } from '@/service/common' import { createAvatarImageFile, createCroppedAvatarImage } from './avatar-image' diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index 391a8d36281..5bf5cc29f3e 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -4,11 +4,11 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common' import { useLogout } from '@/service/use-common' diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 6fa3ee3d882..51177d7de45 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -8,13 +8,13 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import PremiumBadge from '@/app/components/base/premium-badge' import Collapse from '@/app/components/header/account-setting/collapse' +import { toast } from '@/app/notifications' import { validPassword } from '@/config' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/account/(commonLayout)/delete-account/components/__tests__/feed-back.spec.tsx b/web/app/account/(commonLayout)/delete-account/components/__tests__/feed-back.spec.tsx index 6ee1deb284a..2a5649f7fff 100644 --- a/web/app/account/(commonLayout)/delete-account/components/__tests__/feed-back.spec.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/__tests__/feed-back.spec.tsx @@ -28,7 +28,7 @@ vi.mock('@/service/use-common', () => ({ useLogout: () => ({ mutateAsync: mockLogout }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { info: vi.fn() }, })) diff --git a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx index 8b40a65adec..754bd7285ad 100644 --- a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx @@ -4,10 +4,10 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index 410aff077c2..2f6ef347f92 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -3,7 +3,6 @@ import { Avatar } from '@langgenius/dify-ui/avatar' import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiAccountCircleLine, RiGlobalLine, @@ -16,6 +15,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { toast } from '@/app/notifications' import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' diff --git a/web/app/account/oauth/authorize/use-silent-authorize.ts b/web/app/account/oauth/authorize/use-silent-authorize.ts index c967cd7cfa4..ad561b2e3f6 100644 --- a/web/app/account/oauth/authorize/use-silent-authorize.ts +++ b/web/app/account/oauth/authorize/use-silent-authorize.ts @@ -1,10 +1,10 @@ -import { toast } from '@langgenius/dify-ui/toast' import Cookies from 'js-cookie' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' import { flushEvents } from '@/app/components/base/amplitude/utils' import { reportOAuthRegistrationIfNewUser } from '@/app/components/oauth-registration-attribution' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' export function buildReturnUrl(pathname: string, search: string) { diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index a07aae99ba1..cf6b8161f60 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -66,7 +66,7 @@ vi.mock('@/app/components/app/use-export-app-dsl', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.api, { success: vi.fn((message, options) => toastMocks.call({ type: 'success', message, ...options })), error: vi.fn((message, options) => toastMocks.call({ type: 'error', message, ...options })), diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 6936882a1b1..116c09ad12b 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -6,12 +6,12 @@ import type { Dispatch, SetStateAction } from 'react' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { App } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { useExportAppDsl, useExportWorkflowAppDsl } from '@/app/components/app/use-export-app-dsl' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx index 86826918303..91cf28d2b56 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx @@ -129,7 +129,7 @@ vi.mock('@/service/datasets', () => ({ deleteDataset: (...args: unknown[]) => mockDeleteDataset(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (...args: unknown[]) => mockToast(...args), }, diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index bfefae17b7e..a1def0ee008 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -17,12 +17,12 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' diff --git a/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx b/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx index 801d831b6ec..5d921704730 100644 --- a/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx +++ b/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx @@ -37,7 +37,7 @@ vi.mock('@/utils/download', () => ({ downloadBlob: (args: { data: Blob; fileName: string }) => mockDownloadBlob(args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (...args: unknown[]) => mockToastSuccess(...args), error: (...args: unknown[]) => mockToastError(...args), diff --git a/web/app/components/app-sidebar/snippet-info/dropdown.tsx b/web/app/components/app-sidebar/snippet-info/dropdown.tsx index c1f69f0ff93..6ddc0d0081d 100644 --- a/web/app/components/app-sidebar/snippet-info/dropdown.tsx +++ b/web/app/components/app-sidebar/snippet-info/dropdown.tsx @@ -18,7 +18,6 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -27,6 +26,7 @@ import { canCreateAndModifySnippets, canManageSnippets, } from '@/app/components/snippets/utils/permission' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter } from '@/next/navigation' import { diff --git a/web/app/components/app/__tests__/use-export-app-dsl.spec.tsx b/web/app/components/app/__tests__/use-export-app-dsl.spec.tsx index cd54f987bc9..8dd0e69a450 100644 --- a/web/app/components/app/__tests__/use-export-app-dsl.spec.tsx +++ b/web/app/components/app/__tests__/use-export-app-dsl.spec.tsx @@ -34,7 +34,7 @@ vi.mock('@/utils/download', () => ({ downloadBlob: mocks.downloadBlob, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mocks.toastError, promise: mocks.toastPromise, diff --git a/web/app/components/app/access-point/__tests__/environment-access-control.spec.tsx b/web/app/components/app/access-point/__tests__/environment-access-control.spec.tsx index a3e5376086b..57f3405fe6d 100644 --- a/web/app/components/app/access-point/__tests__/environment-access-control.spec.tsx +++ b/web/app/components/app/access-point/__tests__/environment-access-control.spec.tsx @@ -64,7 +64,7 @@ vi.mock('@/service/access-control', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx b/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx index 210dd6b189a..fce4ff54df5 100644 --- a/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx +++ b/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx @@ -1,8 +1,8 @@ import type { ReactElement } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createTestQueryClient } from '@/test/query-client' import { EnvironmentServiceApiCard } from '../deployed-environment-access-points/environment-service-api-card' @@ -105,7 +105,7 @@ vi.mock('@/app/components/app/access-point/shared/api-secret-key-button', () => }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx index 2e47e1084bd..b272e10ed63 100644 --- a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx @@ -1,9 +1,9 @@ import type { AccessPointAppInfo, PublishedWorkflow } from '../shared/utils' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createTestQueryClient } from '@/test/query-client' import { AppModeEnum } from '@/types/app' @@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ updateServer: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx b/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx index f317ef8880a..2f26c1af6bd 100644 --- a/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx @@ -1,9 +1,9 @@ import type { AccessPointAppInfo } from '../shared/utils' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' +import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createTestQueryClient } from '@/test/query-client' import { AppModeEnum } from '@/types/app' @@ -14,7 +14,7 @@ const mocks = vi.hoisted(() => ({ apiEnable: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx index c07e7592f70..d4c78c09465 100644 --- a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx @@ -1,9 +1,9 @@ import type { AccessPointAppInfo } from '../shared/utils' import type { AppTrigger } from '@/service/use-tools' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createTestQueryClient } from '@/test/query-client' import { AppModeEnum } from '@/types/app' @@ -19,7 +19,7 @@ const mocks = vi.hoisted(() => ({ updateTriggerStatus: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/__tests__/use-access-point-actions.spec.ts b/web/app/components/app/access-point/__tests__/use-access-point-actions.spec.ts index b0b169bbc0b..711884b2afa 100644 --- a/web/app/components/app/access-point/__tests__/use-access-point-actions.spec.ts +++ b/web/app/components/app/access-point/__tests__/use-access-point-actions.spec.ts @@ -11,7 +11,7 @@ const mocks = vi.hoisted(() => ({ updateAppSiteConfig: vi.fn().mockResolvedValue({}), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: mocks.toast })) +vi.mock('@/app/notifications', () => ({ toast: mocks.toast })) vi.mock('@/app/components/app/store', () => ({ useStore: (selector: (state: { setAppDetail: typeof mocks.setAppDetail }) => unknown) => diff --git a/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx b/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx index 4115a40e206..402b94c5476 100644 --- a/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx @@ -1,11 +1,11 @@ import type { AccessPointAppInfo, PublishedWorkflow } from '../shared/utils' import type { InputVar, Node } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { AccessMode } from '@/models/access-control' import { render } from '@/test/console/render' import { createTestQueryClient } from '@/test/query-client' @@ -18,7 +18,7 @@ const mocks = vi.hoisted(() => ({ resetSiteAccessToken: vi.fn().mockResolvedValue({}), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx index eff1bf597fe..59a754cc314 100644 --- a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx @@ -11,7 +11,6 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -19,6 +18,7 @@ import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { useInvalidateMCPServerDetail, diff --git a/web/app/components/app/access-point/built-in-access-points/service-api-card.tsx b/web/app/components/app/access-point/built-in-access-points/service-api-card.tsx index 09f1506ce88..4b6fa613341 100644 --- a/web/app/components/app/access-point/built-in-access-points/service-api-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/service-api-card.tsx @@ -2,11 +2,11 @@ import type { AccessPointAppInfo } from '../shared/utils' import type { AccessPointAvailability } from '@/app/components/base/access-point/status' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { getAccessPointStatus } from '@/app/components/base/access-point/status' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { ServiceApiCardView } from '../shared/service-api-card-view' import { getBuiltInAccessUrls } from '../shared/utils' diff --git a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx index a5850a1f8ca..5c46fac1ee6 100644 --- a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx @@ -5,7 +5,6 @@ import type { TriggerWithProvider } from '@/app/components/workflow/block-select import type { AppTrigger } from '@/service/use-tools' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -13,6 +12,7 @@ import { AccessPointCard, AccessPointEmptyContent } from '@/app/components/base/ import BlockIcon from '@/app/components/workflow/block-icon' import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import Link from '@/next/link' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/access-point/built-in-access-points/web-app-card.tsx b/web/app/components/app/access-point/built-in-access-points/web-app-card.tsx index 6f7e09de87c..4b8d353990c 100644 --- a/web/app/components/app/access-point/built-in-access-points/web-app-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/web-app-card.tsx @@ -14,7 +14,6 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -28,6 +27,7 @@ import { AccessPointCard } from '@/app/components/base/access-point/card' import { getAccessPointStatus } from '@/app/components/base/access-point/status' import { AccessPointUrl } from '@/app/components/base/access-point/url' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { AccessMode } from '@/models/access-control' import { useAppWhiteListSubjects } from '@/service/access-control/use-app-access-control' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/access-point/deployed-environment-access-points/environment-access-control.tsx b/web/app/components/app/access-point/deployed-environment-access-points/environment-access-control.tsx index 3213bcb5afc..423d3b0cc2b 100644 --- a/web/app/components/app/access-point/deployed-environment-access-points/environment-access-control.tsx +++ b/web/app/components/app/access-point/deployed-environment-access-points/environment-access-control.tsx @@ -6,11 +6,11 @@ import type { AccessControlSubjectsStatus, } from '@/app/components/app/app-access-control/specific-groups-or-members' import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { AccessControlForm } from '@/app/components/app/app-access-control/access-control-form' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AccessMode, SubjectType } from '@/models/access-control' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/access-point/deployed-environment-access-points/environment-service-api-card.tsx b/web/app/components/app/access-point/deployed-environment-access-points/environment-service-api-card.tsx index bcc25889b07..02c3e01dde1 100644 --- a/web/app/components/app/access-point/deployed-environment-access-points/environment-service-api-card.tsx +++ b/web/app/components/app/access-point/deployed-environment-access-points/environment-service-api-card.tsx @@ -1,9 +1,9 @@ 'use client' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { ServiceApiCardView } from '../shared/service-api-card-view' diff --git a/web/app/components/app/access-point/deployed-environment-access-points/environment-web-app-card.tsx b/web/app/components/app/access-point/deployed-environment-access-points/environment-web-app-card.tsx index 0e8ce4ca6e6..95de24e62f8 100644 --- a/web/app/components/app/access-point/deployed-environment-access-points/environment-web-app-card.tsx +++ b/web/app/components/app/access-point/deployed-environment-access-points/environment-web-app-card.tsx @@ -11,7 +11,6 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -21,6 +20,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AccessMode, isAccessMode } from '@/models/access-control' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/access-point/shared/use-access-point-actions.ts b/web/app/components/app/access-point/shared/use-access-point-actions.ts index eb82d3d5743..ed08627abb1 100644 --- a/web/app/components/app/access-point/shared/use-access-point-actions.ts +++ b/web/app/components/app/access-point/shared/use-access-point-actions.ts @@ -3,11 +3,11 @@ import type { ConfigParams } from '@/app/components/app/overview/settings' import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' +import { toast } from '@/app/notifications' import { fetchAppDetail, updateAppSiteConfig } from '@/service/apps' import { consoleQuery } from '@/service/console' import { asyncRunSafe } from '@/utils' diff --git a/web/app/components/app/annotation/__tests__/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx index 865196a19fb..06bd55a94c2 100644 --- a/web/app/components/app/annotation/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -2,9 +2,9 @@ import type { ReactElement } from 'react' import type { Mock } from 'vite-plus/test' import type { AnnotationItem } from '../type' import type { App } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' +import { toast } from '@/app/notifications' import { addAnnotation, delAnnotation, diff --git a/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx index dfade0ee399..d53ae4f4746 100644 --- a/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import { renderWithConsoleQuery } from '@/test/console/query-data' import AddAnnotationModal from '../index' const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn((args) => mockToastNotify(args)), }, diff --git a/web/app/components/app/annotation/add-annotation-modal/index.tsx b/web/app/components/app/annotation/add-annotation-modal/index.tsx index f42400f84d0..5459b456b81 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.tsx @@ -13,13 +13,13 @@ import { DrawerTitle, DrawerViewport, } from '@langgenius/dify-ui/drawer' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import AnnotationFull from '@/app/components/billing/annotation-full' +import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' import EditItem, { EditItemType } from './edit-item' diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx index 4ce407f3ca3..fb1f689757a 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx @@ -10,7 +10,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string, options?: Record) => toastMocks.notify({ type: 'success', message, ...options }), diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index 7ecd4160d62..22744957f9c 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -42,7 +42,7 @@ vi.mock('@/app/components/billing/annotation-full', () => ({ })) const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx index 38eb1e4ae00..7388d5cea69 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx @@ -2,12 +2,12 @@ import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiDeleteBinLine } from '@remixicon/react' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' +import { toast } from '@/app/notifications' export type Props = Readonly<{ file: File | undefined diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index ff4d2ba0242..4fb651b70e7 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -10,6 +9,7 @@ import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import AnnotationFull from '@/app/components/billing/annotation-full' +import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx index 7b2de8b4626..4030169a517 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import EditAnnotationModal from '../index' const { mockEditAnnotation } = vi.hoisted(() => ({ diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 67ab2207ee9..d3147815aba 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -18,11 +18,11 @@ import { DrawerTitle, DrawerViewport, } from '@langgenius/dify-ui/drawer' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' +import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' import { editAnnotation } from '@/service/annotation' import EditItem, { EditItemType } from './edit-item' diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 1d41e9cec7e..01db0cb7a27 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -8,7 +8,6 @@ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Pagination } from '@langgenius/dify-ui/pagination' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { RiEqualizer2Line } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' @@ -20,6 +19,7 @@ import ConfigParamModal from '@/app/components/base/features/new-feature-panel/a import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' +import { toast } from '@/app/notifications' import { APP_PAGE_LIMIT } from '@/config' import { useDocLink } from '@/context/i18n' import { deploymentEditionAtom } from '@/features/system-features/state' diff --git a/web/app/components/app/app-access-control/__tests__/index.spec.tsx b/web/app/components/app/app-access-control/__tests__/index.spec.tsx index 5e41e3cd594..946ba118a9c 100644 --- a/web/app/components/app/app-access-control/__tests__/index.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import type { ReactElement } from 'react' import type { App } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { AccessMode } from '@/models/access-control' import { renderWithConsoleQuery } from '@/test/console/query-data' import AccessControl from '../index' diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index b6fddfe41d4..92216f70e0b 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -6,10 +6,10 @@ import type { AccessControlSubjectsStatus, } from './specific-groups-or-members' import type { Subject } from '@/models/access-control' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AccessMode, isAccessMode, SubjectType } from '@/models/access-control' import { useAppWhiteListSubjects } from '@/service/access-control' diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index 3e9bbb4781f..a4adce4dcca 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -160,7 +160,7 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (...args: unknown[]) => mockToastError(...args), success: (...args: unknown[]) => mockToastSuccess(...args), diff --git a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx index 44c99cc2f29..24697749f7a 100644 --- a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx @@ -1,8 +1,8 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' +import { toast } from '@/app/notifications' import VersionInfoModal from '../version-info-modal' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts b/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts index 740ee176c65..2ccdd0c4424 100644 --- a/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts +++ b/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { publishToCreatorsPlatform } from '@/service/apps' export function useMarketplacePublish(appId?: string) { diff --git a/web/app/components/app/app-publisher/publisher-content/use-version-info.ts b/web/app/components/app/app-publisher/publisher-content/use-version-info.ts index 25f2ee00cba..6e722b96ea0 100644 --- a/web/app/components/app/app-publisher/publisher-content/use-version-info.ts +++ b/web/app/components/app/app-publisher/publisher-content/use-version-info.ts @@ -1,8 +1,8 @@ import type { WorkflowResponse } from '@dify/contracts/api/console/apps/types.gen' import type { AppModeEnum } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useInvalidateAppWorkflow, useUpdateWorkflow } from '@/service/use-workflow' type VersionInfoUpdate = { diff --git a/web/app/components/app/app-publisher/version-info-modal.tsx b/web/app/components/app/app-publisher/version-info-modal.tsx index 01120b0a44d..8a806e08d8d 100644 --- a/web/app/components/app/app-publisher/version-info-modal.tsx +++ b/web/app/components/app/app-publisher/version-info-modal.tsx @@ -5,9 +5,9 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' type VersionInfoModalProps = { isOpen: boolean diff --git a/web/app/components/app/configuration/__tests__/index.spec.tsx b/web/app/components/app/configuration/__tests__/index.spec.tsx index 049a70c11de..3847c2f5535 100644 --- a/web/app/components/app/configuration/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ -import { ToastHost } from '@langgenius/dify-ui/toast' import { act, render, screen } from '@testing-library/react' +import { AppToastHost } from '@/app/notifications/host' import Configuration from '../index' import { toast } from '../toast' @@ -23,7 +23,7 @@ describe('Configuration', () => { it('should render configuration notifications in its offset viewport', async () => { render( <> - + , ) diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2dadd0373e3..c88bb02cd2b 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { ToastHost } from '@langgenius/dify-ui/toast' import * as React from 'react' +import { AppToastHost } from '@/app/notifications/host' import ConfigurationView from './configuration-view' import { useConfiguration } from './hooks/use-configuration' import { appConfigurationToastManager } from './toast' @@ -9,7 +9,7 @@ const Configuration = () => { const viewModel = useConfiguration() return ( <> - + ) diff --git a/web/app/components/app/configuration/toast.ts b/web/app/components/app/configuration/toast.ts index f1e64bbc1f8..35678e9c205 100644 --- a/web/app/components/app/configuration/toast.ts +++ b/web/app/components/app/configuration/toast.ts @@ -2,7 +2,5 @@ import { createToast, createToastManager } from '@langgenius/dify-ui/toast' -const appConfigurationToastManager = createToastManager() -const toast = createToast(appConfigurationToastManager) - -export { appConfigurationToastManager, toast } +export const appConfigurationToastManager = createToastManager() +export const toast = createToast(appConfigurationToastManager) diff --git a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx index 773b5fad799..4ebf8d9aaaa 100644 --- a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx @@ -122,7 +122,7 @@ vi.mock('@/app/components/explore/create-app-modal', () => ({
) : null, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (...args: unknown[]) => mockToastSuccess(...args), error: (...args: unknown[]) => mockToastError(...args), diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index a3915247047..5d7e5207c5e 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -5,7 +5,6 @@ import type { App } from '@/models/explore' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useDebouncedValue } from 'foxact/use-debounced-value' import { useAtomValue } from 'jotai' @@ -17,6 +16,7 @@ import Divider from '@/app/components/base/divider' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index 68af8670fe4..9f011a711de 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -73,7 +73,7 @@ const toastMocks = vi.hoisted(() => ({ mockToastSuccess: vi.fn(), mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: toastMocks.mockToastSuccess, error: toastMocks.mockToastError, diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 47f2810f4ea..24dafa3d8d8 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -8,7 +8,6 @@ import { cn } from '@langgenius/dify-ui/cn' import { Input } from '@langgenius/dify-ui/input' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' @@ -18,6 +17,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import Divider from '@/app/components/base/divider' import AppsFull from '@/app/components/billing/apps-full-in-dialog' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index fc0af74cfae..6b4b9bc4c3d 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -132,7 +132,7 @@ vi.mock('@/utils/imported-app-redirection', () => ({ mockResolveImportedAppRedirectionTarget(target), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign((...args: unknown[]) => toastMocks.call(...args), { success: (...args: unknown[]) => toastMocks.success(...args), error: (...args: unknown[]) => toastMocks.error(...args), diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx index 97e2b364f11..2d4084445bd 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx @@ -1,10 +1,10 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' +import { toast } from '@/app/notifications' import { Uploader } from '../uploader' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 95a0cdf0ba1..26819825675 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -17,7 +17,6 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -25,6 +24,7 @@ import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index 448e07ed7e5..2223d64578f 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -3,9 +3,9 @@ import type { ChangeEvent, DragEvent, MouseEvent, RefObject } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useId, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { formatFileSize } from '@/utils/format' type Props = Readonly<{ diff --git a/web/app/components/app/deploy/__tests__/index.spec.tsx b/web/app/components/app/deploy/__tests__/index.spec.tsx index 4df3f221db9..83851c64d24 100644 --- a/web/app/components/app/deploy/__tests__/index.spec.tsx +++ b/web/app/components/app/deploy/__tests__/index.spec.tsx @@ -19,9 +19,9 @@ import { PluginCategory, RuntimeState, } from '@dify/contracts/enterprise-app-deploy/types.gen' -import { toast } from '@langgenius/dify-ui/toast' import { act, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { appWorkflowQueryOptions, @@ -852,7 +852,7 @@ vi.mock('#i18n', async (importOriginal) => ({ useLocale: () => 'en-US', })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/app/deploy/environment-table/row-actions.tsx b/web/app/components/app/deploy/environment-table/row-actions.tsx index 976807ceab1..5fa3f678454 100644 --- a/web/app/components/app/deploy/environment-table/row-actions.tsx +++ b/web/app/components/app/deploy/environment-table/row-actions.tsx @@ -12,10 +12,10 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { Fragment, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' +import { toast } from '@/app/notifications' import { getEnvironmentDeploymentActions } from '../utils/environment-deployment' import { UndeployConfirmDialog } from './undeploy-confirm-dialog' diff --git a/web/app/components/app/deploy/hooks/use-undeploy-workflow.ts b/web/app/components/app/deploy/hooks/use-undeploy-workflow.ts index 7666906fd34..07a7e02fbb0 100644 --- a/web/app/components/app/deploy/hooks/use-undeploy-workflow.ts +++ b/web/app/components/app/deploy/hooks/use-undeploy-workflow.ts @@ -1,10 +1,10 @@ 'use client' import type { EnvironmentDeployment } from '@dify/contracts/enterprise-app-deploy/types.gen' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' export function useUndeployWorkflow(appId: string) { diff --git a/web/app/components/app/deploy/shared/deployment-configuration/use-deployment-configuration-form.ts b/web/app/components/app/deploy/shared/deployment-configuration/use-deployment-configuration-form.ts index baa4388d5f1..c2b6efc7c2c 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/use-deployment-configuration-form.ts +++ b/web/app/components/app/deploy/shared/deployment-configuration/use-deployment-configuration-form.ts @@ -3,8 +3,8 @@ import type { FormEventHandler } from 'react' import type { DeploymentConfigurationQueryState } from './use-deployment-configuration-queries' import type { DeploymentConfigurationValuesController } from './use-deployment-configuration-values' -import { toast } from '@langgenius/dify-ui/toast' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDeployWorkflow } from '../../hooks/use-deploy-workflow' import { credentialProviderName, diff --git a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx index f36ee285ac1..081afa0339e 100644 --- a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx @@ -14,7 +14,7 @@ const { mockAppQuota, toastErrorMock } = vi.hoisted(() => ({ toastErrorMock: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (...args: unknown[]) => toastErrorMock(...args), }, diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 85e6cab976b..2f31e1adb21 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -5,7 +5,6 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' @@ -13,6 +12,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppsFull from '@/app/components/billing/apps-full-in-dialog' +import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' import AppIconPicker from '../../base/app-icon-picker' diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 758229d009e..790f928229c 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -30,7 +30,6 @@ import { } from '@langgenius/dify-ui/drawer' import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiEditFill } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' @@ -52,6 +51,7 @@ import CopyIcon from '@/app/components/base/copy-icon' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import MessageLogModal from '@/app/components/base/message-log-modal' import { WorkflowContextProvider } from '@/app/components/workflow/context' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' diff --git a/web/app/components/app/overview/settings/__tests__/index.spec.tsx b/web/app/components/app/overview/settings/__tests__/index.spec.tsx index 71683d3d37f..e5fce70fcbc 100644 --- a/web/app/components/app/overview/settings/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/settings/__tests__/index.spec.tsx @@ -44,7 +44,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 00a1b78214c..2ecfba0aec1 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -27,7 +27,6 @@ import { } from '@langgenius/dify-ui/select' import { Switch } from '@langgenius/dify-ui/switch' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -43,6 +42,7 @@ import { pricingQueryParamName, pricingQueryParser, } from '@/app/components/billing/pricing/query-params' +import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' import { languages } from '@/i18n/language' import Link from '@/next/link' diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index 35f7a381bef..9ce404cad91 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -102,7 +102,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string, options?: Record) => toastMocks.notify({ type: 'success', message, ...options }), diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index ba56025dbf8..c70a90a57e0 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -16,13 +16,13 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' import AppsFull from '@/app/components/billing/apps-full-in-dialog' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/app/text-generate/item/__tests__/action-groups.spec.tsx b/web/app/components/app/text-generate/item/__tests__/action-groups.spec.tsx index 775f0e3cc57..d32565093a4 100644 --- a/web/app/components/app/text-generate/item/__tests__/action-groups.spec.tsx +++ b/web/app/components/app/text-generate/item/__tests__/action-groups.spec.tsx @@ -14,7 +14,7 @@ vi.mock('copy-to-clipboard', () => ({ default: (...args: unknown[]) => mockCopy(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (...args: unknown[]) => mockSuccess(...args), }, diff --git a/web/app/components/app/text-generate/item/__tests__/index.spec.tsx b/web/app/components/app/text-generate/item/__tests__/index.spec.tsx index 8b1f7b35110..45f153f997e 100644 --- a/web/app/components/app/text-generate/item/__tests__/index.spec.tsx +++ b/web/app/components/app/text-generate/item/__tests__/index.spec.tsx @@ -56,7 +56,7 @@ vi.mock('@/app/components/base/markdown', () => ({ Markdown: ({ content }: { content: string }) =>
{`markdown:${content}`}
, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { warning: (...args: unknown[]) => mockToastWarning(...args), success: vi.fn(), diff --git a/web/app/components/app/text-generate/item/action-groups.tsx b/web/app/components/app/text-generate/item/action-groups.tsx index b07a224ba59..1c16273a321 100644 --- a/web/app/components/app/text-generate/item/action-groups.tsx +++ b/web/app/components/app/text-generate/item/action-groups.tsx @@ -4,11 +4,11 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type' import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { AppSourceType } from '@/service/share' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { Toggle } from '@langgenius/dify-ui/toggle' import copy from 'copy-to-clipboard' import { useTranslation } from 'react-i18next' import NewAudioButton from '@/app/components/base/new-audio-button' +import { toast } from '@/app/notifications' import { AppSourceType as AppSourceTypeEnum } from '@/service/share' import { getCopyContent, MAX_GENERATION_DEPTH } from './utils' diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index da2c0683474..7541f973c6c 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -5,7 +5,6 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type' import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { SiteInfo } from '@/models/share' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiPlayList2Line, RiSparklingFill } from '@remixicon/react' import { useBoolean } from 'ahooks' import * as React from 'react' @@ -15,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { useChatContext } from '@/app/components/base/chat/chat/context' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { Markdown } from '@/app/components/base/markdown' +import { toast } from '@/app/notifications' import { useParams } from '@/next/navigation' import { fetchTextGenerationMessage } from '@/service/debug' import { diff --git a/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx b/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx index d48f2ea8905..93863263268 100644 --- a/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx +++ b/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx @@ -1,9 +1,9 @@ import type { ISavedItemsProps } from '../index' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' import copy from 'copy-to-clipboard' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import SavedItems from '../index' vi.mock('copy-to-clipboard', () => ({ diff --git a/web/app/components/app/text-generate/saved-items/index.tsx b/web/app/components/app/text-generate/saved-items/index.tsx index 1f8fdc3e763..bb7e80cd219 100644 --- a/web/app/components/app/text-generate/saved-items/index.tsx +++ b/web/app/components/app/text-generate/saved-items/index.tsx @@ -3,13 +3,13 @@ import type { FC } from 'react' import type { SavedMessage } from '@/models/debug' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { RiClipboardLine, RiDeleteBinLine } from '@remixicon/react' import copy from 'copy-to-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' +import { toast } from '@/app/notifications' import NoData from './no-data' export type ISavedItemsProps = { diff --git a/web/app/components/app/use-export-app-dsl.ts b/web/app/components/app/use-export-app-dsl.ts index 32533de40d4..927e9da58af 100644 --- a/web/app/components/app/use-export-app-dsl.ts +++ b/web/app/components/app/use-export-app-dsl.ts @@ -1,10 +1,10 @@ 'use client' import type { EnvironmentVariableItemResponse } from '@dify/contracts/api/console/apps/types.gen' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleClient } from '@/service/console' import { downloadBlob } from '@/utils/download' diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index e7e4b988469..e164657fe1b 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -141,7 +141,7 @@ const toastMocks = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/apps/app-card/index.tsx b/web/app/components/apps/app-card/index.tsx index a7676157440..6e441135c8a 100644 --- a/web/app/components/apps/app-card/index.tsx +++ b/web/app/components/apps/app-card/index.tsx @@ -4,7 +4,6 @@ import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' import type { WorkflowOnlineUser } from '@/models/app' import { zIconType } from '@dify/contracts/api/console/apps/zod.gen' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { memo, useCallback, useId, useMemo } from 'react' @@ -12,6 +11,7 @@ import { useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import { UserAvatarList } from '@/app/components/base/user-avatar-list' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/apps/app-card/interactions.tsx b/web/app/components/apps/app-card/interactions.tsx index 23713d5439f..5b38e3e5943 100644 --- a/web/app/components/apps/app-card/interactions.tsx +++ b/web/app/components/apps/app-card/interactions.tsx @@ -36,7 +36,6 @@ import { import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { Toggle } from '@langgenius/dify-ui/toggle' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useMutation, useSuspenseQuery } from '@tanstack/react-query' @@ -50,6 +49,7 @@ import { getStepByStepTourDropdownMenuContentProps, useStepByStepTourControlledDropdown, } from '@/app/components/step-by-step-tour/dropdown-menu' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/apps/import-from-marketplace-template-modal.tsx b/web/app/components/apps/import-from-marketplace-template-modal.tsx index d92dfdc1053..13227a97bb9 100644 --- a/web/app/components/apps/import-from-marketplace-template-modal.tsx +++ b/web/app/components/apps/import-from-marketplace-template-modal.tsx @@ -2,11 +2,11 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { MARKETPLACE_API_PREFIX } from '@/config' import { fetchMarketplaceTemplateDSL, diff --git a/web/app/components/apps/starred-app-card.tsx b/web/app/components/apps/starred-app-card.tsx index 253ee98af87..f71b18291ad 100644 --- a/web/app/components/apps/starred-app-card.tsx +++ b/web/app/components/apps/starred-app-card.tsx @@ -3,13 +3,13 @@ import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' import { zIconType } from '@dify/contracts/api/console/apps/zod.gen' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { memo, useCallback, useId, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx index fe52d3d9e06..db020f292ef 100644 --- a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx @@ -23,7 +23,7 @@ vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx index 5f6d1ee5fde..3cfa551f2f7 100644 --- a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx @@ -21,7 +21,7 @@ vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index 9e0bd43e163..d32076e7db3 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentIteration, AgentLogDetailResponse } from '@/models/log' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { uniq } from 'es-toolkit/array' import { flatten } from 'es-toolkit/compat' import * as React from 'react' @@ -11,6 +10,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { fetchAgentLogDetail } from '@/service/log' import ResultPanel from './result' import TracingPanel from './tracing' diff --git a/web/app/components/base/agent-log-modal/index.stories.tsx b/web/app/components/base/agent-log-modal/index.stories.tsx index 7008274c8a6..8279a45e37d 100644 --- a/web/app/components/base/agent-log-modal/index.stories.tsx +++ b/web/app/components/base/agent-log-modal/index.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' -import { ToastHost } from '@langgenius/dify-ui/toast' import { useEffect, useRef } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' +import { AppToastHost } from '@/app/notifications/host' import AgentLogModal from '.' const MOCK_RESPONSE: AgentLogDetailResponse = { @@ -104,7 +104,7 @@ const AgentLogModalDemo = ({ width = 960 }: { width?: number }) => { return ( <> - +
const createWrapper = (queryClient: QueryClient) => { return ({ children }: { children: ReactNode }) => ( - + {children} ) diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index c1625ef99a9..c39b263122d 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -3,7 +3,6 @@ import type { ExtraContent } from '../chat/type' import type { Callback, ChatConfig, ChatItem, OnFeedback } from '../types' import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -14,6 +13,7 @@ import { } from '@/app/components/base/chat/storage' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { InputVarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' import { changeLanguage } from '@/i18n/client' diff --git a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx index a9c3a7af1b3..ee344841691 100644 --- a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx @@ -5,7 +5,7 @@ import { TransferMethod } from '@/types/app' import { useCheckInputsForms } from '../check-input-forms-hooks' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index acaabee1935..e37bcca4d93 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -24,7 +24,7 @@ vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn() }, toast: { success: vi.fn(), diff --git a/web/app/components/base/chat/chat/__tests__/question.spec.tsx b/web/app/components/base/chat/chat/__tests__/question.spec.tsx index fa1fa3cba37..2d5cc31ac0b 100644 --- a/web/app/components/base/chat/chat/__tests__/question.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/question.spec.tsx @@ -1,11 +1,11 @@ import type { Theme } from '../../embedded-chatbot/theme/theme' import type { ChatConfig, ChatItem, OnRegenerate } from '../../types' import type { FileEntity } from '@/app/components/base/file-uploader/types' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import copy from 'copy-to-clipboard' import * as React from 'react' +import { toast } from '@/app/notifications' import { createTheme } from '../../embedded-chatbot/theme/theme' import { ChatContextProvider } from '../context-provider' import Question from '../question' diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx index 67e7bbd8bbc..431cc4fccd9 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx @@ -15,7 +15,7 @@ const { mockSetShowAnnotationFullModal, mockT, mockAddAnnotation } = vi.hoisted( vi.mock('copy-to-clipboard', () => ({ default: vi.fn() })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn() }, })) diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index cecf5d452b2..05db31c0633 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -12,7 +12,6 @@ import { } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { Toggle } from '@langgenius/dify-ui/toggle' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' @@ -22,6 +21,7 @@ import EditReplyModal from '@/app/components/app/annotation/edit-annotation-moda import Log from '@/app/components/base/chat/chat/log' import AnnotationCtrlButton from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button' import NewAudioButton from '@/app/components/base/new-audio-button' +import { toast } from '@/app/notifications' import { useChatContext } from '../context' type OperationProps = { diff --git a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx index dcb894fca7b..1df89f887e3 100644 --- a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx @@ -122,7 +122,7 @@ vi.mock('@/app/components/base/features/hooks', () => ({ // --------------------------------------------------------------------------- // Toast context // --------------------------------------------------------------------------- -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 3d10ce26157..e276b293528 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -5,7 +5,6 @@ import type { InputForm } from '../type' import type { FileUpload } from '@/app/components/base/features/types' import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { decode } from 'html-entities' import { useCallback, useRef, useState } from 'react' @@ -17,6 +16,7 @@ import { useFile } from '@/app/components/base/file-uploader/hooks' import { FileContextProvider, useFileStore } from '@/app/components/base/file-uploader/store' import { Infotip } from '@/app/components/base/infotip' import VoiceInput from '@/app/components/base/voice-input' +import { toast } from '@/app/notifications' import { TransferMethod } from '@/types/app' import { useCheckInputsForms } from '../check-input-forms-hooks' import { useTextAreaHeight } from './hooks' diff --git a/web/app/components/base/chat/chat/check-input-forms-hooks.ts b/web/app/components/base/chat/chat/check-input-forms-hooks.ts index 1230b15ccd6..3237a0d804f 100644 --- a/web/app/components/base/chat/chat/check-input-forms-hooks.ts +++ b/web/app/components/base/chat/chat/check-input-forms-hooks.ts @@ -1,8 +1,8 @@ import type { InputForm } from './type' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { TransferMethod } from '@/types/app' export const useCheckInputsForms = () => { diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index c35af2fb373..5bd16f0b853 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -6,7 +6,6 @@ import type { Annotation } from '@/models/log' import type { IOnDataMoreInfo, IOtherOptions } from '@/service/base' import type { VisionFile } from '@/types/app' import type { FileResponse, ReasoningChunkResponse } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { uniqBy } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' import { produce, setAutoFreeze } from 'immer' @@ -22,6 +21,7 @@ import { import { isInstalledAppPath } from '@/app/components/explore/installed-app/routes' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' import { useParams, usePathname } from '@/next/navigation' import { sseGet, ssePost } from '@/service/base' diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx index 254e0accb0a..41f68b1ab29 100644 --- a/web/app/components/base/chat/chat/question.tsx +++ b/web/app/components/base/chat/chat/question.tsx @@ -4,7 +4,6 @@ import type { ChatItem } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import copy from 'copy-to-clipboard' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +11,7 @@ import Textarea from 'react-textarea-autosize' import { FileList } from '@/app/components/base/file-uploader' import { User } from '@/app/components/base/icons/src/public/avatar' import { Markdown } from '@/app/components/base/markdown' +import { toast } from '@/app/notifications' import { CssTransform } from '../embedded-chatbot/theme/utils' import ContentSwitch from './content-switch' import { useChatContext } from './context' diff --git a/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx b/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx index 32fe04d931a..6508db8e4d8 100644 --- a/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/embedded-chatbot/__tests__/hooks.spec.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react' import type { ChatConfig } from '../../types' import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share' -import { ToastHost } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook, waitFor } from '@testing-library/react' import { InputVarType } from '@/app/components/workflow/types' +import { AppToastHost } from '@/app/notifications/host' import { AppSourceType, fetchChatList, @@ -111,7 +111,7 @@ const createQueryClient = () => const createWrapper = (queryClient: QueryClient) => { return ({ children }: { children: ReactNode }) => ( - + {children} ) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 9be176dc322..4cb7a2cd050 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -2,7 +2,6 @@ import type { ChatConfig, ChatItem, OnFeedback } from '../types' import type { InputValueTypes } from '@/app/components/share/text-generation/types' import type { Locale } from '@/i18n' import type { AppData, ConversationItem } from '@/models/share' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -10,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { useConversationSelection } from '@/app/components/base/chat/storage' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { InputVarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useWebAppStore } from '@/context/web-app-context' import { changeLanguage } from '@/i18n/client' import { AppSourceType, updateFeedback } from '@/service/share' diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx index a1317158c63..70a9b2eb0ec 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/content.spec.tsx @@ -15,7 +15,7 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => new URLSearchParams(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({})) +vi.mock('@/app/notifications', () => ({})) // Mock CodeEditor to trigger onChange easily vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx index 36a9039e4d6..b4387ed2087 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx @@ -21,7 +21,7 @@ vi.mock('@/service/annotation', () => ({ addAnnotation: (...args: unknown[]) => mockAddAnnotation(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn() }, toast: { success: vi.fn(), diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx index 71796e9f1c8..9e4da19f767 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx @@ -1,5 +1,5 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { toast } from '@/app/notifications' import ConfigParamModal from '../config-param-modal' let mockHooksReturn: { diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index a1a09a71167..80dce9ca088 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -1,13 +1,13 @@ 'use client' import type { FC } from 'react' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditLine, RiFileEditLine } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useModalContext } from '@/context/modal-context' import { deploymentEditionAtom } from '@/features/system-features/state' import { addAnnotation } from '@/service/annotation' diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index fb48515d053..b7d13a8a29c 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -3,13 +3,13 @@ import type { FC } from 'react' import type { AnnotationReplyConfig } from '@/models/debug' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector' +import { toast } from '@/app/notifications' import { ANNOTATION_DEFAULT } from '@/config' import { Item } from './config-param' import ScoreSlider from './score-slider' diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx index 392c79e689f..a41ce22b909 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx @@ -7,7 +7,7 @@ import { withSelectorKey } from '@/test/i18n-mock' import ModerationSettingModal from '../moderation-setting-modal' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockNotify({ type: 'error', message }), }, diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index 148b1a56d34..67d50a360b2 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -5,7 +5,6 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryState } from 'nuqs' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -17,6 +16,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { LanguagesSupported } from '@/i18n/language' import { useCodeBasedExtensions, useModelProviderDetails } from '@/service/use-common' diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/voice-settings.spec.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/voice-settings.spec.tsx index 781f95af9b4..c7d288740a7 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/voice-settings.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/voice-settings.spec.tsx @@ -3,7 +3,7 @@ import type { Features } from '../../../types' import { fireEvent, render, screen } from '@testing-library/react' import { FeaturesProvider } from '../../../context' import VoiceSettings from '../voice-settings' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/base/file-uploader/__tests__/hooks.spec.ts b/web/app/components/base/file-uploader/__tests__/hooks.spec.ts index 3ba3ce25395..7272a52090a 100644 --- a/web/app/components/base/file-uploader/__tests__/hooks.spec.ts +++ b/web/app/components/base/file-uploader/__tests__/hooks.spec.ts @@ -23,7 +23,7 @@ vi.mock('../upload-context', () => ({ useFileUploadContext: () => mockFileUploadContext, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockNotify({ type: 'error', message }), }, diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx index 626a175e448..0a4e924be01 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.stories.tsx @@ -1,11 +1,11 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FileEntity } from '../types' import type { FileUpload } from '@/app/components/base/features/types' -import { ToastHost } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { fn } from 'storybook/test' import { PreviewMode } from '@/app/components/base/features/types' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { AppToastHost } from '@/app/notifications/host' import { TransferMethod } from '@/types/app' import FileUploaderInAttachmentWrapper from './index' @@ -86,7 +86,7 @@ const AttachmentDemo = (props: React.ComponentProps - +
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx index 133850ea895..b4b399df005 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { FileEntity } from '../types' import type { FileUpload } from '@/app/components/base/features/types' -import { ToastHost } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { AppToastHost } from '@/app/notifications/host' import { TransferMethod } from '@/types/app' import FileUploaderInChatInput from '.' import { FileList } from '../file-uploader-in-chat-input/file-list' @@ -37,7 +37,7 @@ const ChatInputDemo = ({ initialFiles = mockFiles, ...props }: ChatInputDemoProp return ( <> - +
Simulated chat input
diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index 1e62e59ab3b..da92adbdd09 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -2,7 +2,6 @@ import type { ClipboardEvent } from 'react' import type { FileEntity } from './types' import type { FileUpload } from '@/app/components/base/features/types' import type { FileUploadConfigResponse } from '@/models/common' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useState } from 'react' @@ -17,6 +16,7 @@ import { VIDEO_SIZE_LIMIT, } from '@/app/components/base/file-uploader/constants' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useParams, usePathname } from '@/next/navigation' import { uploadRemoteFileInfo } from '@/service/common' import { uploadHumanInputFormLocalFile, uploadHumanInputFormRemoteFileInfo } from '@/service/share' diff --git a/web/app/components/base/form/components/base/__tests__/validation-accessibility.spec.tsx b/web/app/components/base/form/components/base/__tests__/validation-accessibility.spec.tsx index 175889b5d38..5a54a364b65 100644 --- a/web/app/components/base/form/components/base/__tests__/validation-accessibility.spec.tsx +++ b/web/app/components/base/form/components/base/__tests__/validation-accessibility.spec.tsx @@ -13,7 +13,7 @@ vi.mock('@/service/use-triggers', () => ({ useTriggerPluginDynamicOptions: () => ({ data: undefined, isLoading: false, error: null }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: { error: vi.fn() } })) +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() } })) const requiredSchema = (label: string): FormSchema => ({ name: 'token', diff --git a/web/app/components/base/form/hooks/__tests__/use-check-validated.spec.ts b/web/app/components/base/form/hooks/__tests__/use-check-validated.spec.ts index 96f7aa0a83b..d7494bb69cd 100644 --- a/web/app/components/base/form/hooks/__tests__/use-check-validated.spec.ts +++ b/web/app/components/base/form/hooks/__tests__/use-check-validated.spec.ts @@ -5,7 +5,7 @@ import { useCheckValidated } from '../use-check-validated' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/base/form/hooks/use-check-validated.ts b/web/app/components/base/form/hooks/use-check-validated.ts index d7ca22616b0..9dbdaca8016 100644 --- a/web/app/components/base/form/hooks/use-check-validated.ts +++ b/web/app/components/base/form/hooks/use-check-validated.ts @@ -1,7 +1,7 @@ import type { AnyFormApi } from '@tanstack/react-form' import type { FormSchema } from '@/app/components/base/form/types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' +import { toast } from '@/app/notifications' export const useCheckValidated = ( form: AnyFormApi, diff --git a/web/app/components/base/image-uploader/__tests__/hooks.spec.ts b/web/app/components/base/image-uploader/__tests__/hooks.spec.ts index 586ffc93357..e59dce6cc7d 100644 --- a/web/app/components/base/image-uploader/__tests__/hooks.spec.ts +++ b/web/app/components/base/image-uploader/__tests__/hooks.spec.ts @@ -4,7 +4,7 @@ import { TransferMethod } from '@/types/app' import { useImageFiles, useLocalFileUploader } from '../hooks' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockNotify({ type: 'error', message }), }, diff --git a/web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx b/web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx index 66a7cf69f8a..81e9cbe0b5a 100644 --- a/web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx +++ b/web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx @@ -11,7 +11,7 @@ const mocks = vi.hoisted(() => ({ clipboardWrite: vi.fn<(items: ClipboardItem[]) => Promise>(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (...args: Parameters) => mocks.notify(...args), }, diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 0f363f8b1dd..e8d12b193b8 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -1,7 +1,7 @@ import type { ImageFile } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useParams } from '@/next/navigation' import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' import { getImageUploadErrorMessage, imageUpload } from './utils' diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx index 97328610afa..69db31c78cc 100644 --- a/web/app/components/base/image-uploader/image-preview.tsx +++ b/web/app/components/base/image-uploader/image-preview.tsx @@ -1,12 +1,12 @@ import type { FC } from 'react' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useHotkey } from '@tanstack/react-hotkeys' import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { downloadUrl } from '@/utils/download' type ImagePreviewProps = { diff --git a/web/app/components/base/tag-input/__tests__/index.spec.tsx b/web/app/components/base/tag-input/__tests__/index.spec.tsx index 983c4ac4df6..8eddf775564 100644 --- a/web/app/components/base/tag-input/__tests__/index.spec.tsx +++ b/web/app/components/base/tag-input/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import TagInput from '../index' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index f40b8e7c418..3250c00acc5 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -1,8 +1,8 @@ import type { ChangeEvent, InputHTMLAttributes, KeyboardEvent } from 'react' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' type TagInputProps = Pick< InputHTMLAttributes, diff --git a/web/app/components/base/text-generation/__tests__/hooks.spec.ts b/web/app/components/base/text-generation/__tests__/hooks.spec.ts index 74a57b32380..6f626df6202 100644 --- a/web/app/components/base/text-generation/__tests__/hooks.spec.ts +++ b/web/app/components/base/text-generation/__tests__/hooks.spec.ts @@ -12,7 +12,7 @@ const mockSsePost = ) => void >() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/base/text-generation/hooks.ts b/web/app/components/base/text-generation/hooks.ts index 08d8c1ea394..b0a6738e655 100644 --- a/web/app/components/base/text-generation/hooks.ts +++ b/web/app/components/base/text-generation/hooks.ts @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { ssePost } from '@/service/base' type SendOptions = { diff --git a/web/app/components/billing/hooks/use-education-discount.ts b/web/app/components/billing/hooks/use-education-discount.ts index 10dbd209ec8..a2ccd0ca101 100644 --- a/web/app/components/billing/hooks/use-education-discount.ts +++ b/web/app/components/billing/hooks/use-education-discount.ts @@ -1,8 +1,8 @@ 'use client' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { consoleClient } from '@/service/console' diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index b0590c7ce5b..2468ed745d2 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -10,10 +10,10 @@ import { DialogTitle, } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { consoleClient } from '@/service/console' diff --git a/web/app/components/custom/custom-page/__tests__/index.spec.tsx b/web/app/components/custom/custom-page/__tests__/index.spec.tsx index b68a53937cf..ac3fc103f0a 100644 --- a/web/app/components/custom/custom-page/__tests__/index.spec.tsx +++ b/web/app/components/custom/custom-page/__tests__/index.spec.tsx @@ -62,7 +62,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx index a8eb5060e94..ef6a6bc4577 100644 --- a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx +++ b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx @@ -54,7 +54,7 @@ const consoleStateRef = vi.hoisted(() => ({ })) const mockUpdateCustomConfig = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) vi.mock('@/service/console', async (importOriginal) => { diff --git a/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts b/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts index 7dd76072233..65a52000335 100644 --- a/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts +++ b/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts @@ -1,11 +1,11 @@ import type { WorkspaceCustomConfigPayload } from '@dify/contracts/api/console/workspaces/types.gen' import type { ChangeEvent } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx index 619dc6a5922..23d795c26fa 100644 --- a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx +++ b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx @@ -1,8 +1,8 @@ -import { toast } from '@langgenius/dify-ui/toast' import { queryOptions, useQuery } from '@tanstack/react-query' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { useAutoDisabledDocuments } from '@/service/knowledge/use-document' import { createConsoleQueryWrapper } from '@/test/console/query-data' import AutoDisabledDocument from '../auto-disabled-document' @@ -33,7 +33,7 @@ vi.mock('@/service/knowledge/use-document', () => ({ useInvalidDisabledDocument: vi.fn(() => mockInvalidDisabledDocument), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, }, diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx index ee034e8810e..7f91928582e 100644 --- a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useAutoDisabledDocuments, useDocumentEnable, diff --git a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx index d2d92d0e190..2abf79ace6b 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx +++ b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx @@ -1,9 +1,9 @@ import type { PropsWithChildren } from 'react' import type { FileEntity } from '../../types' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { FileContextProvider } from '../../store' import { useUpload } from '../use-upload' @@ -21,7 +21,7 @@ vi.mock('@/service/use-common', () => ({ })), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts index 9969e28874b..30a2295ebcf 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts +++ b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts @@ -1,10 +1,10 @@ import type { FileEntity, FileUploadConfig } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { v4 as uuid4 } from 'uuid' import { fileUpload, getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' +import { toast } from '@/app/notifications' import { useFileUploadConfig } from '@/service/use-common' import { ACCEPT_TYPES } from '../constants' import { useFileStore } from '../store' diff --git a/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx b/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx index a191c70bcc8..a4fc6898a97 100644 --- a/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import { RETRIEVE_METHOD } from '@/types/app' import RetrievalParamConfig from '../index' const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockNotify(message), }, diff --git a/web/app/components/datasets/common/retrieval-param-config/index.tsx b/web/app/components/datasets/common/retrieval-param-config/index.tsx index aa48a542549..12095534151 100644 --- a/web/app/components/datasets/common/retrieval-param-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/index.tsx @@ -4,7 +4,6 @@ import type { RetrievalConfig } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' import { RadioGroup } from '@langgenius/dify-ui/radio-group' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -20,6 +19,7 @@ import { useModelListAndDefaultModel, } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector' +import { toast } from '@/app/notifications' import { DEFAULT_WEIGHTED_SCORE, RerankingModeEnum, WeightedScoreEnum } from '@/models/datasets' import { RETRIEVE_METHOD } from '@/types/app' import ProgressIndicator from '../../create/assets/progress-indicator.svg' diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx index 6a8344b5f9f..83574c22c92 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx @@ -60,7 +60,7 @@ const toastMocks = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/uploader.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/uploader.spec.tsx index d38576ebecf..d9b4a80249b 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/uploader.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/uploader.spec.tsx @@ -15,7 +15,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx index 40163a3c226..c2fdfac60bb 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx @@ -59,7 +59,7 @@ const toastMocks = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts index 6228668fabe..4737b6192fb 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts @@ -1,9 +1,9 @@ 'use client' -import { toast } from '@langgenius/dify-ui/toast' import { useDebounceFn } from 'ahooks' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline' diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx index 97c601ba43d..e68b40b68bb 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { RiDeleteBinLine, RiNodeTree, RiUploadCloud2Line } from '@remixicon/react' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { formatFileSize } from '@/utils/format' type Props = Readonly<{ diff --git a/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx index badb3274000..3ee202ab480 100644 --- a/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx @@ -17,8 +17,8 @@ const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/create-from-pipeline/list/create-card.tsx b/web/app/components/datasets/create-from-pipeline/list/create-card.tsx index 5daf9a51b95..c9ed34224d0 100644 --- a/web/app/components/datasets/create-from-pipeline/list/create-card.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/create-card.tsx @@ -1,9 +1,9 @@ -import { toast } from '@langgenius/dify-ui/toast' import { RiAddCircleLine } from '@remixicon/react' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index 24c28a0071b..52527e64d48 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -29,8 +29,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx index 09540e47775..631d1e49b6f 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx @@ -20,8 +20,8 @@ const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx index 9515d4ea854..6fa341c2f8d 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx @@ -6,12 +6,12 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' +import { toast } from '@/app/notifications' import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline' type EditPipelineInfoProps = { diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx index 39bfc89fe6a..5bc3f8a5a43 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx @@ -9,12 +9,12 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx b/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx index 884fc5b9446..ffc7cbf5dfc 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx @@ -19,7 +19,7 @@ const { mockNotify, mockToast } = vi.hoisted(() => { return { mockNotify, mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx index c725e3a7d17..1280dfb6df6 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx @@ -5,11 +5,11 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { createEmptyDataset } from '@/service/datasets' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' diff --git a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx index 5586bb01ea4..aebf6f625b9 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx @@ -10,7 +10,7 @@ import { useFileUpload } from '../use-file-upload' // Mock notify function const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockNotify({ type: 'error', message }), }, diff --git a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts index 2606889e901..5ff3a677a91 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts +++ b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts @@ -1,12 +1,12 @@ 'use client' import type { RefObject } from 'react' import type { CustomFile as File, FileItem } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { LanguagesSupported } from '@/i18n/language' import { upload } from '@/service/base' diff --git a/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts b/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts index 04f1c2ebd5b..f2065bc673a 100644 --- a/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts +++ b/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts @@ -19,7 +19,7 @@ const mocks = vi.hoisted(() => ({ invalidDatasetList: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: mocks.toastNotify }, toast: { success: (message: string) => mocks.toastNotify({ type: 'success', message }), diff --git a/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts b/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts index 7c7b38af414..54734679c11 100644 --- a/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts +++ b/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts @@ -13,11 +13,11 @@ import type { SummaryIndexSetting as SummaryIndexSettingType, } from '@/models/datasets' import type { RetrievalConfig, RETRIEVE_METHOD } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' +import { toast } from '@/app/notifications' import { DataSourceProvider } from '@/models/common' import { DataSourceType } from '@/models/datasets' import { diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 5ed0e40578b..f574b2f291d 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -3,11 +3,11 @@ import type { FC } from 'react' import type { StepTwoProps } from './types' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Divider from '@/app/components/base/divider' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { LanguagesSupported } from '@/i18n/language' diff --git a/web/app/components/datasets/create/website/firecrawl/index.tsx b/web/app/components/datasets/create/website/firecrawl/index.tsx index 211bcfb5854..5204e9a3d56 100644 --- a/web/app/components/datasets/create/website/firecrawl/index.tsx +++ b/web/app/components/datasets/create/website/firecrawl/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryState } from 'nuqs' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' @@ -10,6 +9,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { checkFirecrawlTaskStatus, createFirecrawlTask } from '@/service/datasets' import { sleep } from '@/utils' import CrawledResult from '../base/crawled-result' diff --git a/web/app/components/datasets/create/website/jina-reader/index.tsx b/web/app/components/datasets/create/website/jina-reader/index.tsx index 3f85f5c69d2..6998baaea35 100644 --- a/web/app/components/datasets/create/website/jina-reader/index.tsx +++ b/web/app/components/datasets/create/website/jina-reader/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryState } from 'nuqs' import * as React from 'react' import { useCallback, useState } from 'react' @@ -10,6 +9,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets' import { sleep } from '@/utils' import CrawledResult from '../base/crawled-result' diff --git a/web/app/components/datasets/create/website/watercrawl/index.tsx b/web/app/components/datasets/create/website/watercrawl/index.tsx index cff8845bf0d..09f0c0cfd3d 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryState } from 'nuqs' import * as React from 'react' import { useCallback, useState } from 'react' @@ -10,6 +9,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datasets' import { sleep } from '@/utils' import CrawledResult from '../base/crawled-result' diff --git a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx index c1abe441b1d..da2b79c295f 100644 --- a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx @@ -24,7 +24,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx index f6fe6688041..228d71f9129 100644 --- a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx @@ -15,7 +15,7 @@ vi.mock('@/service/datasets', () => ({ renameDocumentName: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts index a31216c1144..97052acdf35 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts @@ -27,7 +27,7 @@ vi.mock('@/service/knowledge/use-document', () => ({ useDocumentDownloadZip: () => ({ mutateAsync: mockDownloadZip, isPending: mockIsDownloadingZip }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts index cf48ee5bc80..8dead17f351 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts @@ -1,7 +1,7 @@ import type { CommonResponse } from '@/models/common' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { DocumentActionType } from '@/models/datasets' import { useDocumentArchive, diff --git a/web/app/components/datasets/documents/components/operations.tsx b/web/app/components/datasets/documents/components/operations.tsx index 4d0280721cd..2a29c64db3f 100644 --- a/web/app/components/datasets/documents/components/operations.tsx +++ b/web/app/components/datasets/documents/components/operations.tsx @@ -18,7 +18,6 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean, useDebounceFn } from 'ahooks' import { noop } from 'es-toolkit/function' @@ -26,6 +25,7 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { DataSourceType, DocumentActionType } from '@/models/datasets' import { useRouter } from '@/next/navigation' diff --git a/web/app/components/datasets/documents/components/rename-modal.tsx b/web/app/components/datasets/documents/components/rename-modal.tsx index 1875e23f385..0e783bf43f2 100644 --- a/web/app/components/datasets/documents/components/rename-modal.tsx +++ b/web/app/components/datasets/documents/components/rename-modal.tsx @@ -5,11 +5,11 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { renameDocumentName } from '@/service/datasets' type Props = Readonly<{ diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx index d6e9047723f..b098f25b348 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx @@ -19,7 +19,7 @@ const { mockNotify, mockToast } = vi.hoisted(() => { return { mockNotify, mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx index 7d7d40c8345..f0636b4b838 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx @@ -38,8 +38,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx index 76e1e62c4d0..937bfcfd918 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx @@ -1,7 +1,6 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common' import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' @@ -11,6 +10,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { DatasourceType } from '@/models/pipeline' diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx index aa2b6cef6e3..38f8efbad42 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx @@ -61,8 +61,8 @@ vi.mock('@/service/use-datasource', () => ({ useGetDataSourceAuth: (...args: unknown[]) => mocks.useGetDataSourceAuth(...args), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx index 12c0306c63d..9652ba65e41 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx @@ -1,7 +1,6 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import type { OnlineDriveFile } from '@/models/pipeline' import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -10,6 +9,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline' diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/__tests__/index.spec.tsx index 7c44569960b..f77c4be751c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/__tests__/index.spec.tsx @@ -11,8 +11,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx index 2866e4f2533..852fdb95752 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx @@ -1,7 +1,6 @@ import type { RAGPipelineVariables } from '@/models/pipeline' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiPlayLargeLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useEffect, useId, useMemo } from 'react' @@ -14,6 +13,7 @@ import { useConfigurations, useInitialData, } from '@/app/components/rag-pipeline/hooks/use-input-fields' +import { toast } from '@/app/notifications' import { CrawlStep } from '@/models/datasets' const I18N_PREFIX = 'stepOne.website' diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx index abbefcd97aa..821f3744b54 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx @@ -9,8 +9,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx index 00a649bd5ac..57f42d6e832 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx @@ -1,12 +1,12 @@ 'use client' import type { NotionPage } from '@/models/common' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Notion } from '@/app/components/base/icons/src/public/common' import { Markdown } from '@/app/components/base/markdown' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { usePreviewOnlineDocument } from '@/service/use-pipeline' import { formatNumberAbbreviated } from '@/utils/format' diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/components.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/components.spec.tsx index c0783b294cf..ff05fda5eee 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/components.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/components.spec.tsx @@ -11,8 +11,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/form.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/form.spec.tsx index bf5a749ba96..8e2e3198161 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/form.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/form.spec.tsx @@ -8,8 +8,8 @@ const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx b/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx index 4a29e4fd7cb..c89f71eab0d 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx @@ -1,9 +1,9 @@ import type { ZodSchema } from 'zod' import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useImperativeHandle } from 'react' import { useAppForm } from '@/app/components/base/form' import BaseField from '@/app/components/base/form/form-scenarios/base/field' +import { toast } from '@/app/notifications' import Header from './header' type OptionsProps = { diff --git a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx index cb933dc1a92..3a6d3d2e7c7 100644 --- a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx @@ -89,7 +89,7 @@ vi.mock('@/service/use-base', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: mocks.toastNotify }, })) diff --git a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx index ecbfb1f0fee..a39adfedf6d 100644 --- a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx @@ -1,6 +1,7 @@ -import { toast, ToastHost } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' +import { AppToastHost } from '@/app/notifications/host' import { ChunkingMode } from '@/models/datasets' import { IndexingType } from '../../../create/step-two' import NewSegmentModal from '../new-segment' @@ -459,7 +460,7 @@ describe('NewSegmentModal', () => { render( <> - + { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx index 7bf1952a98c..8d3d89206e5 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { FileItem } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiDeleteBinLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -11,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' import SimplePieChart from '@/app/components/base/simple-pie-chart' +import { toast } from '@/app/notifications' import useTheme from '@/hooks/use-theme' import { upload } from '@/service/base' import { useFileUploadConfig } from '@/service/use-common' diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx index 0d6055f7838..6a8e2f24970 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx @@ -78,7 +78,7 @@ vi.mock('../../context', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx index 3ed01d9a9ca..4b476364495 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx @@ -1,6 +1,7 @@ -import { toast, ToastHost } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' +import { AppToastHost } from '@/app/notifications/host' import NewChildSegmentModal from '../new-child-segment' vi.mock('@/next/navigation', () => ({ @@ -385,7 +386,7 @@ describe('NewChildSegmentModal', () => { render( <> - + ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts index caa917c8a97..bee656a85bb 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts @@ -107,7 +107,7 @@ vi.mock('../../../context', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts index 8ed8a687fac..b8f5759fec1 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts @@ -4,10 +4,10 @@ import type { SegmentDetailModel, SegmentUpdater, } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useChildSegmentList, diff --git a/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts b/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts index f61e7a64d93..9f11a0df03c 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts @@ -1,10 +1,10 @@ import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { SegmentDetailModel, SegmentsResponse, SegmentUpdater } from '@/models/datasets' import type { SegmentImportStatus } from '@/types/dataset' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { ChunkingMode } from '@/models/datasets' import { usePathname } from '@/next/navigation' diff --git a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx index c7ba7e3e318..ccf728955bb 100644 --- a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx +++ b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' import type { ChildChunkDetail, SegmentUpdater } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' import { memo, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' +import { toast } from '@/app/notifications' import { ChunkingMode } from '@/models/datasets' import { useParams } from '@/next/navigation' import { useAddChildSegment } from '@/service/knowledge/use-segment' diff --git a/web/app/components/datasets/documents/detail/embedding/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/embedding/__tests__/index.spec.tsx index 7397d49bb4d..77c25e20dd8 100644 --- a/web/app/components/datasets/documents/detail/embedding/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/embedding/__tests__/index.spec.tsx @@ -28,7 +28,7 @@ const { mockToast } = vi.hoisted(() => { vi.mock('@/service/datasets') vi.mock('@/service/knowledge/use-dataset') -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/documents/detail/embedding/index.tsx b/web/app/components/datasets/documents/detail/embedding/index.tsx index a36ec10960e..aba1995795f 100644 --- a/web/app/components/datasets/documents/detail/embedding/index.tsx +++ b/web/app/components/datasets/documents/detail/embedding/index.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react' import type { IndexingType } from '../../../create/step-two' import type { RETRIEVE_METHOD } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useProcessRule } from '@/service/knowledge/use-dataset' import { useDocumentContext } from '../context' import { ProgressBar, RuleDetail, SegmentProgress, StatusHeader } from './components' diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index c6ca1e714bd..4b03ff319b5 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -5,7 +5,6 @@ import type { DocumentDisplayStatus, FileItem, FullDocumentDetail } from '@/mode import type { SegmentImportStatus } from '@/types/dataset' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' @@ -15,6 +14,7 @@ import Divider from '@/app/components/base/divider' import FloatRightContainer from '@/app/components/base/float-right-container' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import Metadata from '@/app/components/datasets/metadata/metadata-document' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 392b888a48a..5c27f970927 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -2,12 +2,12 @@ import type { FC } from 'react' import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { SegmentUpdater } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' import { memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import ImageUploaderInChunk from '@/app/components/datasets/common/image-uploader/image-uploader-in-chunk' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { ChunkingMode } from '@/models/datasets' import { useParams } from '@/next/navigation' diff --git a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx index f0d72b55ac4..a955140f274 100644 --- a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx @@ -37,7 +37,7 @@ vi.mock('use-context-selector', () => ({ useContextSelector: (context: unknown, selector: (state: unknown) => unknown) => selector({}), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/datasets/documents/status-item/index.tsx b/web/app/components/datasets/documents/status-item/index.tsx index 15215060be3..21eeceb58b1 100644 --- a/web/app/components/datasets/documents/status-item/index.tsx +++ b/web/app/components/datasets/documents/status-item/index.tsx @@ -5,13 +5,13 @@ import type { DocumentDisplayStatus } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useDebounceFn } from 'ahooks' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' +import { toast } from '@/app/notifications' import { useDocumentDelete, useDocumentDisable, diff --git a/web/app/components/datasets/external-api/external-api-modal/__tests__/index.spec.tsx b/web/app/components/datasets/external-api/external-api-modal/__tests__/index.spec.tsx index fa1a78e7b22..a2ee9f93dba 100644 --- a/web/app/components/datasets/external-api/external-api-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/__tests__/index.spec.tsx @@ -13,7 +13,7 @@ vi.mock('@/service/datasets', () => ({ })) const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockNotify(args), }, diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index 2b1e39a7075..7739406f24f 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -13,10 +13,10 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { RiBook2Line, RiCloseLine, RiInformation2Line, RiLock2Fill } from '@remixicon/react' import { memo, useId, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { createExternalAPI } from '@/service/datasets' import Form from './Form' diff --git a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx index 972214965cc..74366f7e285 100644 --- a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx @@ -27,8 +27,8 @@ vi.mock('@/context/i18n', () => ({ const mockToastSuccess = vi.hoisted(() => vi.fn()) const mockToastError = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/datasets/external-knowledge-base/connector/index.tsx b/web/app/components/datasets/external-knowledge-base/connector/index.tsx index f8194ff84d7..fedce465e3f 100644 --- a/web/app/components/datasets/external-knowledge-base/connector/index.tsx +++ b/web/app/components/datasets/external-knowledge-base/connector/index.tsx @@ -1,12 +1,12 @@ 'use client' import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' import { createExternalKnowledgeBase } from '@/service/datasets' diff --git a/web/app/components/datasets/hit-testing/__tests__/modify-retrieval-modal.spec.tsx b/web/app/components/datasets/hit-testing/__tests__/modify-retrieval-modal.spec.tsx index 4a9582db78b..d214187b9bc 100644 --- a/web/app/components/datasets/hit-testing/__tests__/modify-retrieval-modal.spec.tsx +++ b/web/app/components/datasets/hit-testing/__tests__/modify-retrieval-modal.spec.tsx @@ -20,7 +20,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx index dce7f4d080e..0a1a63ca871 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { IndexingType } from '../create/step-two' import type { RetrievalConfig } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import * as React from 'react' @@ -12,6 +11,7 @@ import { useTranslation } from 'react-i18next' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config' import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx index 0e5de21a2c7..5199b4eff3a 100644 --- a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx @@ -51,7 +51,7 @@ const toastMocks = vi.hoisted(() => { return { record, api } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts index a2431ef9931..d807c486711 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts @@ -1,8 +1,8 @@ import type { DataSet } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { useExportPipelineDSL } from '@/service/use-pipeline' diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 695c9e9c80b..97b156aefbe 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -3,11 +3,11 @@ import type { KeyboardEvent, MouseEvent } from 'react' import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags' diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx index 0702456d07b..b848e9e69c5 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx @@ -31,7 +31,7 @@ vi.mock('../../hooks/use-check-metadata-name', () => ({ // Mock Toast to verify notifications const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockToastNotify(args), }, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx index d84bb5763f4..7d7c5c459ea 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx @@ -5,12 +5,12 @@ import { Button } from '@langgenius/dify-ui/button' import { Checkbox } from '@langgenius/dify-ui/checkbox' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' +import { toast } from '@/app/notifications' import { useCreateMetaData } from '@/service/knowledge/use-metadata' import { Infotip } from '../../../base/infotip' import useCheckMetadataName from '../hooks/use-check-metadata-name' diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts index b96d12547ce..540d7a5c68f 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts @@ -34,7 +34,7 @@ vi.mock('@/service/knowledge/use-metadata', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts index 4f704dff420..dea5dc81291 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts @@ -40,7 +40,7 @@ vi.mock('@/service/knowledge/use-metadata', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn(), }, diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts index 19f05e8218d..9d1ae745959 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts @@ -81,7 +81,7 @@ vi.mock('@/hooks/use-metadata', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn(), }, diff --git a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts index ae76e5933eb..d08b4d75fef 100644 --- a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts +++ b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts @@ -5,10 +5,10 @@ import type { MetadataItemWithValue, } from '../types' import type { SimpleDocumentDetail } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import { t } from 'i18next' import { useMemo } from 'react' +import { toast } from '@/app/notifications' import { useBatchUpdateDocMetadata } from '@/service/knowledge/use-metadata' import { UpdateType } from '../types' diff --git a/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts b/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts index 619eacfa98b..def012190d4 100644 --- a/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts +++ b/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts @@ -1,9 +1,9 @@ import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types' import type { DataSet } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useBuiltInMetaDataFields, useCreateMetaData, diff --git a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts index 42b84eb6231..f8f2463bb2e 100644 --- a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts +++ b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts @@ -1,9 +1,9 @@ import type { BuiltInMetadataItem, MetadataItemWithValue } from '../types' import type { FullDocumentDetail } from '@/models/datasets' -import { toast } from '@langgenius/dify-ui/toast' import { get } from 'es-toolkit/compat' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDatasetDetailContext } from '@/context/dataset-detail' import { useLanguages, useMetadataMap } from '@/hooks/use-metadata' import { diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx index 0f12c67b2e0..abb9d2025ff 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx @@ -21,7 +21,7 @@ vi.mock('../../hooks/use-check-metadata-name', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => mockToastNotify(args), }, diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx index e0d19bebb9a..4098b01dc05 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx @@ -25,7 +25,6 @@ import { } from '@langgenius/dify-ui/drawer' import { Input } from '@langgenius/dify-ui/input' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react' import { useBoolean, useHover } from 'ahooks' import * as React from 'react' @@ -33,6 +32,7 @@ import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' import { CreateMetadataModal } from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal' +import { toast } from '@/app/notifications' import { getIconClassName } from '../utils/get-icon' import Field from './field' diff --git a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx index 52ed523e6b8..98773598bbc 100644 --- a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx @@ -19,7 +19,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx index 9f82f925385..47d9190f31b 100644 --- a/web/app/components/datasets/rename-modal/index.tsx +++ b/web/app/components/datasets/rename-modal/index.tsx @@ -7,9 +7,9 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { updateDatasetSetting } from '@/service/datasets' import AppIcon from '../../base/app-icon' import AppIconPicker from '../../base/app-icon-picker' diff --git a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx index 461e1eb7255..a8f6e484d02 100644 --- a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx @@ -208,7 +208,7 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: () => true, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, success: vi.fn(), @@ -402,7 +402,7 @@ describe('Form', () => { }) it('should show error when trying to save with empty name', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') + const { toast } = await import('@/app/notifications') render() // Clear the name diff --git a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts index 736e8ec15d9..53ccd46eeaf 100644 --- a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts +++ b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts @@ -182,7 +182,7 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: () => true, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, @@ -490,7 +490,7 @@ describe('useFormState', () => { describe('handleSave', () => { it('should show error toast when name is empty', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') + const { toast } = await import('@/app/notifications') const { result } = renderHook(() => useFormState()) act(() => { @@ -505,7 +505,7 @@ describe('useFormState', () => { }) it('should show error toast when name is whitespace only', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') + const { toast } = await import('@/app/notifications') const { result } = renderHook(() => useFormState()) act(() => { @@ -553,7 +553,7 @@ describe('useFormState', () => { }) it('should show success toast on successful save', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') + const { toast } = await import('@/app/notifications') const { result } = renderHook(() => useFormState()) await act(async () => { @@ -628,7 +628,7 @@ describe('useFormState', () => { it('should show error toast on save failure', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { toast } = await import('@langgenius/dify-ui/toast') + const { toast } = await import('@/app/notifications') vi.mocked(updateDatasetSetting).mockRejectedValueOnce(new Error('Network error')) const { result } = renderHook(() => useFormState()) diff --git a/web/app/components/datasets/settings/form/hooks/use-form-state.ts b/web/app/components/datasets/settings/form/hooks/use-form-state.ts index 3fa3ac4ca36..354487d14e7 100644 --- a/web/app/components/datasets/settings/form/hooks/use-form-state.ts +++ b/web/app/components/datasets/settings/form/hooks/use-form-state.ts @@ -4,13 +4,13 @@ import type { DefaultModel } from '@/app/components/header/account-setting/model import type { Member } from '@/models/common' import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index d22ba097ec4..39b1a1c6192 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -8,7 +8,6 @@ import { Input } from '@langgenius/dify-ui/input' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Switch } from '@langgenius/dify-ui/switch' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' @@ -18,6 +17,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppsFull from '@/app/components/billing/apps-full-in-dialog' +import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx index d67c1645688..7aeb3adfc36 100644 --- a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx @@ -3,10 +3,10 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render as renderWithoutPricing, screen, waitFor } from '@testing-library/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' +import { toast } from '@/app/notifications' import { getDocDownloadUrl } from '@/service/common' import { seedFeatures } from '@/test/console/query-data' import { downloadUrl } from '@/utils/download' diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx index 0d458f733e9..7e8ebdc5ce4 100644 --- a/web/app/components/header/account-dropdown/compliance.tsx +++ b/web/app/components/header/account-dropdown/compliance.tsx @@ -8,7 +8,6 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useMutation, useQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' @@ -22,6 +21,7 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { toast } from '@/app/notifications' import { getDocDownloadUrl } from '@/service/common' import { consoleQuery } from '@/service/console' import { downloadUrl } from '@/utils/download' diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx index 6e2a4bbae28..c167813814f 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx @@ -18,10 +18,10 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' type AccessRuleRowMenuProps = { diff --git a/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx index 834b15c386a..adee5bf1961 100644 --- a/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx +++ b/web/app/components/header/account-setting/access-rules-page/resource-access-rule-section.tsx @@ -3,12 +3,12 @@ import type { PermissionSetFormValues, PermissionSetModalMode } from './permission-set-modal' import type { AccessRule } from './types' import type { AccessPolicyResourceType } from '@/models/access-control' -import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useMutation } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { getAccessControlTemplateLanguage } from '@/i18n/language' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/header/account-setting/api-based-extension-page/__tests__/modal.spec.tsx b/web/app/components/header/account-setting/api-based-extension-page/__tests__/modal.spec.tsx index f5dd0d3057a..7226f018bd2 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/__tests__/modal.spec.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/__tests__/modal.spec.tsx @@ -51,7 +51,7 @@ vi.mock('@tanstack/react-query', () => ({ })), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index 2c09de22bf6..2bf3ea1b1d6 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -8,9 +8,9 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '@langgenius/dif import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx index 98c835987a9..8d4adf6e2eb 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx @@ -1,9 +1,9 @@ import type { Role } from '@/models/access-control' import type { Member } from '@/models/common' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import { deleteMemberOrCancelInvitation } from '@/service/common' @@ -16,7 +16,7 @@ vi.mock('@/service/access-control/use-workspace-roles') vi.mock('@/service/common', () => ({ deleteMemberOrCancelInvitation: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), }, diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx index 41e19f28878..3c2e99d19a7 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx @@ -22,7 +22,7 @@ vi.mock('@/context/workspace-state', async () => { }) vi.mock('@/service/common') -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => toastMocks.mockNotify(args), }, diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx index cd01e2f7d06..c04b05d97ba 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx @@ -4,10 +4,10 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' import { updateWorkspaceInfo } from '@/service/common' diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index d808cea5fd6..3141b022438 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -2,7 +2,6 @@ import type { MemberInviteResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { Role } from '@/models/access-control' import type { Member } from '@/models/common' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -11,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' import UpgradeBtn from '@/app/components/billing/upgrade-btn' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' diff --git a/web/app/components/header/account-setting/members-page/member-menu.tsx b/web/app/components/header/account-setting/members-page/member-menu.tsx index 5d69a2b3c7a..13c4d788ecf 100644 --- a/web/app/components/header/account-setting/members-page/member-menu.tsx +++ b/web/app/components/header/account-setting/members-page/member-menu.tsx @@ -18,10 +18,10 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { deleteMemberOrCancelInvitation } from '@/service/common' import { commonQueryKeys } from '@/service/use-common' diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx index 750aa5efdad..703fccb4492 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx @@ -23,7 +23,7 @@ vi.mock('@/context/workspace-state', async () => { vi.mock('@/service/common') vi.mock('@/service/use-common') -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => toastMocks.mockNotify(args), }, diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index fb883a14695..317236f0e0d 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -1,12 +1,12 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { currentWorkspaceAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx index 540879d975e..14484fd2f4c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx @@ -17,8 +17,8 @@ const mockAddModelCredential = vi.fn() const mockEditProviderCredential = vi.fn() const mockEditModelCredential = vi.fn() -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, default: { diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts index 7918092c7c3..25a3a73e91d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts @@ -6,13 +6,13 @@ import type { ModelModalModeEnum, ModelProvider, } from '../../declarations' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useModelModalHandler, useRefreshModel, } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { toast } from '@/app/notifications' import { useDeleteModel } from '@/service/use-models' import { useAuthService } from './use-auth-service' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx index d3e9231dbd9..d6e5b29471f 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx @@ -29,7 +29,7 @@ const { mockChangePriorityFn: vi.fn().mockResolvedValue({ result: 'success' }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: mockToastNotify }, toast: { success: (message: string) => mockToastNotify({ type: 'success', message }), diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.dynamic-import.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.dynamic-import.spec.tsx index 32ee443c2f2..d6a1a253fe7 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.dynamic-import.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.dynamic-import.spec.tsx @@ -21,7 +21,7 @@ vi.mock('../../hooks', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.spec.tsx index cd34dcf3190..d925fe5ec90 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list.spec.tsx @@ -32,7 +32,7 @@ vi.mock('../../hooks', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx index dded8cc2881..3880d6d2a40 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx @@ -56,8 +56,8 @@ let mockCredentialData: CredentialData | undefined = { current_credential_name: 'Default', } -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, default: { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-change-provider-priority.spec.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-change-provider-priority.spec.ts index ce2247fd7ce..1d86dc9807a 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-change-provider-priority.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-change-provider-priority.spec.ts @@ -24,7 +24,7 @@ const mockMutationOptions = vi.fn((options: Record) => ({ ...options, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (...args: unknown[]) => mockNotify(...args), }, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx index d9331e04e8f..c8acafef9cf 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx @@ -1,6 +1,6 @@ import type { Credential, ModelProvider } from '../../../declarations' -import { toast } from '@langgenius/dify-ui/toast' import { act, renderHook } from '@testing-library/react' +import { toast } from '@/app/notifications' import { useActivateCredential } from '../use-activate-credential' const mockMutate = vi.fn() diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts index 1c7d2a13770..98f67b6ce56 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts @@ -1,7 +1,7 @@ import type { Credential, ModelProvider } from '../../declarations' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useActiveProviderCredential } from '@/service/use-models' import { useUpdateModelList, useUpdateModelProviders } from '../../hooks' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx index 327c77aebc7..3d8015f1588 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx @@ -3,10 +3,10 @@ import type { ComponentType, FC } from 'react' import type { Credential, ModelItem, ModelProvider } from '../declarations' import type { ModelLoadBalancingModalProps } from './model-load-balancing-modal' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { hasPermission } from '@/utils/permission' import { ConfigurationMethodEnum } from '../declarations' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx index 838b287eac3..1275275327f 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -17,11 +17,11 @@ import { import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' +import { toast } from '@/app/notifications' import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models' import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations' import { useRefreshModel } from '../hooks' diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts index 71339c9b5ce..d05cf2e7287 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts @@ -1,8 +1,8 @@ import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { ModelProvider, PreferredProviderTypeEnum } from '../declarations' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { ConfigurationMethodEnum } from '../declarations' import { useUpdateModelList, useUpdateModelProviders } from '../hooks' diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx index 7e2dbaa169a..fab82b7341e 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx @@ -53,8 +53,8 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx index aae33c99393..491b6721e64 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx @@ -4,13 +4,13 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { parseAsStringLiteral, useQueryState } from 'nuqs' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { updateDefaultModel } from '@/service/common' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx index 7f9ea498088..cd538617f16 100644 --- a/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx @@ -1,7 +1,7 @@ import type { Role } from '@/models/access-control' -import { toast } from '@langgenius/dify-ui/toast' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { useCreateWorkspaceRole, useUpdateWorkspaceRole, @@ -16,7 +16,7 @@ const mocks = vi.hoisted(() => ({ updateWorkspaceRole: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), }, diff --git a/web/app/components/header/account-setting/permissions-page/index.tsx b/web/app/components/header/account-setting/permissions-page/index.tsx index dd4b1fdc7c4..305ee6341f2 100644 --- a/web/app/components/header/account-setting/permissions-page/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/index.tsx @@ -3,11 +3,11 @@ import type { RoleModalMode, submitRoleData } from './role-modal' import type { Role } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { getAccessControlTemplateLanguage } from '@/i18n/language' import { diff --git a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx index 42d4503b353..0e975884a67 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx @@ -52,7 +52,7 @@ vi.mock('@/service/access-control/use-workspace-roles', () => ({ useGetMembersOfRole: membersOfRoleQueryMock.useGetMembersOfRole, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), }, diff --git a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx index 49ddf1825d3..167490c671b 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx @@ -17,10 +17,10 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useCopyWorkspaceRole, diff --git a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx index 494c1479bd0..a1e0af6e721 100644 --- a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' -import { ToastHost } from '@langgenius/dify-ui/toast' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { AppToastHost } from '@/app/notifications/host' import { languages } from '@/i18n/language' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { render } from '@/test/console/render' @@ -53,7 +53,7 @@ const renderPage = () => { return render( - + , ) } diff --git a/web/app/components/header/account-setting/preference-page/index.tsx b/web/app/components/header/account-setting/preference-page/index.tsx index 281c09a4f9f..e30061acf0b 100644 --- a/web/app/components/header/account-setting/preference-page/index.tsx +++ b/web/app/components/header/account-setting/preference-page/index.tsx @@ -8,12 +8,12 @@ import { SelectItemText, SelectTrigger, } from '@langgenius/dify-ui/select' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useTheme } from 'next-themes' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import { setLocaleOnClient } from '@/i18n/client' import { languages } from '@/i18n/language' diff --git a/web/app/components/header/account-setting/update-setting-dialog.tsx b/web/app/components/header/account-setting/update-setting-dialog.tsx index 5ce51580d87..756f40052c3 100644 --- a/web/app/components/header/account-setting/update-setting-dialog.tsx +++ b/web/app/components/header/account-setting/update-setting-dialog.tsx @@ -15,7 +15,6 @@ import { DialogTrigger, } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -30,6 +29,7 @@ import { dayjsToTimeOfDay, timeOfDayToDayjs, } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useMutationPluginAutoUpgradeSettings, diff --git a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx index 96950b8fbd2..c1053deaefc 100644 --- a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx +++ b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx @@ -7,7 +7,6 @@ import type { } from '@dify/contracts/api/console/workflow-run-archives/types.gen' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { skipToken, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' @@ -18,6 +17,7 @@ import { pricingQueryParamName, pricingQueryParser, } from '@/app/components/billing/pricing/query-params' +import { toast } from '@/app/notifications' import { API_PREFIX } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index f98ae8b8ee0..268e40b442b 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -364,8 +364,8 @@ vi.mock('@/service/console', async (importOriginal) => { } }) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx index d91f1d0fc45..7ae225de38d 100644 --- a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx @@ -40,7 +40,7 @@ vi.mock('@/app/components/base/zendesk/runtime', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError }, })) diff --git a/web/app/components/main-nav/components/support-menu.tsx b/web/app/components/main-nav/components/support-menu.tsx index 928e6334bdf..69f988b77dd 100644 --- a/web/app/components/main-nav/components/support-menu.tsx +++ b/web/app/components/main-nav/components/support-menu.tsx @@ -1,5 +1,4 @@ import { DropdownMenuItem, DropdownMenuLinkItem } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useTranslation } from 'react-i18next' @@ -13,6 +12,7 @@ import { MenuItemContent, } from '@/app/components/header/account-dropdown/menu-item-content' import { generateMailToLink, mailToSupport } from '@/app/components/header/utils/util' +import { toast } from '@/app/notifications' import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/components/main-nav/components/web-apps-section.tsx b/web/app/components/main-nav/components/web-apps-section.tsx index 9ee92db5211..bdeac549470 100644 --- a/web/app/components/main-nav/components/web-apps-section.tsx +++ b/web/app/components/main-nav/components/web-apps-section.tsx @@ -24,7 +24,6 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { toast } from '@langgenius/dify-ui/toast' import { keepPreviousData, useInfiniteQuery, useMutation } from '@tanstack/react-query' import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual' import { useAtomValue } from 'jotai' @@ -37,6 +36,7 @@ import { SearchInput } from '@/app/components/base/search-input' import AppNavItem from '@/app/components/explore/installed-app-navigation/app-nav-item' import { InstalledAppPaginationSkeleton } from '@/app/components/explore/installed-app-navigation/pagination-skeleton' import { isInstalledAppPath } from '@/app/components/explore/installed-app/routes' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { usePathname } from '@/next/navigation' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx index 9edcf19652e..695aec921e6 100644 --- a/web/app/components/main-nav/components/workspace-card.tsx +++ b/web/app/components/main-nav/components/workspace-card.tsx @@ -4,7 +4,6 @@ import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/co import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { noop, useMutation, @@ -27,6 +26,7 @@ import { } from '@/app/components/header/account-setting/query-params' import LicenseBadge from '@/app/components/header/license-badge' import { buildIntegrationPath } from '@/app/components/integrations/routes' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' diff --git a/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts b/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts index f00eced0a2d..e4daa471cac 100644 --- a/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts +++ b/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts @@ -6,7 +6,7 @@ const { mockToastError, mockUploadGitHub } = vi.hoisted(() => ({ mockUploadGitHub: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/plugins/install-plugin/hooks.ts b/web/app/components/plugins/install-plugin/hooks.ts index f6b0abf28dd..ffcb2b4441a 100644 --- a/web/app/components/plugins/install-plugin/hooks.ts +++ b/web/app/components/plugins/install-plugin/hooks.ts @@ -1,5 +1,5 @@ import type { GitHubRepoReleaseResponse } from '../types' -import { toast } from '@langgenius/dify-ui/toast' +import { toast } from '@/app/notifications' import { uploadGitHub } from '@/service/plugins' import { compareVersion, getLatestVersion } from '@/utils/semver' diff --git a/web/app/components/plugins/install-plugin/install-from-github/__tests__/index.spec.tsx b/web/app/components/plugins/install-plugin/install-from-github/__tests__/index.spec.tsx index 38a39753ea0..79c628a0b60 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/__tests__/index.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/__tests__/index.spec.tsx @@ -84,7 +84,7 @@ const createUpdatePayload = ( // Mock external dependencies const mockNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign((props: { type: string; message: string }) => mockNotify(props), { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/plugins/install-plugin/install-from-github/index.tsx b/web/app/components/plugins/install-plugin/install-from-github/index.tsx index c599227ff1b..58745548189 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/index.tsx @@ -8,11 +8,11 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' +import { toast } from '@/app/notifications' import { InstallStepFromGitHub } from '../../types' import Installed from '../base/installed' import { fetchReleases } from '../hooks' diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx index a3c27c71e38..1ddbb3f9dd4 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/api-key-modal.spec.tsx @@ -28,7 +28,7 @@ const { mockToast } = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/authorize-components.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/authorize-components.spec.tsx index d7cb0c17665..7b42d4409dc 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/authorize-components.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/authorize-components.spec.tsx @@ -97,7 +97,7 @@ const { mockToast } = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx index 6df82b7e112..e3ad0b19dca 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx @@ -22,7 +22,7 @@ const mockToast = { promise: vi.fn(), } -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({}) diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index 8e9d4839bc5..a88e48fdfaf 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -4,13 +4,13 @@ import type { CredentialPermission } from '@/models/permission' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { EncryptedBottom } from '@/app/components/base/encrypted-bottom' import AuthForm from '@/app/components/base/form/form-scenarios/auth' import { FormTypeEnum } from '@/app/components/base/form/types' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { PermissionLevel } from '@/models/permission' import { ReadmeEntrance } from '../../readme-panel/entrance' import { diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx index c2c6aa22ffd..6296cec2202 100644 --- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -3,11 +3,11 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useForm, useStore } from '@tanstack/react-form' import { memo, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AuthForm from '@/app/components/base/form/form-scenarios/auth' +import { toast } from '@/app/notifications' import { ReadmeEntrance } from '../../readme-panel/entrance' import { useDeletePluginOAuthCustomClientHook, diff --git a/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx index d67b349ff88..0f8a8d29892 100644 --- a/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx @@ -59,7 +59,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/plugins/plugin-auth/authorized/index.tsx b/web/app/components/plugins/plugin-auth/authorized/index.tsx index 9a72b52e429..9c2325c9328 100644 --- a/web/app/components/plugins/plugin-auth/authorized/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -13,9 +13,9 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import Authorize from '../authorize' import ApiKeyModal from '../authorize/api-key-modal' diff --git a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-plugin-auth-action.spec.ts b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-plugin-auth-action.spec.ts index 366f69d3af5..f5ac3845f61 100644 --- a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-plugin-auth-action.spec.ts +++ b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-plugin-auth-action.spec.ts @@ -16,7 +16,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth-action.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth-action.ts index f62ba05eb35..13a12965da5 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth-action.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth-action.ts @@ -1,7 +1,7 @@ import type { PluginPayload } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDeletePluginCredentialHook, useSetPluginDefaultCredentialHook, diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx index 1502069bc6c..17b6b5fe3e9 100644 --- a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx @@ -15,7 +15,7 @@ const getEditButton = () => screen.getByRole('button', { name: 'common.operation const getDeleteButton = () => screen.getByRole('button', { name: 'common.operation.delete' }) const getCopyButton = () => screen.getByRole('button', { name: 'common.operation.copy' }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-modal.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-modal.spec.tsx index a42223a8592..008ce84d138 100644 --- a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-modal.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-modal.spec.tsx @@ -6,7 +6,7 @@ import EndpointModal from '../endpoint-modal' const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts index db8dc0ac74c..1247538b42d 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts @@ -34,7 +34,7 @@ const { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts index 30005bdb255..e2b4d31d739 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts @@ -2,11 +2,11 @@ import type { PluginDetail } from '../../../types' import type { ModalStates, VersionTarget } from './use-detail-header-state' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' +import { toast } from '@/app/notifications' import { useModalContext } from '@/context/modal-context' import { uninstallPlugin } from '@/service/plugins' import { useInvalidateCheckInstalled } from '@/service/use-plugins' diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index ea1d4bdc5b1..20ce15f0ce2 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -11,7 +11,6 @@ import { import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' @@ -22,6 +21,7 @@ import { addDefaultValue, toolCredentialToFormSchemas, } from '@/app/components/tools/utils/to-form-schema' +import { toast } from '@/app/notifications' import { useDeleteEndpoint, useDisableEndpoint, diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 4b8ed551d7e..0bc82f5f948 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -2,12 +2,12 @@ import type { PluginDetail } from '@/app/components/plugins/types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { useCreateEndpoint, diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index 551a749e2d8..f9e9e1f0aa5 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -13,11 +13,11 @@ import { DrawerViewport, } from '@langgenius/dify-ui/drawer' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightUpLine, RiCloseLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' +import { toast } from '@/app/notifications' import { useRenderI18nObject } from '@/hooks/use-i18n' import { ReadmeEntrance } from '../readme-panel/entrance' diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx index 851ef468771..4d38c62adea 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/index.spec.tsx @@ -18,7 +18,7 @@ import ModelParameterModal from '../index' // ==================== Mock Setup ==================== const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx index f19ffcb8ac3..3a49bcbfab2 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx @@ -6,7 +6,6 @@ import type { import type { ModelSelectorValue } from '@/app/components/header/account-setting/model-provider-page/model-selector/types' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -16,6 +15,7 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelSettingsTrigger } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger' import { SplitModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' import LLMParamsPanel from './llm-params-panel' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx index 8838cffef8f..876f45935b2 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx @@ -20,8 +20,8 @@ vi.mock('@/service/use-triggers', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@/app/notifications', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, toast: { diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/log-viewer.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/log-viewer.spec.tsx index bb23c8135ea..16dc2639b31 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/log-viewer.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/log-viewer.spec.tsx @@ -6,7 +6,7 @@ import LogViewer from '../log-viewer' const mockToastNotify = vi.fn() const mockWriteText = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx index d743394d4fc..91dffd4e5fc 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx @@ -27,7 +27,7 @@ vi.mock('@/service/use-triggers', () => ({ useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: Object.assign(vi.fn(), { success: vi.fn(), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-view.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-view.spec.tsx index dedf9451fa7..0d9580e5bb3 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-view.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-view.spec.tsx @@ -25,7 +25,7 @@ vi.mock('@/service/use-triggers', () => ({ useDeleteTriggerSubscription: () => ({ mutate: mockDelete, isPending: false }), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: Object.assign(vi.fn(), { success: vi.fn(), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/subscription-card.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/subscription-card.spec.tsx index a70f8448f03..2105a12a36f 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/subscription-card.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/subscription-card.spec.tsx @@ -31,7 +31,7 @@ vi.mock('@/service/use-triggers', () => ({ useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/common-modal.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/common-modal.spec.tsx index f9b7719a75f..8a9216301f3 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/common-modal.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/common-modal.spec.tsx @@ -138,7 +138,7 @@ vi.mock('@/utils/urlValidation', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign((params: unknown) => mockToastNotify(params), { success: (message: unknown) => mockToastNotify({ type: 'success', message }), error: (message: unknown) => mockToastNotify({ type: 'error', message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx index 10376299b19..46c7e251aa0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx @@ -14,7 +14,7 @@ const toastMocks = vi.hoisted(() => ({ error: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks, })) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx index e22a125aafc..6b8cb55b9e1 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx @@ -103,7 +103,7 @@ vi.mock('@/hooks/use-oauth', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.helpers.spec.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.helpers.spec.ts index af953735a92..05f105d69fd 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.helpers.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.helpers.spec.ts @@ -25,7 +25,7 @@ const { mockToastError, mockIsPrivateOrLocalAddress } = vi.hoisted(() => ({ mockIsPrivateOrLocalAddress: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.spec.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.spec.ts index bb079785a3b..74443ea3312 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-common-modal-state.spec.ts @@ -93,7 +93,7 @@ vi.mock('@/service/use-triggers', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockToastNotify({ type: 'success', message }), error: (message: string) => mockToastNotify({ type: 'error', message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts index 086e22494dc..8b7acba171a 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts @@ -93,7 +93,7 @@ vi.mock('@/hooks/use-oauth', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.helpers.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.helpers.ts index ba4c9671e86..7ba99ffeab3 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.helpers.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.helpers.ts @@ -4,9 +4,9 @@ import type { Dispatch, SetStateAction } from 'react' import type { FormRefObject } from '@/app/components/base/form/types' import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types' import type { BuildTriggerSubscriptionPayload } from '@/service/use-triggers' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useRef } from 'react' import { SupportedCreationMethods } from '@/app/components/plugins/types' +import { toast } from '@/app/notifications' import { isPrivateOrLocalAddress } from '@/utils/urlValidation' type FormValuesResult = { diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts index e092e631be8..13be8852817 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts @@ -7,12 +7,12 @@ import type { TriggerLogEntity, TriggerSubscriptionBuilder, } from '@/app/components/workflow/block-selector/types' -import { toast } from '@langgenius/dify-ui/toast' import { debounce } from 'es-toolkit/compat' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SupportedCreationMethods } from '@/app/components/plugins/types' import { TriggerCredentialType } from '@/app/components/workflow/block-selector/types' +import { toast } from '@/app/notifications' import { useBuildTriggerSubscription, useCreateTriggerSubscriptionBuilder, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts index 598563a63ac..92bc82b503c 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts @@ -6,9 +6,9 @@ import type { TriggerSubscriptionBuilder, } from '@/app/components/workflow/block-selector/types' import type { ConfigureTriggerOAuthPayload } from '@/service/use-triggers' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { openOAuthPopup } from '@/hooks/use-oauth' import { useConfigureTriggerOAuth, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx index fa3c9b264c6..108c05a8c92 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -9,13 +9,13 @@ import { SelectItemIndicator, SelectTrigger, } from '@langgenius/dify-ui/select' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Infotip } from '@/app/components/base/infotip' +import { toast } from '@/app/notifications' import { openOAuthPopup } from '@/hooks/use-oauth' import { useInitiateTriggerOAuth, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx index 56ad1567906..22ab5d89cca 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx @@ -6,11 +6,11 @@ import type { import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' +import { toast } from '@/app/notifications' import { usePluginStore } from '../../store' import { ClientTypeEnum, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx index 8f1e281ca17..70ff266ec85 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx @@ -9,9 +9,9 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDeleteTriggerSubscription } from '@/service/use-triggers' import { useSubscriptionList } from './use-subscription-list' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx index bf30df020d7..75033df8f4b 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx @@ -14,7 +14,7 @@ import { OAuthEditModal } from '../oauth-edit-modal' // ==================== Mock Setup ==================== const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx index f7cf746c5e3..d98feaeab56 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx @@ -5,7 +5,6 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -13,6 +12,7 @@ import { EncryptedBottom } from '@/app/components/base/encrypted-bottom' import { BaseForm } from '@/app/components/base/form/components/base' import { FormTypeEnum } from '@/app/components/base/form/types' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' +import { toast } from '@/app/notifications' import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers' import { parsePluginErrorMessage } from '@/utils/error-parser' import { usePluginStore } from '../../store' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx index 8a02883c257..726fea0550c 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx @@ -5,13 +5,13 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' import { FormTypeEnum } from '@/app/components/base/form/types' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' +import { toast } from '@/app/notifications' import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx index 2d8edb940b2..a1d90c83cdf 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx @@ -5,13 +5,13 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' import { FormTypeEnum } from '@/app/components/base/form/types' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' +import { toast } from '@/app/notifications' import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx index fa07129742a..e80fa31f880 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx @@ -1,7 +1,6 @@ 'use client' import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowDownSLine, RiArrowRightSLine, @@ -15,6 +14,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { toast } from '@/app/notifications' type Props = Readonly<{ logs: TriggerLogEntity[] diff --git a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx index 672e569011c..51095bdb14b 100644 --- a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx +++ b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx @@ -26,7 +26,7 @@ const { mockToastNotify: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign( (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index 0ae0f0e7094..b825d1fc2b0 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -11,12 +11,12 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useModalContext } from '@/context/modal-context' import { uninstallPlugin } from '@/service/plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' diff --git a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts index c6b4c319cff..34f7b37f95c 100644 --- a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts +++ b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts @@ -1,9 +1,9 @@ import type { LangGeniusVersionInfo } from '@/context/app-context-types' // Import mocks for assertions import type { ConsoleStateFixture as BaseConsoleStateFixture } from '@/test/console/state-fixture' -import { toast } from '@langgenius/dify-ui/toast' import { waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { useInvalidateReferenceSettings, useMutationPluginPermissionSettings, diff --git a/web/app/components/plugins/plugin-page/use-reference-setting.ts b/web/app/components/plugins/plugin-page/use-reference-setting.ts index 3194d35592b..28780b232e5 100644 --- a/web/app/components/plugins/plugin-page/use-reference-setting.ts +++ b/web/app/components/plugins/plugin-page/use-reference-setting.ts @@ -1,9 +1,9 @@ import type { PluginCategoryEnum } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom, workspacePermissionKeysLoadingAtom, diff --git a/web/app/components/plugins/update-plugin/__tests__/from-market-place.spec.tsx b/web/app/components/plugins/update-plugin/__tests__/from-market-place.spec.tsx index 35d5cc56bb7..4ebd85f6cba 100644 --- a/web/app/components/plugins/update-plugin/__tests__/from-market-place.spec.tsx +++ b/web/app/components/plugins/update-plugin/__tests__/from-market-place.spec.tsx @@ -36,7 +36,7 @@ vi.mock('@/app/components/base/badge/index', () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx b/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx index 2fab812b0de..4f42635a1e9 100644 --- a/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx +++ b/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx @@ -4,12 +4,12 @@ import type { UpdateFromMarketPlacePayload, UpdatePluginModalType, } from '../../types' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { PluginCategoryEnum, PluginSource, TaskStatus } from '../../types' import DowngradeWarningModal from '../downgrade-warning' diff --git a/web/app/components/plugins/update-plugin/from-market-place.tsx b/web/app/components/plugins/update-plugin/from-market-place.tsx index fc93b1cea0a..9683e3e0885 100644 --- a/web/app/components/plugins/update-plugin/from-market-place.tsx +++ b/web/app/components/plugins/update-plugin/from-market-place.tsx @@ -4,7 +4,6 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +11,7 @@ import Badge, { BadgeState } from '@/app/components/base/badge/index' import Card from '@/app/components/plugins/card' import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status' import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils' +import { toast } from '@/app/notifications' import { updateFromMarketPlace } from '@/service/plugins' import { usePluginTaskList, useRemoveAutoUpgrade } from '@/service/use-plugins' import useGetIcon from '../install-plugin/base/use-get-icon' diff --git a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx index 3c0c976ad51..2f3bcf7cf8c 100644 --- a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx @@ -56,7 +56,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx index 01a752d93fb..34e75e903e9 100644 --- a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx @@ -27,7 +27,7 @@ const toastMocks = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.api, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/rag-pipeline/components/conversion.tsx b/web/app/components/rag-pipeline/components/conversion.tsx index a28a3826278..d5c06b682bc 100644 --- a/web/app/components/rag-pipeline/components/conversion.tsx +++ b/web/app/components/rag-pipeline/components/conversion.tsx @@ -8,12 +8,12 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx index 867283eefe9..b2e79b66fb5 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import type { FormData, InputFieldFormProps } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react' import * as React from 'react' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' import { useConfigurations, useHiddenConfigurations, useHiddenFieldNames } from '../hooks' import InputFieldForm from '../index' diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx index eaa09f77b18..d132c07826d 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx @@ -1,13 +1,13 @@ import type { FormData, InputFieldFormProps } from './types' import type { MoreInfo } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { useAppForm } from '@/app/components/base/form' import { ChangeType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useFileUploadConfig } from '@/service/use-common' import HiddenFields from './hidden-fields' import InitialFields from './initial-fields' diff --git a/web/app/components/rag-pipeline/components/panel/input-field/field-list/__tests__/hooks.spec.ts b/web/app/components/rag-pipeline/components/panel/input-field/field-list/__tests__/hooks.spec.ts index 810e6dae986..111a5ef1ec5 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/field-list/__tests__/hooks.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/field-list/__tests__/hooks.spec.ts @@ -23,7 +23,7 @@ vi.mock('../../../../../hooks/use-pipeline', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: (...args: unknown[]) => mockToastNotify(...args), }, diff --git a/web/app/components/rag-pipeline/components/panel/input-field/field-list/hooks.ts b/web/app/components/rag-pipeline/components/panel/input-field/field-list/hooks.ts index e7a99b0e46c..8ea4d4a0bd1 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/field-list/hooks.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/field-list/hooks.ts @@ -1,12 +1,12 @@ import type { SortableItem } from './types' import type { MoreInfo, ValueSelector } from '@/app/components/workflow/types' import type { InputVar } from '@/models/pipeline' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import { produce } from 'immer' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ChangeType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { useInputFieldPanel } from '../../../../hooks/use-input-field-panel' import { usePipeline } from '../../../../hooks/use-pipeline' diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx index 6ab4683fb43..d30eef3e949 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx @@ -61,7 +61,7 @@ vi.mock('@/app/components/base/form/form-scenarios/base/utils', () => ({ const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockToastNotify({ type: 'error', message }), }, diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/options.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/options.spec.tsx index 17a01ba1374..fd80cac89a2 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/options.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/options.spec.tsx @@ -15,7 +15,7 @@ const { mockFormValue, mockHandleSubmit, mockToastError, mockBaseField } = vi.ho }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx index 16a1fce37db..b54ad05026b 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx @@ -1,9 +1,9 @@ import type { ZodSchema } from 'zod' import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions' import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types' -import { toast } from '@langgenius/dify-ui/toast' import { useAppForm } from '@/app/components/base/form' import BaseField from '@/app/components/base/form/form-scenarios/base/field' +import { toast } from '@/app/notifications' type OptionsProps = { initialData: Record diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index 63f735852a4..fbf6b367fbb 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -131,7 +131,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 97daa81e3ff..8afee8cad19 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -26,7 +26,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx index e0ea938e948..96e5ba3e363 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx @@ -2,7 +2,6 @@ import type { IconInfo } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { memo, useCallback, useState } from 'react' @@ -10,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { useHooksStore } from '@/app/components/workflow/hooks-store' import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' import { useStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import Link from '@/next/link' import { diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 478df63029e..9ce94c465df 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -11,7 +11,6 @@ import { import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightUpLine, RiPlayCircleLine, RiTerminalBoxLine } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' @@ -30,6 +29,7 @@ import { } from '@/app/components/billing/pricing/query-params' import { useChecklistBeforePublish } from '@/app/components/workflow/hooks/use-checklist' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom, diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts index c4b84df87ad..417c9e7a7dc 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts @@ -9,7 +9,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts index cae5320829a..fd8c20bfd79 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts @@ -26,7 +26,7 @@ const toastMocks = vi.hoisted(() => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.api, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/rag-pipeline/hooks/use-DSL.ts b/web/app/components/rag-pipeline/hooks/use-DSL.ts index 9158d14b871..30db16c2beb 100644 --- a/web/app/components/rag-pipeline/hooks/use-DSL.ts +++ b/web/app/components/rag-pipeline/hooks/use-DSL.ts @@ -1,9 +1,9 @@ import type { useNodesSyncDraft } from './use-nodes-sync-draft' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' import { useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useExportPipelineDSL } from '@/service/use-pipeline' import { fetchWorkflowDraft } from '@/service/workflow' diff --git a/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts b/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts index 5db04050573..c5691fdb755 100644 --- a/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts +++ b/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts @@ -1,11 +1,11 @@ import type { MouseEventHandler } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { WORKFLOW_DATA_UPDATE } from '@/app/components/workflow/constants' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { useWorkflowStore } from '@/app/components/workflow/store' import { initialEdges, initialNodes } from '@/app/components/workflow/utils' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline' diff --git a/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts b/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts index 0a9dae58cc1..8ea18a226be 100644 --- a/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts +++ b/web/app/components/share/text-generation/hooks/__tests__/use-text-generation-app-state.spec.ts @@ -28,7 +28,7 @@ const { useDocumentTitleMock: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: notifyMock, }, diff --git a/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts b/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts index 90a989d2273..dcda05c3407 100644 --- a/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts +++ b/web/app/components/share/text-generation/hooks/use-text-generation-app-state.ts @@ -7,11 +7,11 @@ import type { } from '@/models/debug' import type { SiteInfo } from '@/models/share' import type { VisionSettings } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { getRawInputsFromUrlParams } from '@/app/components/base/chat/utils' +import { toast } from '@/app/notifications' import { useWebAppStore } from '@/context/web-app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useAppFavicon } from '@/hooks/use-app-favicon' diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index be82e474cd5..49530a88fb9 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -3,11 +3,11 @@ import type { FC } from 'react' import type { InputValueTypes, TextGenerationRunControl, TextGenerationTranslate } from './types' import type { VisionFile } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useSearchParams } from '@/next/navigation' import { useTextGenerationAppState } from './hooks/use-text-generation-app-state' diff --git a/web/app/components/share/text-generation/result/__tests__/index.spec.tsx b/web/app/components/share/text-generation/result/__tests__/index.spec.tsx index 285cca94c49..2a47c6c313b 100644 --- a/web/app/components/share/text-generation/result/__tests__/index.spec.tsx +++ b/web/app/components/share/text-generation/result/__tests__/index.spec.tsx @@ -45,7 +45,7 @@ vi.mock('react-i18next', async (importOriginal) => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: (...args: unknown[]) => notifyMock(...args), })) diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 1a39d3074d2..c4e10f664e0 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -6,13 +6,13 @@ import type { SiteInfo } from '@/models/share' import type { AppSourceType } from '@/service/share' import type { VisionFile, VisionSettings } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' import TextGenerationRes from '@/app/components/app/text-generate/item' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import NoData from '@/app/components/share/text-generation/no-data' +import { toast } from '@/app/notifications' import { useResultRunState } from './hooks/use-result-run-state' import { useResultSender } from './hooks/use-result-sender' diff --git a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx index 5b36d04c8e1..39d21fb4107 100644 --- a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx +++ b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx @@ -91,7 +91,7 @@ vi.mock('@/utils/download', () => ({ downloadBlob: mockDownloadBlob, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/snippet-list/components/__tests__/snippet-create-button.spec.tsx b/web/app/components/snippet-list/components/__tests__/snippet-create-button.spec.tsx index e9feb419425..f113a765af3 100644 --- a/web/app/components/snippet-list/components/__tests__/snippet-create-button.spec.tsx +++ b/web/app/components/snippet-list/components/__tests__/snippet-create-button.spec.tsx @@ -28,7 +28,7 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/snippet-list/components/snippet-card.tsx b/web/app/components/snippet-list/components/snippet-card.tsx index 5ad8a0be4f4..c267d65a471 100644 --- a/web/app/components/snippet-list/components/snippet-card.tsx +++ b/web/app/components/snippet-list/components/snippet-card.tsx @@ -18,7 +18,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,6 +26,7 @@ import { canCreateAndModifySnippets, canManageSnippets, } from '@/app/components/snippets/utils/permission' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { TagSelector } from '@/features/tag-management/components/tag-selector' import Link from '@/next/link' diff --git a/web/app/components/snippets/__tests__/import-snippet-dsl-dialog.spec.tsx b/web/app/components/snippets/__tests__/import-snippet-dsl-dialog.spec.tsx index 966f0a47b28..b2f28584e0a 100644 --- a/web/app/components/snippets/__tests__/import-snippet-dsl-dialog.spec.tsx +++ b/web/app/components/snippets/__tests__/import-snippet-dsl-dialog.spec.tsx @@ -32,7 +32,7 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks, })) diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx index d1fe99577cd..adba9b918d2 100644 --- a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react' import type { WorkflowProps } from '@/app/components/workflow' import type { SnippetDetail, SnippetDetailPayload, SnippetInputField } from '@/models/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' import { useSnippetDraftStore } from '../../draft-store' import SnippetMain from '../snippet-main' @@ -46,7 +46,7 @@ const mockInspectVarsCrud = { invalidateConversationVarValues: vi.fn(), } -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/snippets/components/__tests__/snippet-run-panel.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-run-panel.spec.tsx index b09aec06e8b..0b057cb75c4 100644 --- a/web/app/components/snippets/components/__tests__/snippet-run-panel.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-run-panel.spec.tsx @@ -31,7 +31,7 @@ vi.mock('copy-to-clipboard', () => ({ default: copyMock, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks, })) diff --git a/web/app/components/snippets/components/__tests__/snippet-sidebar.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-sidebar.spec.tsx index 7e4e3ec0b46..796f43d9ee5 100644 --- a/web/app/components/snippets/components/__tests__/snippet-sidebar.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-sidebar.spec.tsx @@ -1,7 +1,7 @@ import type { InputVar } from '@/app/components/workflow/types' import type { SnippetDetail, SnippetInputField } from '@/models/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' import { SnippetSidebarContent } from '../snippet-sidebar' @@ -24,7 +24,7 @@ vi.mock('@/app/components/app-sidebar/snippet-info/dropdown', () => ({ default: () =>
, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/snippets/components/hooks/__tests__/use-snippet-dsl.spec.ts b/web/app/components/snippets/components/hooks/__tests__/use-snippet-dsl.spec.ts index 0a02982a096..f282b35a745 100644 --- a/web/app/components/snippets/components/hooks/__tests__/use-snippet-dsl.spec.ts +++ b/web/app/components/snippets/components/hooks/__tests__/use-snippet-dsl.spec.ts @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { renderHook } from '@testing-library/react' import { act } from 'react' +import { toast } from '@/app/notifications' import { useExportSnippetMutation } from '@/service/use-snippets' import { downloadBlob } from '@/utils/download' import { useSnippetDSL } from '../use-snippet-dsl' @@ -17,7 +17,7 @@ vi.mock('@/utils/download', () => ({ downloadBlob: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts b/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts index 7f2006fa234..34b5399bc1b 100644 --- a/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts +++ b/web/app/components/snippets/components/hooks/__tests__/use-snippet-publish.spec.ts @@ -1,5 +1,5 @@ -import { toast } from '@langgenius/dify-ui/toast' import { act, renderHook } from '@testing-library/react' +import { toast } from '@/app/notifications' import { useSnippetPublish } from '../use-snippet-publish' const mockMutateAsync = vi.fn() @@ -10,7 +10,7 @@ const mockHandleCheckBeforePublish = vi.fn<() => Promise>() let isPending = false -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/app/components/snippets/components/hooks/use-snippet-dsl.ts b/web/app/components/snippets/components/hooks/use-snippet-dsl.ts index eba5a0879e9..d1fffa7f1e7 100644 --- a/web/app/components/snippets/components/hooks/use-snippet-dsl.ts +++ b/web/app/components/snippets/components/hooks/use-snippet-dsl.ts @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useExportSnippetMutation } from '@/service/use-snippets' import { downloadBlob } from '@/utils/download' diff --git a/web/app/components/snippets/components/hooks/use-snippet-publish.ts b/web/app/components/snippets/components/hooks/use-snippet-publish.ts index caa91089acd..c7f15fdb04c 100644 --- a/web/app/components/snippets/components/hooks/use-snippet-publish.ts +++ b/web/app/components/snippets/components/hooks/use-snippet-publish.ts @@ -1,10 +1,10 @@ import type { Snippet as SnippetContract } from '@/types/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useChecklistBeforePublish } from '@/app/components/workflow/hooks/use-checklist' import { useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { usePublishSnippetWorkflowMutation } from '@/service/use-snippet-workflows' import { useResetWorkflowVersionHistory } from '@/service/use-workflow' diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index ed53c71ba0c..632f5a42a39 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -4,7 +4,6 @@ import type { WorkflowProps } from '@/app/components/workflow' import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store' import type { SnippetCanvasData, SnippetDetailPayload, SnippetInputField } from '@/models/snippet' import type { SnippetDraftSyncPayload } from '@/types/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { initialEdges, initialNodes } from '@/app/components/workflow/utils' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useSnippetDraftStore } from '../draft-store' import { useConfigsMap } from '../hooks/use-configs-map' diff --git a/web/app/components/snippets/components/snippet-run-panel.tsx b/web/app/components/snippets/components/snippet-run-panel.tsx index f6f1631ce09..46d08e7ee14 100644 --- a/web/app/components/snippets/components/snippet-run-panel.tsx +++ b/web/app/components/snippets/components/snippet-run-panel.tsx @@ -4,7 +4,6 @@ import type { InputForm } from '@/app/components/base/chat/chat/type' import type { InputVar as WorkflowInputVar } from '@/app/components/workflow/types' import type { SnippetInputField } from '@/models/snippet' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import copy from 'copy-to-clipboard' import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,6 +22,7 @@ import TracingPanel from '@/app/components/workflow/run/tracing-panel' import { useStore } from '@/app/components/workflow/store' import { InputVarType, WorkflowRunningStatus } from '@/app/components/workflow/types' import { formatWorkflowRunIdentifier } from '@/app/components/workflow/utils' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' type SnippetRunPanelProps = { diff --git a/web/app/components/snippets/components/snippet-sidebar.tsx b/web/app/components/snippets/components/snippet-sidebar.tsx index 567dbabe41e..31d20a7c7c4 100644 --- a/web/app/components/snippets/components/snippet-sidebar.tsx +++ b/web/app/components/snippets/components/snippet-sidebar.tsx @@ -3,7 +3,6 @@ import type { InputVar } from '@/app/components/workflow/types' import type { SnippetDetail, SnippetInputField } from '@/models/snippet' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { isEqual } from 'es-toolkit/predicate' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +11,7 @@ import SnippetInfoDropdown from '@/app/components/app-sidebar/snippet-info/dropd import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal' import Field from '@/app/components/workflow/nodes/_base/components/field' import VarList from '@/app/components/workflow/nodes/start/components/var-list' +import { toast } from '@/app/notifications' import { hasDuplicateStr } from '@/utils/var' import { SnippetPlaceholderIcon } from './snippet-placeholder-icon' diff --git a/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx b/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx index 351ba35ff06..77165047b25 100644 --- a/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx +++ b/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx @@ -2,9 +2,9 @@ import type { ReactElement } from 'react' import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { Edge, EnvironmentVariable, Node } from '@/app/components/workflow/types' import type { SnippetCanvasData, SnippetInputField } from '@/models/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { act, renderHook } from '@testing-library/react' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' import { useCreateSnippetFromSelection } from '../use-create-snippet-from-selection' @@ -26,7 +26,7 @@ vi.mock('../use-create-snippet', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() }, })) diff --git a/web/app/components/snippets/hooks/__tests__/use-create-snippet.spec.tsx b/web/app/components/snippets/hooks/__tests__/use-create-snippet.spec.tsx index 4afcc49a849..3626a2d65c5 100644 --- a/web/app/components/snippets/hooks/__tests__/use-create-snippet.spec.tsx +++ b/web/app/components/snippets/hooks/__tests__/use-create-snippet.spec.tsx @@ -51,7 +51,7 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (...args: unknown[]) => mockToastSuccess(...args), error: (...args: unknown[]) => mockToastError(...args), diff --git a/web/app/components/snippets/hooks/use-create-snippet-from-selection.tsx b/web/app/components/snippets/hooks/use-create-snippet-from-selection.tsx index 0d6a7e2274a..cbdcaf2a49d 100644 --- a/web/app/components/snippets/hooks/use-create-snippet-from-selection.tsx +++ b/web/app/components/snippets/hooks/use-create-snippet-from-selection.tsx @@ -6,13 +6,13 @@ import type { ValueSelector, } from '@/app/components/workflow/types' import type { SnippetCanvasData, SnippetInputField } from '@/models/snippet' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { getNodesBounds } from 'reactflow' import { CreateSnippetDialog } from '@/app/components/snippets/create-snippet-dialog' import { resolveLLMNodeModel } from '@/app/components/workflow/nodes/llm/utils' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { PipelineInputVarType } from '@/models/pipeline' import { useCreateSnippet } from './use-create-snippet' diff --git a/web/app/components/snippets/hooks/use-create-snippet.ts b/web/app/components/snippets/hooks/use-create-snippet.ts index 4d60e782db5..40e52393d00 100644 --- a/web/app/components/snippets/hooks/use-create-snippet.ts +++ b/web/app/components/snippets/hooks/use-create-snippet.ts @@ -1,8 +1,8 @@ import type { CreateSnippetDialogPayload } from '@/app/components/snippets/create-snippet-dialog' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter } from '@/next/navigation' import { consoleClient } from '@/service/console' diff --git a/web/app/components/snippets/import-snippet-dsl-dialog.tsx b/web/app/components/snippets/import-snippet-dsl-dialog.tsx index f6b2a0bbce2..6b113aed6d9 100644 --- a/web/app/components/snippets/import-snippet-dsl-dialog.tsx +++ b/web/app/components/snippets/import-snippet-dsl-dialog.tsx @@ -15,11 +15,11 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Input } from '@langgenius/dify-ui/input' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { Uploader } from '@/app/components/app/create-from-dsl-modal/uploader' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter } from '@/next/navigation' import { diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/get-schema.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/get-schema.spec.tsx index 11a3f9422b1..8bdbe680065 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/get-schema.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/get-schema.spec.tsx @@ -7,7 +7,7 @@ vi.mock('@/service/tools', () => ({ importSchemaFromURL: vi.fn(), })) const mockToastError = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx index c0015ce8647..ec7f5475b20 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx @@ -1,5 +1,4 @@ import type { ModalContextState } from '@/context/modal-context' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, @@ -10,6 +9,7 @@ import { import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types' +import { toast } from '@/app/notifications' import { parseParamsSchema } from '@/service/tools' import EditCustomCollectionModal from '../index' diff --git a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx index 979afbb3e8e..f168896963e 100644 --- a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx @@ -9,10 +9,10 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { importSchemaFromURL } from '@/service/tools' import examples from './examples' diff --git a/web/app/components/tools/edit-custom-collection-modal/index.tsx b/web/app/components/tools/edit-custom-collection-modal/index.tsx index 134854d3557..1a6898d9f95 100644 --- a/web/app/components/tools/edit-custom-collection-modal/index.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/index.tsx @@ -15,7 +15,6 @@ import { } from '@langgenius/dify-ui/drawer' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useDebounce, useGetState } from 'ahooks' import { produce } from 'immer' import * as React from 'react' @@ -24,6 +23,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import EmojiPicker from '@/app/components/base/emoji-picker' import LabelSelector from '@/app/components/tools/labels/selector' +import { toast } from '@/app/notifications' import { parseParamsSchema } from '@/service/tools' import { LinkExternal02 } from '../../base/icons/src/vender/line/general' import { AuthHeaderPrefix, AuthType } from '../types' diff --git a/web/app/components/tools/mcp/__tests__/modal.spec.tsx b/web/app/components/tools/mcp/__tests__/modal.spec.tsx index 7326cb5e713..d4181af317e 100644 --- a/web/app/components/tools/mcp/__tests__/modal.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/modal.spec.tsx @@ -13,7 +13,7 @@ vi.mock('@/service/common', () => ({ })) const mockToastError = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts b/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts index 3a91a5a8157..e49609fcc87 100644 --- a/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts +++ b/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts @@ -2,11 +2,11 @@ import type { HeaderItem } from '../headers-input' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { ToolWithProvider } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useMemo, useRef, useState } from 'react' import { getDomain } from 'tldts' import { v4 as uuid } from 'uuid' import { MCPAuthMethod } from '@/app/components/tools/types' +import { toast } from '@/app/notifications' import { uploadRemoteFileInfo } from '@/service/common' const DEFAULT_ICON = { type: 'emoji', icon: '🔗', background: '#6366F1' } diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index b25bcc86780..73d2f84a318 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -9,7 +9,6 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Input } from '@langgenius/dify-ui/input' import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useHover } from 'ahooks' import { useId } from 'react' @@ -17,6 +16,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' import { MCPAuthMethod } from '@/app/components/tools/types' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { shouldUseMcpIconForAppIcon } from '@/utils/mcp' import { isValidServerID, isValidUrl, useMCPModalForm } from './hooks/use-mcp-modal-form' diff --git a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx index e3889cf7d70..b47eaa71d1f 100644 --- a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx +++ b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx @@ -87,7 +87,7 @@ vi.mock('@/app/components/tools/edit-custom-collection-modal', () => ({ // Mock toast const mockToastSuccess = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (title: string) => mockToastSuccess(title), }, diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index a5bbc4f1576..d95b94d48f5 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -119,7 +119,7 @@ vi.mock('@/utils/var', () => ({ const mockToastSuccess = vi.hoisted(() => vi.fn()) const mockToastError = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/tools/provider/custom-create-card.tsx b/web/app/components/tools/provider/custom-create-card.tsx index cdd0a3f4869..a5df58614c5 100644 --- a/web/app/components/tools/provider/custom-create-card.tsx +++ b/web/app/components/tools/provider/custom-create-card.tsx @@ -1,11 +1,11 @@ 'use client' import type { CustomCollectionBackend } from '../types' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { createCustomCollection } from '@/service/tools' import CreateEntryCard from './create-entry-card' diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index 6678b7ac43e..fc3affa0b6b 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -28,7 +28,6 @@ import { } from '@langgenius/dify-ui/drawer' import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useQuery, useQueryClient } from '@tanstack/react-query' import * as React from 'react' @@ -45,6 +44,7 @@ import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-m import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool' +import { toast } from '@/app/notifications' import { useModalContext } from '@/context/modal-context' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { getPluginLanguage } from '@/i18n/metadata' diff --git a/web/app/components/tools/setting/build-in/__tests__/config-credentials.spec.tsx b/web/app/components/tools/setting/build-in/__tests__/config-credentials.spec.tsx index 082e4644423..df7eb3c225d 100644 --- a/web/app/components/tools/setting/build-in/__tests__/config-credentials.spec.tsx +++ b/web/app/components/tools/setting/build-in/__tests__/config-credentials.spec.tsx @@ -24,7 +24,7 @@ vi.mock('../../../utils/to-form-schema', () => ({ addDefaultValue: (value: Record, _schemas: unknown[]) => ({ ...value }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ default: { notify: vi.fn() }, })) diff --git a/web/app/components/tools/setting/build-in/config-credentials.tsx b/web/app/components/tools/setting/build-in/config-credentials.tsx index 632e3f39c55..0b28171b88c 100644 --- a/web/app/components/tools/setting/build-in/config-credentials.tsx +++ b/web/app/components/tools/setting/build-in/config-credentials.tsx @@ -14,7 +14,6 @@ import { DrawerTitle, DrawerViewport, } from '@langgenius/dify-ui/drawer' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' @@ -23,6 +22,7 @@ import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/gene import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' +import { toast } from '@/app/notifications' import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools' import { addDefaultValue, toolCredentialToFormSchemas } from '../../utils/to-form-schema' diff --git a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx index 96d372a0b21..17d7848d98b 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -46,7 +46,7 @@ vi.mock('../confirm-modal', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockToastNotify({ type: 'success', message }), error: (message: string) => mockToastNotify({ type: 'error', message }), diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index d5d70c448f4..5949c47dd1d 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -25,7 +25,7 @@ vi.mock('@/service/use-tools', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockToastNotify({ type: 'success', message }), error: (message: string) => mockToastNotify({ type: 'error', message }), diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 3fa0e39e3e4..46d78699ec9 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -8,9 +8,9 @@ import type { } from '@/app/components/tools/types' import type { InputVar } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { createWorkflowToolProvider, saveWorkflowToolProvider } from '@/service/tools' import { useInvalidateAllWorkflowTools, diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index b09d1659f95..d1cff525453 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -23,7 +23,6 @@ import { import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import * as React from 'react' import { useMemo, useState } from 'react' @@ -35,6 +34,7 @@ import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' import { normalizeWorkflowOutputName } from '@/app/components/workflow/utils/variable' +import { toast } from '@/app/notifications' import { buildWorkflowToolRequestPayload, getReservedWorkflowOutputParameters, diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 461a220099a..0bae47e95a5 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -28,7 +28,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 5dab50d4e32..4215944d38d 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -8,7 +8,6 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { CommonEdgeType, Node } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -28,6 +27,7 @@ import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/type import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, InputVarType, isTriggerNode } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useTheme from '@/hooks/use-theme' import { fetchAppDetail } from '@/service/apps' diff --git a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts index 88a0b0e1b35..0b522aa9292 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts @@ -9,7 +9,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options }), diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts index cb1519ec694..6aa174ba034 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts @@ -31,7 +31,7 @@ vi.mock('@/service/base', () => ({ handleStream: mockHandleStream, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/workflow-app/hooks/use-DSL.ts b/web/app/components/workflow-app/hooks/use-DSL.ts index 1cd434cabdb..e7e8f2bc56c 100644 --- a/web/app/components/workflow-app/hooks/use-DSL.ts +++ b/web/app/components/workflow-app/hooks/use-DSL.ts @@ -1,9 +1,9 @@ import type { useNodesSyncDraft } from './use-nodes-sync-draft' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { exportAppConfig } from '@/service/apps' import { fetchWorkflowDraft } from '@/service/workflow' diff --git a/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts b/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts index 07cceee9525..dd85dacec26 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run-utils.ts @@ -2,11 +2,11 @@ import type { Features as FeaturesData } from '@/app/components/base/features/ty import type { TriggerNodeType } from '@/app/components/workflow/types' import type { IOtherOptions } from '@/service/base' import type { VersionHistory } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { isInstalledAppPath } from '@/app/components/explore/installed-app/routes' import { TriggerType } from '@/app/components/workflow/header/test-run-menu' import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { handleStream, post } from '@/service/base' import { ContentType } from '@/service/fetch' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx index 6ae715e20e1..afc95679c55 100644 --- a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx @@ -1,7 +1,7 @@ import type { EventEmitter } from 'ahooks/lib/useEventEmitter' import type { EventEmitterValue } from '@/context/event-emitter' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { toast } from '@/app/notifications' import { EventEmitterContext } from '@/context/event-emitter' import { DSLImportStatus } from '@/models/app' import UpdateDSLModal from '../update-dsl-modal' @@ -21,7 +21,7 @@ vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader) const mockEmit = vi.fn() const mockEmitWorkflowUpdate = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), info: vi.fn(), diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index 43b122c1cf4..08cd0507ad5 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -192,7 +192,7 @@ vi.mock('@/service/workflow', () => ({ fetchAllInspectVars: vi.fn().mockResolvedValue([]), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: toastErrorMock, info: toastInfoMock, diff --git a/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx index 4141d717904..3d783632d49 100644 --- a/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx @@ -44,7 +44,7 @@ vi.mock('@/service/base', () => ({ request: (...args: unknown[]) => queryMocks.request(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => queryMocks.toastError(message), }, diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index 3faf345a598..4adb1c449f4 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -132,7 +132,7 @@ vi.mock('@/service/use-tools', () => ({ useInvalidateAllMCPTools: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/workflow/block-selector/agent-selector.tsx b/web/app/components/workflow/block-selector/agent-selector.tsx index fa1b05ee184..34ef1b2a066 100644 --- a/web/app/components/workflow/block-selector/agent-selector.tsx +++ b/web/app/components/workflow/block-selector/agent-selector.tsx @@ -14,7 +14,6 @@ import { ComboboxStatus, } from '@langgenius/dify-ui/combobox' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import { useState } from 'react' @@ -22,6 +21,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import Badge from '@/app/components/base/badge' import { useHooksStore } from '@/app/components/workflow/hooks-store' +import { toast } from '@/app/notifications' import Link from '@/next/link' import { consoleQuery } from '@/service/console' import BlockIcon from '../block-icon' diff --git a/web/app/components/workflow/block-selector/snippets/__tests__/use-insert-snippet.spec.tsx b/web/app/components/workflow/block-selector/snippets/__tests__/use-insert-snippet.spec.tsx index 47b9ce6a5bb..b4529fbb7b8 100644 --- a/web/app/components/workflow/block-selector/snippets/__tests__/use-insert-snippet.spec.tsx +++ b/web/app/components/workflow/block-selector/snippets/__tests__/use-insert-snippet.spec.tsx @@ -92,7 +92,7 @@ vi.mock('../../../hooks/use-workflow-history', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (...args: unknown[]) => mockToastError(...args), }, diff --git a/web/app/components/workflow/block-selector/snippets/use-insert-snippet.ts b/web/app/components/workflow/block-selector/snippets/use-insert-snippet.ts index 647596be8c7..da6239155ae 100644 --- a/web/app/components/workflow/block-selector/snippets/use-insert-snippet.ts +++ b/web/app/components/workflow/block-selector/snippets/use-insert-snippet.ts @@ -2,11 +2,11 @@ import type { AssignerNodeType } from '../../nodes/assigner/types' import type { IterationNodeType } from '../../nodes/iteration/types' import type { LoopNodeType } from '../../nodes/loop/types' import type { Edge, Node, OnNodeAdd, ValueSelector } from '../../types' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { useIncrementSnippetUseCountMutation } from '@/service/use-snippets' import { CUSTOM_EDGE, NESTED_ELEMENT_Z_INDEX, NODE_WIDTH_X_OFFSET, X_OFFSET } from '../../constants' diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index f5df700f804..fad33acdb8a 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -6,7 +6,6 @@ import type { CustomCollectionBackend } from '@/app/components/tools/types' import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useMemo, useState } from 'react' @@ -15,6 +14,7 @@ import SearchBox from '@/app/components/plugins/marketplace/search-box' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' import ToolBrowser from '@/app/components/workflow/block-selector/tool-browser' +import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' diff --git a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx index ff976be6a23..bd05f21d97e 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -86,7 +86,7 @@ vi.mock('../../collaboration/core/collaboration-manager', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/workflow/header/__tests__/run-mode.spec.tsx b/web/app/components/workflow/header/__tests__/run-mode.spec.tsx index 2e3c6720656..617181b4935 100644 --- a/web/app/components/workflow/header/__tests__/run-mode.spec.tsx +++ b/web/app/components/workflow/header/__tests__/run-mode.spec.tsx @@ -99,7 +99,7 @@ vi.mock('../../hooks/use-dynamic-test-run-options', () => ({ useDynamicTestRunOptions: () => mockDynamicOptions, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: (message: string) => mockNotify({ type: 'success', message }), error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/workflow/header/__tests__/view-history.spec.tsx b/web/app/components/workflow/header/__tests__/view-history.spec.tsx index b4663317088..4674bef1111 100644 --- a/web/app/components/workflow/header/__tests__/view-history.spec.tsx +++ b/web/app/components/workflow/header/__tests__/view-history.spec.tsx @@ -60,7 +60,7 @@ vi.mock('@/app/components/base/loading-placeholder', () => ({ LoadingPlaceholder: () =>
, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index d7b50c68324..22f52134284 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -1,6 +1,5 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiHistoryLine } from '@remixicon/react' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -8,6 +7,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import { deploymentEditionAtom } from '@/features/system-features/state' import useTheme from '@/hooks/use-theme' diff --git a/web/app/components/workflow/header/run-mode.tsx b/web/app/components/workflow/header/run-mode.tsx index a3b0d964b89..aacd0011d65 100644 --- a/web/app/components/workflow/header/run-mode.tsx +++ b/web/app/components/workflow/header/run-mode.tsx @@ -1,7 +1,6 @@ import type { TestRunMenuRef, TriggerOption } from './test-run-menu' import type { EventEmitterValue } from '@/context/event-emitter' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useHotkey } from '@tanstack/react-hotkeys' import * as React from 'react' import { useCallback, useRef } from 'react' @@ -12,6 +11,7 @@ import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd' import { useStore } from '@/app/components/workflow/store/workflow' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useWorkflowRunValidation } from '../hooks/use-checklist' import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options' diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index b4d5c20bec7..90234cf1bec 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -141,7 +141,7 @@ vi.mock('../index', () => ({ useNodesMetaData: () => ({ nodes: [], nodesMap: mockNodesMap }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx index be59307b7a1..2e1104f639b 100644 --- a/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-locate-node.spec.tsx @@ -12,7 +12,7 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => mockSearchParams, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(vi.fn(), { success: mockToastSuccess, error: mockToastError, diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 51ce2238b96..d1c7a83f489 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -19,7 +19,6 @@ import type { Emoji } from '@/app/components/tools/types' import type { AgentToolPublishIssue } from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog' import type { DataSet } from '@/models/datasets' import type { I18nKeysWithPrefix } from '@/types/i18n' -import { toast } from '@langgenius/dify-ui/toast' import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query' import isDeepEqual from 'fast-deep-equal' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -29,6 +28,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' +import { toast } from '@/app/notifications' import { MAX_TREE_DEPTH } from '@/config' import { useGetLanguage } from '@/context/i18n' import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions' diff --git a/web/app/components/workflow/hooks/use-locate-node.ts b/web/app/components/workflow/hooks/use-locate-node.ts index 28e97bbd2e5..4c501af7525 100644 --- a/web/app/components/workflow/hooks/use-locate-node.ts +++ b/web/app/components/workflow/hooks/use-locate-node.ts @@ -1,9 +1,9 @@ 'use client' import type { Node } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useSearchParams } from '@/next/navigation' import { scrollToWorkflowNode } from '../utils/node-navigation' import { useNodesInteractions } from './use-nodes-interactions' diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 94df08aeca4..1a52451f33f 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -13,12 +13,12 @@ import type { LoopNodeType } from '../nodes/loop/types' import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types' import type { Edge, Node, OnNodeAdd } from '../types' import type { RAGPipelineVariables } from '@/models/pipeline' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { getConnectedEdges, getOutgoers, useReactFlow } from 'reactflow' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { collaborationManager } from '../collaboration/core/collaboration-manager' import { diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index dfe147756b0..7371fd07bd1 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -19,7 +19,6 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useEventListener } from 'ahooks' import { isEqual } from 'es-toolkit/predicate' import { setAutoFreeze } from 'immer' @@ -47,6 +46,7 @@ import ReactFlow, { useReactFlow, useStoreApi, } from 'reactflow' +import { toast } from '@/app/notifications' import { IS_DEV } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' import { diff --git a/web/app/components/workflow/llm-environment-variable-value-field.tsx b/web/app/components/workflow/llm-environment-variable-value-field.tsx index a3fcecdefd3..26f4b52338d 100644 --- a/web/app/components/workflow/llm-environment-variable-value-field.tsx +++ b/web/app/components/workflow/llm-environment-variable-value-field.tsx @@ -5,10 +5,10 @@ import type { LLMCompletionParams, LLMEnvironmentVariableValue, } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { useTranslation } from 'react-i18next' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' +import { toast } from '@/app/notifications' export function LLMEnvironmentVariableValueField({ disabled = false, diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx index 697416791ae..d4b823c1c52 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx @@ -20,7 +20,7 @@ vi.mock('@/app/components/base/file-uploader/hooks', () => ({ useFileSizeLimit: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/index.spec.tsx index e3896e8db71..40568313442 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/index.spec.tsx @@ -1,11 +1,11 @@ import type { Props as FormProps } from '../form' import type { BeforeRunFormProps } from '../index' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import BeforeRunForm from '../index' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index d10a2f18eb0..fe961a5c8fd 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -8,13 +8,13 @@ import type { NodeRunningStatus } from '@/app/components/workflow/types' import type { HumanInputFormData } from '@/types/workflow' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import Split from '@/app/components/workflow/nodes/_base/components/split' import SingleRunForm from '@/app/components/workflow/nodes/human-input/components/single-run-form' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import Form from './form' import { buildSubmitData, diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/output-var-list.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/output-var-list.spec.tsx index d22420edf58..89b185ec1f2 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/output-var-list.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/output-var-list.spec.tsx @@ -16,7 +16,7 @@ vi.mock('../var-type-picker', () => ({ ), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() }, })) diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index 884511d6c22..d018e9a967f 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -3,12 +3,12 @@ import type { FC } from 'react' import type { OutputVar } from '../../../code/types' import type { VarType } from '@/app/components/workflow/types' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useDebounceFn } from 'ahooks' import { produce } from 'immer' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import RemoveButton from '../remove-button' import VarTypePicker from './var-type-picker' diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index a3556c18d7d..eaae1ba5fbf 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -4,7 +4,6 @@ import type { ValueSelector, Var, Variable } from '@/app/components/workflow/typ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useDebounceFn } from 'ahooks' import { produce } from 'immer' import * as React from 'react' @@ -13,6 +12,7 @@ import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { useKeyboardSortable } from '@/app/components/base/keyboard-sortable/use-keyboard-sortable' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { toast } from '@/app/notifications' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import RemoveButton from '../remove-button' import VarReferencePicker from './var-reference-picker' diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index df98ce1cee7..68cdff9c16e 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -2,7 +2,6 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c import type { Params as OneStepRunParams } from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' // import import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useEffect, useState } from 'react' import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params' @@ -27,6 +26,7 @@ import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/ import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { isSupportCustomRunForm } from '@/app/components/workflow/utils' +import { toast } from '@/app/notifications' import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' import { useInvalidLastRun } from '@/service/use-workflow' import { useWorkflowRunValidation } from '../../../../../hooks/use-checklist' diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts index 43d36571549..2eb7039455b 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts @@ -16,7 +16,7 @@ const mockWorkflowState = { setListeningTriggerIsAll: vi.fn(), setShowVariableInspectPanel: vi.fn(), } -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index feab26bb1df..508b9af846d 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -8,7 +8,6 @@ import type { } from '@/app/components/workflow/types' import type { FlowType } from '@/types/common' import type { NodeRunResult, NodeTracing } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { unionBy } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' import { produce } from 'immer' @@ -48,6 +47,7 @@ import { WorkflowRunningStatus, } from '@/app/components/workflow/types' import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { post, ssePost } from '@/service/base' import { diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx index a7d9b39c37c..3c5ce2de291 100644 --- a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx @@ -130,7 +130,7 @@ const mockSnippetComposerQueryOptions = vi.hoisted(() => ) const trackCreateAppMock = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/app/components/workflow/nodes/agent-v2/components/__tests__/save-inline-agent-to-roster-dialog.spec.tsx b/web/app/components/workflow/nodes/agent-v2/components/__tests__/save-inline-agent-to-roster-dialog.spec.tsx index ff949ad2267..2e8f247f8c2 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/__tests__/save-inline-agent-to-roster-dialog.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/__tests__/save-inline-agent-to-roster-dialog.spec.tsx @@ -20,7 +20,7 @@ vi.mock('@tanstack/react-query', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx index 42a260ca554..4b25a75e47c 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx @@ -16,7 +16,6 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAtom, useAtomValue, useStore as useJotaiStore, useSetAtom } from 'jotai' import { ScopeProvider } from 'jotai-scope' @@ -28,6 +27,7 @@ import { useDefaultModel, useTextGenerationCurrentProviderAndModelAndModelList, } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { toast } from '@/app/notifications' import { agentSoulConfigToFormState, formStateToAgentSoulConfig, diff --git a/web/app/components/workflow/nodes/agent-v2/hooks.ts b/web/app/components/workflow/nodes/agent-v2/hooks.ts index dde2e2cece6..9af87c29ae8 100644 --- a/web/app/components/workflow/nodes/agent-v2/hooks.ts +++ b/web/app/components/workflow/nodes/agent-v2/hooks.ts @@ -1,11 +1,11 @@ import type { AgentInlineBinding } from '../../block-selector/types' -import { toast } from '@langgenius/dify-ui/toast' import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useHooksStore } from '@/app/components/workflow/hooks-store' +import { toast } from '@/app/notifications' import { useInlineAgentScope } from '@/features/agent-v2/analytics' import { consoleQuery } from '@/service/console' import { FlowType } from '@/types/common' diff --git a/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx b/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx index f71af79238a..fecea7190e9 100644 --- a/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx +++ b/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { BodyPayloadValueType, BodyType } from '../../types' import CurlPanel from '../curl-panel' import * as curlParser from '../curl-parser' @@ -21,7 +21,7 @@ vi.mock('../../../../hooks/use-nodes-interactions', async (importOriginal) => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/workflow/nodes/http/components/curl-panel.tsx b/web/app/components/workflow/nodes/http/components/curl-panel.tsx index 58dfe54d9d8..e00b9ceeb5c 100644 --- a/web/app/components/workflow/nodes/http/components/curl-panel.tsx +++ b/web/app/components/workflow/nodes/http/components/curl-panel.tsx @@ -4,10 +4,10 @@ import type { HttpNodeType } from '../types' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useNodesInteractions } from '../../../hooks/use-nodes-interactions' import { parseCurl } from './curl-parser' diff --git a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx index 57c02dc38b5..ba3b2e83d7b 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx @@ -2,11 +2,11 @@ import type { ReactNode } from 'react' import type useConfig from '../hooks/use-config' import type { HumanInputNodeType } from '../types' import type { NodePanelProps } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import copy from 'copy-to-clipboard' import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import Panel from '../panel' import { DeliveryMethodType, UserActionButtonType } from '../types' @@ -24,7 +24,7 @@ vi.mock('copy-to-clipboard', () => ({ default: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), }, diff --git a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx index d453e1bb90f..5b7a5b38d39 100644 --- a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx @@ -26,7 +26,7 @@ vi.mock('@langgenius/dify-ui/input', () => ({ ), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ __esModule: true, toast: { success: (message: string) => mockNotify({ type: 'success', message }), diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx index e413ce6b83c..efa36e3d1ba 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx @@ -17,7 +17,7 @@ const render = (ui: ReactElement) => wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile), }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (message: string) => mockToastError(message), }, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx index 459f1d591f2..065fb5e9005 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx @@ -7,7 +7,6 @@ import type { } from '../../../types' import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types' import type { App, AppSSO } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -17,11 +16,12 @@ import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provide import { createHooksStore } from '@/app/components/workflow/hooks-store/store' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { render } from '@/test/console/render' import EmailSenderModal from '../test-email-sender' -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: { error: vi.fn(), diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx index 254e726cb3f..b7621769443 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx @@ -5,11 +5,11 @@ import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dif import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Switch } from '@langgenius/dify-ui/switch' -import { toast } from '@langgenius/dify-ui/toast' import { RiBugLine } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useCallback, useId, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import MailBodyInput from './mail-body-input' import Recipient from './recipient' diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx index d0bf3b339ab..81d9c56d4a2 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx @@ -4,7 +4,6 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightSFill } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { noop, unionBy } from 'es-toolkit/compat' @@ -23,6 +22,7 @@ import { isSystemVar, } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { InputVarType, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { currentWorkspaceAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useMembers } from '@/service/use-common' diff --git a/web/app/components/workflow/nodes/human-input/components/user-action.tsx b/web/app/components/workflow/nodes/human-input/components/user-action.tsx index 042d93f8881..42f5ca8f87f 100644 --- a/web/app/components/workflow/nodes/human-input/components/user-action.tsx +++ b/web/app/components/workflow/nodes/human-input/components/user-action.tsx @@ -2,9 +2,9 @@ import type { FC } from 'react' import type { UserAction } from '../types' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import ButtonStyleDropdown from './button-style-dropdown' const i18nPrefix = 'nodes.humanInput' diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx index a78f84327ea..3388fad52d5 100644 --- a/web/app/components/workflow/nodes/human-input/panel.tsx +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -4,7 +4,6 @@ import type { NodePanelProps, Var } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { RiAddLine, RiClipboardLine, @@ -23,6 +22,7 @@ import Split from '@/app/components/workflow/nodes/_base/components/split' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { useStore } from '@/app/components/workflow/store' import { VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import DeliveryMethod from './components/delivery-method' import FormContent from './components/form-content' import FormContentPreview from './components/form-content-preview' diff --git a/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx index c1040ccb82b..2801c5da385 100644 --- a/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx @@ -1,9 +1,9 @@ import type { IterationNodeType } from '../types' import type { PanelProps } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ErrorHandleMode } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { BlockEnum, VarType } from '../../../types' import Node from '../node' import Panel from '../panel' @@ -13,7 +13,7 @@ const mockHandleNodeAdd = vi.fn() const mockHandleNodeIterationRerender = vi.fn() let mockNodesReadOnly = false -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/nodes/iteration/node.tsx b/web/app/components/workflow/nodes/iteration/node.tsx index 2d0b1de10d4..a33763e65e3 100644 --- a/web/app/components/workflow/nodes/iteration/node.tsx +++ b/web/app/components/workflow/nodes/iteration/node.tsx @@ -2,10 +2,10 @@ import type { FC } from 'react' import type { IterationNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Background, useNodesInitialized, useViewport } from 'reactflow' +import { toast } from '@/app/notifications' import { IterationStartNodeDumb } from '../iteration-start' import AddBlock from './add-block' import { useNodeIterationInteractions } from './use-interactions' diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index 3b6800bcfa6..b326f7a8978 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -2,10 +2,10 @@ import type { SchemaRoot } from '../../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' +import { toast } from '@/app/notifications' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' import { Type } from '../../types' import { diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx index 2b2875f1123..e27b5e475c3 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx @@ -3,12 +3,12 @@ import type { SchemaRoot } from '../../../types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { CompletionParams, Model } from '@/types/app' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useAutoGenModel } from '@/app/components/app/configuration/config/auto-gen-model-storage' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { toast } from '@/app/notifications' import useTheme from '@/hooks/use-theme' import { useGenerateStructuredOutputRules } from '@/service/use-common' import { ModelModeType, Theme } from '@/types/app' diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 119a174f273..c34e7ac490d 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -1,10 +1,10 @@ import type { VisualEditorProps } from '.' import type { Field } from '../../../types' import type { EditData } from './edit-card' -import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { ArrayType, Type } from '../../../types' import { findPropertyWithPath } from '../../../utils' import { useMittContext } from './context' diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 17752a76797..97c66c5be0b 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -9,7 +9,6 @@ import { SelectItemText, SelectTrigger, } from '@langgenius/dify-ui/select' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useLayoutEffect, useRef } from 'react' @@ -20,6 +19,7 @@ import Field from '@/app/components/workflow/nodes/_base/components/field' import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch' import Split from '@/app/components/workflow/nodes/_base/components/split' import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { FlowType } from '@/types/common' import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx index a1f3a151ed3..0f0e8319f74 100644 --- a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx @@ -4,11 +4,11 @@ import type { } from '@/app/components/workflow/nodes/loop/types' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { RiDeleteBinLine } from '@remixicon/react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { ValueType, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import FormItem from './form-item' import InputModeSelect from './input-mode-selec' diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx index 562cf04b74c..b68861cbe8a 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx @@ -2,10 +2,10 @@ import type { ReactNode } from 'react' import type { Var } from '../../../types' import type { Param, ParameterExtractorNodeType } from '../types' import type { PanelProps } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { toast } from '@/app/notifications' import { AppModeEnum } from '@/types/app' import { BlockEnum } from '../../../types' import ExtractParameter from '../components/extract-parameter/list' @@ -20,7 +20,7 @@ const reasoningModeFunctionToolCallingLabel = 'workflow.nodes.parameterExtractor.reasoningModeFunctionToolCalling' const reasoningModePromptLabel = 'workflow.nodes.parameterExtractor.reasoningModePrompt' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/update.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/update.spec.tsx index 50b81a969fe..ca5a09b6177 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/update.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/update.spec.tsx @@ -1,11 +1,11 @@ import type { Param } from '../../../types' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { ParamType } from '../../../types' import Update from '../update' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx index ef467577f18..406d500232d 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx @@ -16,13 +16,13 @@ import { } from '@langgenius/dify-ui/select' import { Switch } from '@langgenius/dify-ui/switch' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Field from '@/app/components/app/configuration/config-var/config-modal/field' import ConfigSelect from '@/app/components/app/configuration/config-var/config-select' import { ChangeType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { checkKeys } from '@/utils/var' import { ParamType } from '../../types' diff --git a/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts index 27b0e224e80..4854693d88a 100644 --- a/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts @@ -38,7 +38,7 @@ vi.mock('../../../hooks/use-inspect-vars-crud', () => ({ default: (...args: unknown[]) => mockUseInspectVarsCrud(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ __esModule: true, toast: { error: (message: string) => mockNotify({ type: 'error', message }), diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index 12f2084c55b..de855b93a5f 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { InputVar, MoreInfo } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import * as React from 'react' import { useCallback, useMemo } from 'react' @@ -11,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { useKeyboardSortable } from '@/app/components/base/keyboard-sortable/use-keyboard-sortable' import { ChangeType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { hasDuplicateStr } from '@/utils/var' import VarItem from './var-item' diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index 6e1a9d127b2..35f2613f6cb 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -1,11 +1,11 @@ import type { StartNodeType } from './types' import type { InputVar, MoreInfo, ValueSelector } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { ChangeType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { hasDuplicateStr } from '@/utils/var' import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' diff --git a/web/app/components/workflow/nodes/tool/hooks/use-config.ts b/web/app/components/workflow/nodes/tool/hooks/use-config.ts index b3a0ee380b9..e12b95f93a0 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-config.ts @@ -1,6 +1,5 @@ import type { ToolNodeType, ToolVarInputs } from '../types' import type { InputVar } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { capitalize } from 'es-toolkit/string' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -13,6 +12,7 @@ import { } from '@/app/components/tools/utils/to-form-schema' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { updateBuiltInToolCredential } from '@/service/tools' import { useInvalidToolsByType } from '@/service/use-tools' import { useNodesReadOnly } from '../../../hooks/use-workflow' diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx index 3cf1530b045..afd7c069518 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx @@ -1,8 +1,8 @@ import type { WebhookTriggerNodeType } from '../types' -import { toast } from '@langgenius/dify-ui/toast' import { renderHook } from '@testing-library/react' import { useStore as useAppStore } from '@/app/components/app/store' import { BlockEnum, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { fetchWebhookUrl } from '@/service/apps' import { createNodeCrudModuleMock } from '../../__tests__/use-config-test-utils' import { useConfig } from '../use-config' @@ -21,7 +21,7 @@ vi.mock('react-i18next', async () => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ __esModule: true, toast: { error: vi.fn(), diff --git a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts index a87e165a5d6..be9cacbcb47 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts +++ b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts @@ -1,10 +1,10 @@ import type { SelectorParam } from 'i18next' import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' +import { toast } from '@/app/notifications' import { fetchWebhookUrl } from '@/service/apps' import { useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' import { diff --git a/web/app/components/workflow/nodes/variable-assigner/components/__tests__/var-group-item.spec.tsx b/web/app/components/workflow/nodes/variable-assigner/components/__tests__/var-group-item.spec.tsx index 63a20e43148..44a6dbf5e05 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/__tests__/var-group-item.spec.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/__tests__/var-group-item.spec.tsx @@ -1,12 +1,12 @@ import type { ComponentProps } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, screen } from '@testing-library/react' import { createNode, resetFixtureCounters } from '@/app/components/workflow/__tests__/fixtures' import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum, VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import VarGroupItem from '../var-group-item' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx index becc5948849..b275159186e 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx @@ -2,7 +2,6 @@ import type { ChangeEvent, FC } from 'react' import type { VarGroupItem as VarGroupItemType } from '../types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { RiDeleteBinLine } from '@remixicon/react' import { produce } from 'immer' import * as React from 'react' @@ -12,6 +11,7 @@ import { Folder } from '@/app/components/base/icons/src/vender/line/files' import Field from '@/app/components/workflow/nodes/_base/components/field' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { VarType } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import VarReferencePicker from '../../_base/components/variable/var-reference-picker' import VarList from '../components/var-list' diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/__tests__/hooks.spec.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/__tests__/hooks.spec.tsx index 93913ecdacf..354af797063 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/__tests__/hooks.spec.tsx @@ -67,7 +67,7 @@ vi.mock('lexical', () => ({ COMMAND_PRIORITY_LOW: 1, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, }, diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts index b4338e9c2bd..01efe72ac52 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts @@ -1,4 +1,3 @@ -import { toast } from '@langgenius/dify-ui/toast' import { TOGGLE_LINK_COMMAND } from '@lexical/link' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' @@ -6,6 +5,7 @@ import { escape } from 'es-toolkit/string' import { CLICK_COMMAND, COMMAND_PRIORITY_LOW } from 'lexical' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useNoteEditorStore } from '../../store' import { urlRegExp } from '../../utils' diff --git a/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx b/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx index e2e43322165..d9f340afe1d 100644 --- a/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx +++ b/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx @@ -17,7 +17,7 @@ vi.mock('next/navigation', () => ({ useParams: () => ({}), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: { success: (message: string) => mockNotify({ type: 'success', message }), diff --git a/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx b/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx index d140c428f4b..a9da75f2bcb 100644 --- a/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx +++ b/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx @@ -1,7 +1,6 @@ import type { Shape } from '../../store/workflow' import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import copy from 'copy-to-clipboard' @@ -12,6 +11,7 @@ import { } from '@/app/components/workflow/__tests__/fixtures' import { renderWorkflowComponent as renderWithWorkflowStore } from '@/app/components/workflow/__tests__/workflow-test-env' import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { submitHumanInputForm } from '@/service/workflow' import WorkflowPreview from '../workflow-preview' @@ -26,7 +26,7 @@ vi.mock('copy-to-clipboard', () => ({ default: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), }, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/object-value-item.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/object-value-item.spec.tsx index b9970fb8b3e..8ddac1da9fb 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/object-value-item.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/object-value-item.spec.tsx @@ -4,7 +4,7 @@ import ObjectValueItem, { DEFAULT_OBJECT_VALUE } from '../object-value-item' const toastError = vi.fn() -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: (...args: unknown[]) => toastError(...args), }, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx index 6c4c86c0300..7e9d78d2595 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx @@ -1,8 +1,8 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' +import { toast } from '@/app/notifications' import { ChatVarType } from '../../type' import VariableModal from '../variable-modal' @@ -10,7 +10,7 @@ vi.mock('uuid', () => ({ v4: () => 'generated-id', })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: { error: vi.fn(), diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx index 852aafe1c7e..ea6478c3b1a 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx @@ -1,13 +1,13 @@ 'use client' import type { FC } from 'react' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' +import { toast } from '@/app/notifications' type Props = Readonly<{ index: number diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 1f2c52fe097..f858105a892 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -2,12 +2,12 @@ import type { ChatVariableTranslator, ToastPayload } from './variable-modal.help import type { ConversationVariable } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import { useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import { useVariableModalState } from './use-variable-modal-state' import { diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts index 870188d243d..dad3dd59f05 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts @@ -28,7 +28,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: unknown[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts index 9d66f659e99..c6bb9811ad3 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts @@ -26,7 +26,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts index 9ad1186ac48..1fed2813aa6 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts @@ -28,7 +28,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts index 963032b24b8..8984593e15a 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts @@ -26,7 +26,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts index 16a6e1a6516..b35764f3b43 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts @@ -26,7 +26,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts index 42081331e7b..84289e0b540 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts @@ -26,7 +26,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts index 34321513036..470915484cc 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts @@ -25,7 +25,7 @@ vi.mock('@/service/workflow', () => ({ submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index da38afa91d0..ee6b1ccdc84 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -4,7 +4,6 @@ import type { ChatItem, ChatItemInTree, Inputs } from '@/app/components/base/cha import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { IOtherOptions } from '@/service/base' import type { ReasoningChunkResponse } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { uniqBy } from 'es-toolkit/compat' import { produce, setAutoFreeze } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -18,6 +17,7 @@ import { getProcessedFilesFromResponse, } from '@/app/components/base/file-uploader/utils' import { CUSTOM_NODE } from '@/app/components/workflow/constants' +import { toast } from '@/app/notifications' import { sseGet } from '@/service/base' import { useInvalidAllLastRun } from '@/service/use-workflow' import { submitHumanInputForm } from '@/service/workflow' diff --git a/web/app/components/workflow/panel/env-panel/__tests__/variable-modal.spec.tsx b/web/app/components/workflow/panel/env-panel/__tests__/variable-modal.spec.tsx index 7761de6c0e2..fbe216175da 100644 --- a/web/app/components/workflow/panel/env-panel/__tests__/variable-modal.spec.tsx +++ b/web/app/components/workflow/panel/env-panel/__tests__/variable-modal.spec.tsx @@ -5,7 +5,6 @@ import type { import type { ReactElement } from 'react' import type { Shape } from '@/app/components/workflow/store/workflow' import type { EnvironmentVariable } from '@/app/components/workflow/types' -import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { @@ -15,6 +14,7 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import { WorkflowContext } from '@/app/components/workflow/context' import { createWorkflowStore } from '@/app/components/workflow/store/workflow' +import { toast } from '@/app/notifications' import VariableModal from '../variable-modal' type MockModelParameterModalProps = { @@ -78,7 +78,7 @@ vi.mock( }), ) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/panel/env-panel/index.tsx b/web/app/components/workflow/panel/env-panel/index.tsx index 11ea558c0a0..f5dc59e6d3e 100644 --- a/web/app/components/workflow/panel/env-panel/index.tsx +++ b/web/app/components/workflow/panel/env-panel/index.tsx @@ -5,7 +5,6 @@ import type { LLMEnvironmentVariableValue, } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { cloneDeep } from 'es-toolkit/object' import { isEqual } from 'es-toolkit/predicate' @@ -24,6 +23,7 @@ import EnvItem from '@/app/components/workflow/panel/env-panel/env-item' import VariableTrigger from '@/app/components/workflow/panel/env-panel/variable-trigger' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { Resolution } from '@/types/app' import { fetchModelParameterRulesForModel, diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index ca843b1868e..c2619894737 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -6,7 +6,6 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { NumberField, NumberFieldGroup, NumberFieldInput } from '@langgenius/dify-ui/number-field' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { Infotip } from '@/app/components/base/infotip' import { isLLMEnvironmentVariableValue } from '@/app/components/workflow/llm-environment-variable' import { LLMEnvironmentVariableValueField } from '@/app/components/workflow/llm-environment-variable-value-field' import { useWorkflowStore } from '@/app/components/workflow/store' +import { toast } from '@/app/notifications' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' type ModalPropsType = { diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx index 15c525e4b39..73d1f9d611d 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx @@ -81,7 +81,7 @@ type MockVersionHistoryItemProps = { handleClickActionMenuItem: (operation: VersionHistoryContextMenuOptions) => void } -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: mockToast })) +vi.mock('@/app/notifications', () => ({ toast: mockToast })) vi.mock('@/service/use-workflow', () => ({ useDeleteWorkflow: () => ({ mutateAsync: vi.fn() }), diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index 2f484776560..a18a2c74035 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -2,7 +2,6 @@ import type { AppModeEnum } from '@/types/app' import type { VersionHistory } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import copy from 'copy-to-clipboard' import { useAtomValue } from 'jotai' @@ -13,6 +12,7 @@ import VersionInfoModal from '@/app/components/app/app-publisher/version-info-mo import Divider from '@/app/components/base/divider' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' +import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 6de42cc9282..538a9ec51f5 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -1,7 +1,6 @@ import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import copy from 'copy-to-clipboard' import { memo, useCallback, useEffect, useId, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -9,6 +8,7 @@ import { useStore as useReactFlowStore } from 'reactflow' import ReasoningPanel from '@/app/components/base/chat/chat/answer/reasoning-panel' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import ResizeHandle from '@/app/components/base/resize-handle' +import { toast } from '@/app/notifications' import { submitHumanInputForm } from '@/service/workflow' import { useWorkflowInteractions } from '../hooks/use-workflow-panel-interactions' import ResultPanel from '../run/result-panel' diff --git a/web/app/components/workflow/run/__tests__/index.spec.tsx b/web/app/components/workflow/run/__tests__/index.spec.tsx index 4b606a97d03..a54fb4d4b85 100644 --- a/web/app/components/workflow/run/__tests__/index.spec.tsx +++ b/web/app/components/workflow/run/__tests__/index.spec.tsx @@ -22,7 +22,7 @@ vi.mock('@/service/log', () => ({ fetchTracingList: (...args: unknown[]) => mockFetchTracingList(...args), })) -vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => ({ +vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: { error: (...args: unknown[]) => mockToastError(...args), diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 9e77b1d6b3b..c2593ee6af7 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -3,11 +3,11 @@ import type { FC } from 'react' import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracing } from '@/types/workflow' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { fetchRunDetail, fetchTracingList } from '@/service/log' import { useStore } from '../store' import OutputPanel from './output-panel' diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx index fa5f9992520..31901e4d222 100644 --- a/web/app/components/workflow/update-dsl-modal.tsx +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -4,7 +4,6 @@ import type { MouseEventHandler } from 'react' import type { DSLImportWarning } from '@/models/app' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { toast } from '@langgenius/dify-ui/toast' import { RiAlertFill, RiCloseLine, RiFileDownloadLine } from '@remixicon/react' import { memo, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +11,7 @@ import DSLImportWarningDescription from '@/app/components/app/create-from-dsl-mo import { Uploader } from '@/app/components/app/create-from-dsl-modal/uploader' import { useStore as useAppStore } from '@/app/components/app/store' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { importDSL, importDSLConfirm } from '@/service/apps' diff --git a/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx b/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx index 88f9db80ae8..6f4011ca420 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx +++ b/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx @@ -18,7 +18,7 @@ const toastMocks = vi.hoisted(() => ({ promise: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: Object.assign(toastMocks.call, { success: vi.fn(), error: vi.fn(), diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index 2055a0bd9d5..70a7ed20df0 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -23,7 +23,6 @@ import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { matchesKeyboardEvent } from '@tanstack/react-hotkeys' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' @@ -34,6 +33,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import WorkflowPreview from '@/app/components/workflow/workflow-preview' +import { toast } from '@/app/notifications' import { WORKFLOW_GENERATION_TIMEOUT_MS } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' diff --git a/web/app/education/apply/__tests__/page.spec.tsx b/web/app/education/apply/__tests__/page.spec.tsx index 123afe6c3d2..54d109126b7 100644 --- a/web/app/education/apply/__tests__/page.spec.tsx +++ b/web/app/education/apply/__tests__/page.spec.tsx @@ -1,8 +1,8 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' -import { toast } from '@langgenius/dify-ui/toast' import { cleanup, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import EducationApplyPage from '@/app/education/apply/application-form' +import { toast } from '@/app/notifications' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' diff --git a/web/app/education/apply/application-form.tsx b/web/app/education/apply/application-form.tsx index f6ff4e07daa..365daaf7afd 100644 --- a/web/app/education/apply/application-form.tsx +++ b/web/app/education/apply/application-form.tsx @@ -10,12 +10,12 @@ import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group' import { Field, FieldDescription, FieldItem, FieldLabel } from '@langgenius/dify-ui/field' import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset' import { Form } from '@langgenius/dify-ui/form' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { currentWorkspaceAtom, isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' diff --git a/web/app/init/InitPasswordPopup.tsx b/web/app/init/InitPasswordPopup.tsx index a7a24a6af7d..53659e9f882 100644 --- a/web/app/init/InitPasswordPopup.tsx +++ b/web/app/init/InitPasswordPopup.tsx @@ -1,10 +1,10 @@ 'use client' import type { InitValidateStatusResponse } from '@/models/common' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' import { fetchInitValidateStatus, initValidate } from '@/service/common' diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 935875cc1e2..dcb55ff789b 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,11 +1,11 @@ import type { ThemeProviderProps } from 'next-themes' import type { Metadata, Viewport } from '@/next' -import { ToastHost } from '@langgenius/dify-ui/toast' import { TooltipProvider } from '@langgenius/dify-ui/tooltip' import { HydrationBoundary } from '@tanstack/react-query' import { Provider as JotaiProvider } from 'jotai/react' import { ThemeProvider } from 'next-themes' import { NuqsAdapter } from 'nuqs/adapters/next/app' +import { AppToastHost } from '@/app/notifications/host' import { IS_PROD } from '@/config' import { getDatasetMap } from '@/env' import { SystemFeaturesBootstrapBoundary } from '@/features/system-features/bootstrap-boundary' @@ -81,7 +81,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo - + diff --git a/web/app/notifications/__tests__/copy.browser.spec.tsx b/web/app/notifications/__tests__/copy.browser.spec.tsx new file mode 100644 index 00000000000..8a23bfc892d --- /dev/null +++ b/web/app/notifications/__tests__/copy.browser.spec.tsx @@ -0,0 +1,204 @@ +import { createToast, createToastManager } from '@langgenius/dify-ui/toast' +import { page, userEvent } from 'vite-plus/test/browser' +import { render } from 'vitest-browser-react' +import { AppToastHost } from '../host' + +// Use the real clipboard adapter and browser focus order: the copy action must remain +// keyboard-operable, including when the adapter falls back to selection-based copying. +it('copies through the keyboard action and retains focus after feedback', async () => { + const description = Array.from( + { length: 20 }, + (_, index) => `Storage request ${index}: connection refused at storage.internal:19000`, + ).join('\n') + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined) + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render() + toast.error('Upload failed', { description }) + await expect.element(screen.getByText('Upload failed')).toBeInTheDocument() + await userEvent.keyboard('{F6}{Tab}{Tab}') + const action = screen.getByRole('button', { name: 'common.operation.copyErrorDetails' }) + await expect.element(action).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveFocus() + expect(writeText).toHaveBeenCalledExactlyOnceWith(`Upload failed\n${description}`) + } finally { + toast.dismiss() + writeText.mockRestore() + } +}) + +it('retains action focus when clipboard copying falls back to execCommand', async () => { + const writeText = vi + .spyOn(navigator.clipboard, 'writeText') + .mockRejectedValue(new Error('Denied')) + const execCommand = vi.spyOn(document, 'execCommand').mockReturnValue(true) + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render() + toast.error('Connection failed') + await expect.element(screen.getByText('Connection failed')).toBeInTheDocument() + await userEvent.keyboard('{F6}{Tab}{Tab}{Enter}') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveFocus() + expect(execCommand).toHaveBeenCalledExactlyOnceWith('copy') + } finally { + toast.dismiss() + writeText.mockRestore() + execCommand.mockRestore() + } +}) + +it('keeps an accessible focused action when copying fails and is retried with Space', async () => { + const writeText = vi + .spyOn(navigator.clipboard, 'writeText') + .mockRejectedValueOnce(new Error('Denied')) + .mockResolvedValue(undefined) + const execCommand = vi.spyOn(document, 'execCommand').mockReturnValue(false) + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render() + toast.error('Connection failed') + await expect.element(screen.getByText('Connection failed')).toBeInTheDocument() + await userEvent.keyboard('{F6}{Tab}{Tab}{Enter}') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copyErrorFailed' })) + .toHaveFocus() + await userEvent.keyboard(' ') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveFocus() + expect(writeText).toHaveBeenCalledTimes(2) + expect(writeText).toHaveBeenLastCalledWith('Connection failed') + } finally { + toast.dismiss() + writeText.mockRestore() + execCommand.mockRestore() + } +}) + +// Updating the card must not remount the focused copy button. +it('retains focus and copies current content after an error update', async () => { + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined) + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render() + const id = toast.error('First error') + await expect.element(screen.getByText('First error')).toBeInTheDocument() + await userEvent.keyboard('{F6}{Tab}{Tab}{Enter}') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveFocus() + toast.update(id, { title: 'Updated error' }) + await expect.element(screen.getByText('Updated error')).toBeInTheDocument() + await expect + .element(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + .toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveFocus() + expect(writeText).toHaveBeenLastCalledWith('Updated error') + } finally { + toast.dismiss() + writeText.mockRestore() + } +}) + +// Real CSS and pointer hit testing prove the hidden floating action can be discovered +// and clicked without overflowing the card or starting swipe-to-dismiss. +it.each([1280, 360])( + 'reveals a contained copy action on hover at %ipx and keeps the card open after copying', + async (width) => { + await page.viewport(width, 800) + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined) + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render( + <> + + + , + ) + const outside = screen.getByRole('button', { name: 'Outside notifications' }) + await userEvent.hover(outside) + toast.error('Upload failed', { + description: 'Storage connection refused. Please retry the request.', + }) + const card = screen.getByRole('dialog', { name: 'Upload failed' }) + const copy = screen.getByRole('button', { name: 'common.operation.copyErrorDetails' }) + await expect.element(copy).toHaveStyle({ opacity: '0' }) + await userEvent.hover(card) + await expect.element(copy).toHaveStyle({ opacity: '1' }) + const cardBounds = card.element().getBoundingClientRect() + const buttonBounds = copy.element().getBoundingClientRect() + expect(buttonBounds.left).toBeGreaterThanOrEqual(cardBounds.left) + expect(buttonBounds.right).toBeLessThanOrEqual(cardBounds.right) + expect(buttonBounds.top).toBeGreaterThanOrEqual(cardBounds.top) + expect(buttonBounds.bottom).toBeLessThanOrEqual(cardBounds.bottom) + await userEvent.click(copy) + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveStyle({ opacity: '1' }) + expect(writeText).toHaveBeenCalledExactlyOnceWith( + 'Upload failed\nStorage connection refused. Please retry the request.', + ) + await expect.element(card).toBeInTheDocument() + await userEvent.hover(outside) + await expect + .element(screen.getByRole('button', { name: 'common.operation.copied' })) + .toHaveStyle({ opacity: '0' }) + await userEvent.hover(card) + await userEvent.click(screen.getByRole('button', { name: 'Close notification' })) + await expect.element(card).not.toBeInTheDocument() + } finally { + toast.dismiss() + writeText.mockRestore() + await page.viewport(1280, 720) + } + }, +) + +it('reveals the action for keyboard focus, supports reverse tabbing, and restores focus on Escape', async () => { + const manager = createToastManager() + const toast = createToast(manager) + try { + const screen = await render( + <> + + + , + ) + const target = screen.getByRole('button', { name: 'Return target' }) + await userEvent.click(target) + toast.error('Keyboard error') + const card = screen.getByRole('dialog', { name: 'Keyboard error' }) + const copy = screen.getByRole('button', { name: 'common.operation.copyErrorDetails' }) + await expect.element(copy).toHaveStyle({ opacity: '0' }) + await userEvent.keyboard('{F6}{Tab}') + await expect.element(card).toHaveFocus() + await expect.element(copy).toHaveStyle({ opacity: '1' }) + await userEvent.keyboard('{Tab}') + await expect.element(copy).toHaveFocus() + await expect.element(copy).toHaveStyle({ opacity: '1' }) + await userEvent.keyboard('{Tab}') + await expect.element(screen.getByRole('button', { name: 'Close notification' })).toHaveFocus() + await userEvent.keyboard('{Shift>}{Tab}{/Shift}') + await expect.element(copy).toHaveFocus() + await userEvent.keyboard('{Escape}') + await expect.element(card).not.toBeInTheDocument() + await expect.element(target).toHaveFocus() + } finally { + toast.dismiss() + } +}) diff --git a/web/app/notifications/__tests__/toast.spec.tsx b/web/app/notifications/__tests__/toast.spec.tsx new file mode 100644 index 00000000000..97bfe785c92 --- /dev/null +++ b/web/app/notifications/__tests__/toast.spec.tsx @@ -0,0 +1,204 @@ +import { createToast, createToastManager } from '@langgenius/dify-ui/toast' +import { act, render, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { AppToastHost } from '../host' + +const copy = vi.fn() + +function setup() { + const manager = createToastManager() + const toast = createToast(manager) + render() + const user = userEvent.setup() + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: copy } }) + return { toast, user } +} + +beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(document, 'execCommand', { configurable: true, value: vi.fn(() => false) }) + vi.mocked(copy).mockResolvedValue(undefined) +}) + +it('copies the complete error text and shows feedback in the existing action', async () => { + const { toast, user } = setup() + act(() => { + toast.error('Upload failed', { description: 'Storage unavailable' }) + }) + await user.click(await screen.findByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledExactlyOnceWith('Upload failed\nStorage unavailable') + expect(await screen.findByRole('button', { name: 'common.operation.copied' })).toBeInTheDocument() + expect(screen.getAllByRole('dialog')).toHaveLength(1) +}) + +it('preserves caller actions and does not extract rich content', async () => { + const { toast, user } = setup() + const retry = vi.fn() + act(() => { + toast.error('Retryable', { actionProps: { children: 'Retry', onClick: retry } }) + toast.error(Rich error) + }) + await user.click(screen.getByRole('button', { name: 'Retry' })) + expect(retry).toHaveBeenCalledOnce() + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledWith('Retryable') + expect( + within(screen.getByRole('dialog', { name: 'Rich error' })).queryByRole('button', { + name: 'common.operation.copyErrorDetails', + }), + ).not.toBeInTheDocument() +}) + +it('updates copy content and removes generated actions when the tone changes', async () => { + const { toast, user } = setup() + let id = '' + act(() => { + id = toast('First error', { type: 'error' }) + }) + act(() => { + toast.update(id, { title: 'Updated error' }) + }) + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledWith('Updated error') + act(() => { + toast.update(id, { type: 'success', title: 'Recovered' }) + }) + expect(screen.queryByRole('button', { name: 'common.operation.copied' })).not.toBeInTheDocument() +}) + +it('does not let an old clipboard result change an upserted error', async () => { + let finish!: () => void + vi.mocked(copy).mockReturnValueOnce( + new Promise((resolve) => { + finish = resolve + }), + ) + const { toast, user } = setup() + act(() => { + toast.error('Old error', { id: 'request' }) + }) + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + act(() => { + toast.error('New error', { id: 'request' }) + }) + await act(async () => { + finish() + }) + expect(screen.queryByRole('button', { name: 'common.operation.copied' })).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenLastCalledWith('New error') +}) + +it('allows retrying a rejected clipboard operation without another toast', async () => { + vi.mocked(copy).mockRejectedValueOnce(new Error('Denied')) + const { toast, user } = setup() + act(() => { + toast.error('Network error') + }) + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + await user.click(await screen.findByRole('button', { name: 'common.operation.copyErrorFailed' })) + expect(await screen.findByRole('button', { name: 'common.operation.copied' })).toBeInTheDocument() + expect(screen.getAllByRole('dialog')).toHaveLength(1) +}) + +it('decorates a rejected promise and preserves the original rejection', async () => { + const { toast, user } = setup() + const error = new Error('Export failed') + await act(async () => { + await expect( + toast.promise(Promise.reject(error), { + loading: 'Exporting', + success: 'Exported', + error: (reason) => ({ title: (reason as Error).message, description: 'Try again later' }), + }), + ).rejects.toBe(error) + }) + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledWith('Export failed\nTry again later') +}) + +it('keeps scoped managers isolated from each other', async () => { + const first = createToastManager() + const second = createToastManager() + const firstToast = createToast(first) + const secondToast = createToast(second) + render( + <> + + + , + ) + act(() => { + firstToast.error('First') + secondToast.error('Second') + }) + const regions = screen.getAllByRole('region', { name: 'Notifications' }) + expect(within(regions[0]!).getByRole('dialog', { name: 'First' })).toBeInTheDocument() + expect(within(regions[0]!).queryByText('Second')).not.toBeInTheDocument() + act(() => { + firstToast.dismiss() + }) + await waitFor(() => + expect(screen.queryByRole('dialog', { name: 'First' })).not.toBeInTheDocument(), + ) + expect(screen.getByRole('dialog', { name: 'Second' })).toBeInTheDocument() +}) + +it('copies the retained description after a same-id title-only upsert', async () => { + const { toast, user } = setup() + act(() => { + toast.error('First', { id: 'request', description: 'Diagnostic details' }) + }) + act(() => { + toast.error('Second', { id: 'request' }) + }) + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledWith('Second\nDiagnostic details') +}) + +it('retains a caller action across upserts and promise failure', async () => { + const { toast, user } = setup() + const retry = vi.fn() + const actionProps = { children: 'Retry', onClick: retry } + act(() => { + toast.error('First', { id: 'request', actionProps }) + }) + act(() => { + toast.error('Second', { id: 'request' }) + }) + await user.click(screen.getByRole('button', { name: 'Retry' })) + expect(retry).toHaveBeenCalledOnce() + await act(async () => { + await toast + .promise(Promise.reject(new Error('Failed')), { + loading: { title: 'Loading', actionProps: { children: 'Details', onClick: retry } }, + success: 'Done', + error: 'Failed', + }) + .catch(() => {}) + }) + const failedToast = screen.getByRole('dialog', { name: 'Loading' }) + await user.click( + within(failedToast).getByRole('button', { name: 'common.operation.copyErrorDetails' }), + ) + expect(copy).toHaveBeenCalledWith('Loading\nFailed') + await user.click(screen.getByRole('button', { name: 'Details' })) + expect(retry).toHaveBeenCalledTimes(2) +}) + +it('copies the resolved string description without retaining loading text', async () => { + const { toast, user } = setup() + await act(async () => { + await toast + .promise(Promise.reject(new Error('Failed')), { + loading: 'Loading', + success: 'Done', + error: () => 'Failed', + }) + .catch(() => {}) + }) + expect(screen.queryByText('Loading')).not.toBeInTheDocument() + expect(screen.queryByRole('heading')).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.operation.copyErrorDetails' })) + expect(copy).toHaveBeenCalledWith('Failed') +}) diff --git a/web/app/notifications/copy-error-action.tsx b/web/app/notifications/copy-error-action.tsx new file mode 100644 index 00000000000..8d649217755 --- /dev/null +++ b/web/app/notifications/copy-error-action.tsx @@ -0,0 +1,50 @@ +'use client' + +import { IconButton } from '@langgenius/dify-ui/icon-button' +import { useClipboard } from 'foxact/use-clipboard' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +export function CopyErrorAction({ text }: { text: string }) { + const { t } = useTranslation() + const { copy, copied, error, reset } = useClipboard({ timeout: 2000 }) + useEffect(() => reset, [reset]) + const [feedbackText, setFeedbackText] = useState(null) + const showCopied = feedbackText === text && copied + const showError = feedbackText === text && error + const label = showCopied + ? t(($) => $['operation.copied'], { ns: 'common' }) + : showError + ? t(($) => $['operation.copyErrorFailed'], { ns: 'common' }) + : t(($) => $['operation.copyErrorDetails'], { ns: 'common' }) + + return ( + <> + { + reset() + setFeedbackText(null) + void copy(text).then(() => setFeedbackText(text)) + }} + > + + + {showCopied || showError ? label : ''} + + + ) +} diff --git a/web/app/notifications/host.tsx b/web/app/notifications/host.tsx new file mode 100644 index 00000000000..2b337465aef --- /dev/null +++ b/web/app/notifications/host.tsx @@ -0,0 +1,51 @@ +'use client' + +import type { ToastManager, ToastViewportProps } from '@langgenius/dify-ui/toast' +import { + ToastCard, + ToastPortal, + ToastProvider, + ToastViewport, + useToastManager, +} from '@langgenius/dify-ui/toast' +import { manager } from '.' +import { CopyErrorAction } from './copy-error-action' + +export type AppToastHostProps = { + manager?: ToastManager + timeout?: number + limit?: number + offset?: ToastViewportProps['offset'] +} + +function Notifications() { + const { toasts } = useToastManager>() + return toasts.map((item) => { + const parts = [item.title, item.description] + const text = parts.every((part) => part == null || typeof part === 'string') + ? parts.filter(Boolean).join('\n') + : '' + return ( + + {item.type === 'error' && text && } + + ) + }) +} + +export function AppToastHost({ + manager: toastManager = manager, + timeout = 5000, + limit = 3, + offset, +}: AppToastHostProps) { + return ( + + + + + + + + ) +} diff --git a/web/app/notifications/index.tsx b/web/app/notifications/index.tsx new file mode 100644 index 00000000000..330728f392a --- /dev/null +++ b/web/app/notifications/index.tsx @@ -0,0 +1,6 @@ +'use client' + +import { createToast, createToastManager } from '@langgenius/dify-ui/toast' + +export const manager = createToastManager() +export const toast = createToast(manager) diff --git a/web/app/notifications/notifications.stories.tsx b/web/app/notifications/notifications.stories.tsx new file mode 100644 index 00000000000..41f76b18d1d --- /dev/null +++ b/web/app/notifications/notifications.stories.tsx @@ -0,0 +1,599 @@ +import type { ToastApi, ToastManager, ToastViewportProps } from '@langgenius/dify-ui/toast' +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { + createToast, + createToastManager, + ToastCard, + ToastPortal, + ToastProvider, + ToastViewport, + useToastManager, +} from '@langgenius/dify-ui/toast' +import { useState } from 'react' +import { expect, spyOn, userEvent, within } from 'storybook/test' +import { toast as globalToast } from '.' +import { AppToastHost } from './host' + +function ToastCards() { + const { toasts } = useToastManager>() + return toasts.map((item) => ) +} + +function ExampleToastHost({ + manager, + timeout, + limit, + offset, +}: { + manager: ToastManager + timeout?: number + limit?: number + offset?: ToastViewportProps['offset'] +}) { + return ( + + + + + + + + ) +} + +type Scenario = { label: string; run: (toast: ToastApi) => void } +type PlaygroundProps = { + description: string + scenarios: Scenario[] + primitive?: boolean + timeout?: number + limit?: number +} + +const diagnostic = + 'Failed to install plugin langgenius/openai:0.2.7: failed to remap assets and persist provider icon because the object storage service rejected the upload after all retry attempts.' +const details = [ + 'operation error S3: PutObject, exceeded maximum number of attempts, 3, StatusCode: 0, RequestID: req-78bb251e-ec45-4974-b8a8-2c70bd8b9e12, HostID: storage-internal-03.', + 'PUT "https://storage.example.com/plugin-assets/1bd032bb73218a5d141b80cab7111/providers/openai/icon-large-en_US.svg?x-id=PutObject": dial tcp 192.168.0.200:19000: connect: connection refused.', + 'Unable to store asset icon_large_en_US. The plugin package was downloaded and validated, but installation could not complete because the configured S3 endpoint is unreachable from the plugin daemon.', + 'Check the storage endpoint, bucket permissions, service credentials, and network routing before retrying. This diagnostic intentionally includes enough detail to reproduce the original installation failure.', +].join('\n') +const buttonClassName = + 'rounded-lg border border-divider-regular bg-components-button-secondary-bg px-3 py-2 text-sm text-text-primary hover:bg-state-base-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-state-accent-solid' + +function Playground({ + description, + scenarios, + primitive = false, + timeout = 0, + limit = 3, +}: PlaygroundProps) { + const [{ manager, toast }] = useState(() => { + const manager = createToastManager() + return { manager, toast: createToast(manager) } + }) + return ( +
+

{description}

+

+ F6 enters notifications. Tab reaches the card, action, and close button. Use Enter or Space + to activate. Clear notifications between cases. Switch the theme or resize the preview to + inspect layout. +

+
+ {scenarios.map(({ label, run }) => ( + + ))} + +
+ + {primitive ? ( + + ) : ( + + )} +
+ ) +} + +const meta = { + title: 'Application/Notifications', + component: Playground, + parameters: { layout: 'fullscreen' }, + argTypes: { scenarios: { control: false, table: { disable: true } } }, + args: { description: '', scenarios: [] }, + tags: ['!autodocs'], +} satisfies Meta +export default meta +type Story = StoryObj + +export const Tones: Story = { + args: { + description: + 'Only error notifications gain an automatic copy action. Plain, success, warning, and info retain the primitive appearance.', + scenarios: [ + { label: 'Plain', run: (toast) => toast('New activity') }, + { + label: 'Success', + run: (toast) => toast.success('Saved', { description: 'Your changes are available.' }), + }, + { label: 'Warning', run: (toast) => toast.warning('Storage almost full') }, + { label: 'Info', run: (toast) => toast.info('Invitation sent') }, + { label: 'Error', run: (toast) => toast.error('Upload failed', { description: details }) }, + { + label: 'Error via callable API', + run: (toast) => toast('Connection failed', { type: 'error' }), + }, + ], + }, +} + +export const PrimitiveParity: Story = { + args: { + primitive: true, + description: + 'Direct Dify UI factory: an error still has only its close control. Actions remain caller-owned and do not dismiss automatically.', + scenarios: [ + { + label: 'Primitive error', + run: (toast) => toast.error(diagnostic, { description: details }), + }, + { + label: 'Primitive action', + run: (toast) => + toast.info('Review available', { + actionProps: { children: 'Review', onClick: () => toast.success('Review opened') }, + }), + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await userEvent.click(canvas.getByRole('button', { name: /^Primitive error$/ })) + const body = within(canvasElement.ownerDocument.body) + await expect(await body.findByRole('dialog', { name: diagnostic })).toBeVisible() + await expect(body.queryByRole('button', { name: 'Copy error details' })).not.toBeInTheDocument() + }, +} + +export const ErrorContent: Story = { + args: { + description: + 'Copy preserves the full plain-text title and description, separated by a newline. Empty content and React nodes do not generate a misleading copy action.', + scenarios: [ + { label: 'Title only', run: (toast) => toast.error('Connection failed') }, + { label: 'Description only', run: (toast) => toast.error(null, { description: details }) }, + { + label: 'Empty title', + run: (toast) => toast.error('', { description: 'Useful error detail' }), + }, + { label: 'Empty content', run: (toast) => toast.error('') }, + { + label: 'Unicode and multiline', + run: (toast) => + toast.error('上传失败 / فشل التحميل / 🚫', { + description: '第一行\nSecond line\n & "quoted"', + }), + }, + { + label: 'Rich title', + run: (toast) => + toast.error(Permission denied, { + description: 'Ask your workspace administrator.', + }), + }, + { + label: 'Rich description', + run: (toast) => + toast.error('Access denied', { + description: View permission help, + }), + }, + { label: 'Numeric content', run: (toast) => toast.error(503) }, + ], + }, +} + +export const LongContent: Story = { + args: { + description: + 'Inspect long diagnostics, unbroken tokens, and many lines at narrow widths and both themes. There is intentionally no new maximum-height policy; very long cards expose the existing primitive layout limit.', + scenarios: [ + { + label: 'Realistic diagnostic', + run: (toast) => toast.error(diagnostic, { description: details }), + }, + { + label: 'Unbroken token', + run: (toast) => + toast.error('Invalid response', { description: '0123456789abcdef'.repeat(30) }), + }, + { label: 'Very long title', run: (toast) => toast.error(diagnostic.repeat(8)) }, + { + label: 'Many lines', + run: (toast) => + toast.error('Validation errors', { + description: Array.from( + { length: 30 }, + (_, i) => `Field ${i + 1}: required value is missing`, + ).join('\n'), + }), + }, + ], + }, +} + +export const CallerActions: Story = { + args: { + description: + 'Caller actions remain available alongside copying for text errors, including repeated IDs and updates. Rich content retains its caller action without extracting text.', + scenarios: [ + { + label: 'Custom retry', + run: (toast) => + toast.error('Retryable failure', { + id: 'custom', + actionProps: { + children: 'Retry request', + onClick: () => + toast.update('custom', { + title: 'Recovered', + type: 'success', + actionProps: undefined, + }), + }, + }), + }, + { + label: 'Update custom title', + run: (toast) => toast.error('Still unavailable', { id: 'custom' }), + }, + { + label: 'Remove custom action', + run: (toast) => toast.update('custom', { actionProps: undefined }), + }, + { + label: 'Rich error with explicit action', + run: (toast) => + toast.error(Session expired, { + actionProps: { + children: 'Sign in again', + onClick: () => toast.success('Sign-in action invoked'), + }, + }), + }, + ], + }, +} + +export const UpdatesAndRepeatedIds: Story = { + args: { + description: + 'Run these controls in sequence. A repeated ID remains one card and retains omitted description fields. Copy must follow the effective content; success removes the generated action.', + scenarios: [ + { + label: '1. Create error', + run: (toast) => + toast.error('Original title', { id: 'upsert', description: 'Retained description' }), + }, + { + label: '2. Replace title via same ID', + run: (toast) => toast.error('Updated title', { id: 'upsert' }), + }, + { + label: '3. Replace description', + run: (toast) => toast.update('upsert', { description: 'New description' }), + }, + { + label: '4. Clear description', + run: (toast) => toast.update('upsert', { description: null }), + }, + { + label: '5. Recover', + run: (toast) => toast.update('upsert', { title: 'Recovered', type: 'success' }), + }, + { + label: '6. Fail again', + run: (toast) => toast.update('upsert', { title: 'Failed again', type: 'error' }), + }, + { label: 'Dismiss one', run: (toast) => toast.dismiss('upsert') }, + { + label: 'Update absent ID', + run: (toast) => + toast.update('does-not-exist', { title: 'Should not appear', type: 'error' }), + }, + ], + }, +} + +export const PromiseResults: Story = { + args: { + description: + 'A loading toast transitions in place. Error callbacks and string errors get copy actions; caller actions and inherited loading descriptions remain intact. Rejections are handled by these demo triggers.', + scenarios: [ + { + label: 'Resolve', + run: (toast) => { + void toast.promise( + new Promise((resolve) => setTimeout(() => resolve('Upload'), 1200)), + { + loading: 'Uploading…', + success: (value) => `${value} complete`, + error: 'Upload failed', + }, + ) + }, + }, + { + label: 'Reject string', + run: (toast) => { + void toast + .promise( + new Promise((_, reject) => + setTimeout(() => reject(new Error('Network failed')), 1200), + ), + { loading: 'Connecting…', success: 'Connected', error: 'Connection failed' }, + ) + .catch(() => {}) + }, + }, + { + label: 'Reject with details', + run: (toast) => { + void toast + .promise(Promise.reject(new Error('Connection refused')), { + loading: 'Uploading…', + success: 'Uploaded', + error: (error) => ({ title: 'Upload failed', description: String(error) }), + }) + .catch(() => {}) + }, + }, + { + label: 'Retain loading description', + run: (toast) => { + void toast + .promise(Promise.reject(new Error('Denied')), { + loading: { title: 'Uploading…', description: 'File: document.pdf' }, + success: 'Uploaded', + error: 'Permission denied', + }) + .catch(() => {}) + }, + }, + { + label: 'Explicit promise action', + run: (toast) => { + void toast + .promise(Promise.reject(new Error('Denied')), { + loading: 'Checking…', + success: 'Ready', + error: { + title: 'Access denied', + actionProps: { + children: 'Request access', + onClick: () => toast.info('Access requested'), + }, + }, + }) + .catch(() => {}) + }, + }, + ], + }, +} + +export const StackAndDismissal: Story = { + args: { + description: + 'Limit three visible cards. Hover or focus expands the stack. Close individual cards, dismiss all, or swipe upward/right; a downward gesture should not dismiss this top-anchored stack.', + scenarios: [ + { + label: 'Burst of six', + run: (toast) => { + for (let i = 1; i <= 6; i++) + toast.error(`Failure ${i}`, { description: `Diagnostic ${i}` }) + }, + }, + { + label: 'Mixed stack', + run: (toast) => { + toast.success('Saved') + toast.warning('Quota low') + toast.error('Sync failed', { description: details }) + }, + }, + { + label: 'Persistent error', + run: (toast) => toast.error('Still needs attention', { timeout: 0 }), + }, + ], + }, +} + +export const AutoDismissAndPause: Story = { + args: { + timeout: 3000, + description: + 'Default timeout is three seconds in this story. Hovering or keyboard focus pauses dismissal. Copy feedback must not extend the timeout or dismiss the card. Persistent errors remain until closed.', + scenarios: [ + { label: 'Timed error', run: (toast) => toast.error('Expires after three seconds') }, + { + label: 'Persistent error', + run: (toast) => toast.error('Persistent error', { timeout: 0 }), + }, + { label: 'Short success', run: (toast) => toast.success('Saved briefly', { timeout: 1000 }) }, + ], + }, +} + +export const CopyFeedback: Story = { + args: { + description: + 'Real clipboard integration: copy, paste into the field, and compare all lines. The action shows Copied for two seconds and remains focused. Repeated copying stays within the same toast.', + scenarios: [ + { + label: 'Show copyable error', + run: (toast) => toast.error(diagnostic, { description: details }), + }, + ], + }, +} + +export const ClipboardFailure: Story = { + ...CopyFeedback, + args: { + ...CopyFeedback.args, + description: + 'Simulated clipboard and fallback rejection. Copy reports failure in the same action; retry remains available and no secondary error toast appears.', + }, + beforeEach: () => { + const clipboard = spyOn(navigator.clipboard, 'writeText').mockRejectedValue( + new Error('Permission denied'), + ) + const fallback = spyOn(document, 'execCommand').mockReturnValue(false) + return () => { + clipboard.mockRestore() + fallback.mockRestore() + } + }, +} + +export const ClipboardFallback: Story = { + ...CopyFeedback, + args: { + ...CopyFeedback.args, + description: + 'Simulated Clipboard API rejection. The real foxact selection-based fallback runs with execCommand success simulated. Check that the copy button retains keyboard focus.', + }, + beforeEach: () => { + const clipboard = spyOn(navigator.clipboard, 'writeText').mockRejectedValue( + new Error('Permission denied'), + ) + const fallback = spyOn(document, 'execCommand').mockReturnValue(true) + return () => { + clipboard.mockRestore() + fallback.mockRestore() + } + }, +} + +export const PendingCopyAndReplacement: Story = { + args: { + description: + 'Clipboard completion is delayed two seconds. Click copy repeatedly, then replace or dismiss the card while pending. The new card must not show stale Copied feedback.', + scenarios: [ + { + label: 'Original error', + run: (toast) => toast.error('Original diagnostic', { id: 'pending' }), + }, + { + label: 'Replace while copying', + run: (toast) => toast.error('Replacement diagnostic', { id: 'pending' }), + }, + { label: 'Dismiss while copying', run: (toast) => toast.dismiss('pending') }, + ], + }, + beforeEach: () => { + const clipboard = spyOn(navigator.clipboard, 'writeText').mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 2000)), + ) + return () => clipboard.mockRestore() + }, +} + +function IsolatedHosts() { + const [{ left, right }] = useState(() => { + const make = () => { + const manager = createToastManager() + return { manager, toast: createToast(manager) } + } + return { left: make(), right: make() } + }) + return ( +
+

+ Two independently owned managers reuse the same ID. Updating or dismissing one must leave + the other intact. The second viewport uses the configuration host's 60px top offset. +

+
+ + + + +
+ + +
+ ) +} +export const ScopedHostIsolation: Story = { render: () => } + +export const GlobalWebInstance: Story = { + args: { + description: + 'Uses the actual web singleton and the AppToastHost already mounted by the Storybook decorator. Copy, typed calls, and dismissals use the same entrypoint as application callers.', + scenarios: [ + { + label: 'Global error', + run: () => globalToast.error('Global request failed', { description: details, timeout: 0 }), + }, + { label: 'Global success', run: () => globalToast.success('Global save complete') }, + { label: 'Dismiss global notifications', run: () => globalToast.dismiss() }, + ], + }, + beforeEach: () => { + globalToast.dismiss() + return () => globalToast.dismiss() + }, +} + +export const DarkTheme: Story = { + ...CopyFeedback, + globals: { theme: 'dark' }, +} + +export const NarrowViewport: Story = { + ...LongContent, + globals: { viewport: { value: 'toastMobile', isRotated: false } }, + parameters: { + viewport: { + options: { + toastMobile: { + name: 'Mobile 360px', + styles: { width: '360px', height: '740px' }, + type: 'mobile', + }, + }, + }, + }, +} diff --git a/web/app/reset-password/check-code/page.tsx b/web/app/reset-password/check-code/page.tsx index f1d3990e5bc..d6688511f02 100644 --- a/web/app/reset-password/check-code/page.tsx +++ b/web/app/reset-password/check-code/page.tsx @@ -3,11 +3,11 @@ import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Countdown from '@/app/components/signin/countdown' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' import { sendResetPasswordCode, verifyResetPasswordCode } from '@/service/common' diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index 1d3fd131c6b..858432a0d82 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -3,10 +3,10 @@ import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel, FieldValidity } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' +import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx index c38b0045529..fc85ea2bb34 100644 --- a/web/app/signin/check-code/page.tsx +++ b/web/app/signin/check-code/page.tsx @@ -3,7 +3,6 @@ import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -11,6 +10,7 @@ import { useLocale } from '#i18n' import { trackEvent } from '@/app/components/base/amplitude' import Countdown from '@/app/components/signin/countdown' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage' +import { toast } from '@/app/notifications' import { TURNSTILE_SITE_KEY } from '@/config' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 70d94d8a0ab..08e7e93a135 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -5,11 +5,11 @@ import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' +import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' diff --git a/web/app/signin/components/sso-auth.tsx b/web/app/signin/components/sso-auth.tsx index 0f08b4e55d4..8e7206ed64a 100644 --- a/web/app/signin/components/sso-auth.tsx +++ b/web/app/signin/components/sso-auth.tsx @@ -3,10 +3,10 @@ import type { SsoProtocol } from '@dify/contracts/api/console/system-features/ty import type { FC } from 'react' import { zSsoProtocol } from '@dify/contracts/api/console/system-features/zod.gen' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import { toast } from '@/app/notifications' import { useRouter, useSearchParams } from '@/next/navigation' import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso' diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx index fc28e790240..2b6181bee09 100644 --- a/web/app/signin/normal-form.tsx +++ b/web/app/signin/normal-form.tsx @@ -1,11 +1,11 @@ import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' diff --git a/web/app/signin/one-more-step.tsx b/web/app/signin/one-more-step.tsx index 3a553c6037b..8cf6494c1c3 100644 --- a/web/app/signin/one-more-step.tsx +++ b/web/app/signin/one-more-step.tsx @@ -14,10 +14,10 @@ import { SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useReducer } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { resolvePostLoginRedirect } from '@/app/signin/utils/post-login-redirect' import { LICENSE_LINK } from '@/constants/link' import { languages } from '@/i18n/language' diff --git a/web/app/signup/check-code/page.tsx b/web/app/signup/check-code/page.tsx index 97297f5a89a..ae015933dc3 100644 --- a/web/app/signup/check-code/page.tsx +++ b/web/app/signup/check-code/page.tsx @@ -4,11 +4,11 @@ import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Countdown from '@/app/components/signin/countdown' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' import { useMailValidity, useSendMail } from '@/service/use-common' diff --git a/web/app/signup/set-password/page.tsx b/web/app/signup/set-password/page.tsx index 81b2148df88..9e0d28d93a0 100644 --- a/web/app/signup/set-password/page.tsx +++ b/web/app/signup/set-password/page.tsx @@ -12,13 +12,13 @@ import { } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import Cookies from 'js-cookie' import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { rememberRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' +import { toast } from '@/app/notifications' import { resolvePostLoginRedirect } from '@/app/signin/utils/post-login-redirect' import { validPassword } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' diff --git a/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx b/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx index c7ff7124249..107489dbb6c 100644 --- a/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx +++ b/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx @@ -4,11 +4,11 @@ import type { } from '@dify/contracts/api/console/agent/types.gen' import type { AppDetail } from '@dify/contracts/api/console/apps/types.gen' import type React from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient } from '@tanstack/react-query' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' +import { toast } from '@/app/notifications' import { AgentPermission } from '@/features/agent-v2/acl' import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' @@ -41,7 +41,7 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.test${path}`, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/features/agent-v2/agent-detail/access/components/agent-api-key-modal.tsx b/web/features/agent-v2/agent-detail/access/components/agent-api-key-modal.tsx index a7095abeaf1..9eabb3f9a66 100644 --- a/web/features/agent-v2/agent-detail/access/components/agent-api-key-modal.tsx +++ b/web/features/agent-v2/agent-detail/access/components/agent-api-key-modal.tsx @@ -18,11 +18,11 @@ import { DialogTitle, } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { CopyFeedback } from '@/app/components/base/copy-feedback' +import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' import { consoleQuery } from '@/service/console' diff --git a/web/features/agent-v2/agent-detail/access/components/service-api-access-card.tsx b/web/features/agent-v2/agent-detail/access/components/service-api-access-card.tsx index 21bff295487..da373211f27 100644 --- a/web/features/agent-v2/agent-detail/access/components/service-api-access-card.tsx +++ b/web/features/agent-v2/agent-detail/access/components/service-api-access-card.tsx @@ -3,12 +3,12 @@ import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen' import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { useAgentPermissions } from '@/features/agent-v2/permissions' import { consoleQuery } from '@/service/console' diff --git a/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx b/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx index a1e91110141..6674d0ca221 100644 --- a/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx +++ b/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx @@ -5,7 +5,6 @@ import type { AppSiteUpdatePayload } from '@dify/contracts/api/console/apps/type import type { ConfigParams, SettingsAppInfo } from '@/app/components/app/overview/settings' import type { AppIconType } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -19,6 +18,7 @@ import SettingsModal from '@/app/components/app/overview/settings' import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { getAgentACLCapabilities } from '@/features/agent-v2/acl' import dynamic from '@/next/dynamic' import { consoleQuery } from '@/service/console' diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx index 093962482b8..56cbe85f94e 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx @@ -196,7 +196,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index 5e27852b31a..2235abfbf99 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -147,7 +147,7 @@ const configuredModel = { model: 'gpt-4o-mini', } -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx index 39d1f129eb6..e6173eff6e7 100644 --- a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx @@ -8,13 +8,13 @@ import type { import type { useAgentConfigureData } from '../hooks' import type { AgentConfigureRightPanelMode } from '../state' import type { AgentPreviewChatController } from './preview/chat-conversation' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue, useSetAtom } from 'jotai' import { ScopeProvider } from 'jotai-scope' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' import { rebaseAgentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx index 40719552008..954167d03ee 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx @@ -53,7 +53,7 @@ const composerQuery = vi.hoisted(() => ({ shouldFail: false, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/__tests__/env.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/__tests__/env.spec.tsx index f51f7d7b889..a96ef268255 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/__tests__/env.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/__tests__/env.spec.tsx @@ -1,14 +1,14 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' import { AgentOrchestrateReadOnlyContext } from '../../read-only-context' import { AgentEnvEditor, EnvVariablesTable } from '../env' import { getEnvImportPlatform, parseEnvImport } from '../env-utils' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env.tsx index e3243653b73..474601660a3 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env.tsx @@ -12,11 +12,11 @@ import { SelectItemText, SelectTrigger, } from '@langgenius/dify-ui/select' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useAtomValue, useSetAtom } from 'jotai' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { addEnvVariableAtom, agentComposerEnvVariablesAtom, diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx index ed1872b48c6..3483f7d00da 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx @@ -1,11 +1,11 @@ import type { AgentConfigApiContext } from '../../config-context' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient } from '@tanstack/react-query' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useAtomValue } from 'jotai' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions' import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' @@ -57,7 +57,7 @@ const mocks = vi.hoisted(() => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx index 9c8d483ad69..8cbc3312e70 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx @@ -19,13 +19,13 @@ import { } from '@langgenius/dify-ui/dialog' import { FileTreeIcon } from '@langgenius/dify-ui/file-tree' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { getSupportFileType } from '@/app/components/base/file-uploader/utils' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { useFileUploadConfig } from '@/service/use-common' import { formatFileSize } from '@/utils/format' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx index c463fcbe942..db2d7e2c467 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx @@ -17,7 +17,6 @@ import type { } from '@/features/agent-v2/agent-composer/form-state' import { cn } from '@langgenius/dify-ui/cn' import { Kbd } from '@langgenius/dify-ui/kbd' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' @@ -39,6 +38,7 @@ import { Infotip } from '@/app/components/base/infotip' import PromptEditor from '@/app/components/base/prompt-editor' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge' import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt' import { diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx index d6bf797d589..5efe1ed2266 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx @@ -10,13 +10,13 @@ import { Button } from '@langgenius/dify-ui/button' import { Collapsible, CollapsiblePanel } from '@langgenius/dify-ui/collapsible' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { isAgentComposerDirtyAtom } from '@/features/agent-v2/agent-composer/store' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import useTimestamp from '@/hooks/use-timestamp' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index debcb886f34..a68bb0ab526 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -1,13 +1,13 @@ import type { SkillResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { AgentConfigApiContext } from '../../config-context' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useAtomValue } from 'jotai' import { useState } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions' import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' @@ -85,7 +85,7 @@ const mocks = vi.hoisted(() => ({ skillEnabled: true, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), error: vi.fn(), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx index 7e431b8cb2b..7cf56deed03 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx @@ -17,7 +17,6 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { toast } from '@langgenius/dify-ui/toast' import { keepPreviousData, useInfiniteQuery, @@ -31,6 +30,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { SkeletonRectangle } from '@/app/components/base/skeleton' +import { toast } from '@/app/notifications' import { agentComposerSkillsAtom, removeAgentSkillAtom, diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx index 57c2d092cf8..f093e726b65 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx @@ -15,11 +15,11 @@ import { DialogTitle, } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useRef, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' +import { toast } from '@/app/notifications' import Link from '@/next/link' import { consoleQuery } from '@/service/console' import { useFileUploadConfig } from '@/service/use-common' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts index aafcbfdce4a..0494e84c759 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts @@ -4,11 +4,11 @@ import type { AgentConfigSkillFileResponse } from '@dify/contracts/api/console/a import type { AgentConfigApiContext } from '../config-context' import type { AgentSkillDetail, AgentSkillDetailDownloadAction } from './detail-dialog' import type { AgentFileNode, AgentSkill } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { queryOptions, skipToken, useQuery, useQueryClient } from '@tanstack/react-query' import Cookies from 'js-cookie' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { API_PREFIX, CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '@/config' import { consoleQuery } from '@/service/console' import { downloadBlob } from '@/utils/download' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/__tests__/dialog.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/__tests__/dialog.spec.tsx index 977e75e85eb..444c6a6a796 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/__tests__/dialog.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/__tests__/dialog.spec.tsx @@ -1,10 +1,10 @@ -import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { CliToolDialog } from '../dialog' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/dialog.tsx index 189970bf1aa..0e3d996f52a 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/dialog.tsx @@ -16,9 +16,9 @@ import { Field, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' -import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { EnvVariablesTable } from '../../advanced/env' diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx index 6c66bed2c6b..8ceb0c610ca 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx @@ -4,13 +4,13 @@ import type { AgentChatRuntimeEmptyStateProps } from '../chat-runtime' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createStore, Provider as JotaiProvider } from 'jotai' import { createRef, useState } from 'react' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { toast } from '@/app/notifications' import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store' import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model' import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt' @@ -38,7 +38,7 @@ const mockConsoleState = vi.hoisted(() => ({ deploymentEdition: 'COMMUNITY' as 'CLOUD' | 'COMMUNITY' | 'ENTERPRISE', })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx index 8317302679c..915a9c68e6c 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx @@ -1,9 +1,9 @@ import type { AgentWorkingDirectorySource } from '../working-directory-panel' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { AgentWorkingDirectoryPanel } from '../working-directory-panel' type QueryOptionsInput = { @@ -182,7 +182,7 @@ vi.mock('@/utils/download', () => ({ downloadUrl: mocks.downloadUrl, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { success: mocks.toastSuccess, }, diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/chat-conversation.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/chat-conversation.tsx index 189b632d72c..ad7ac25a2b7 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/chat-conversation.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/chat-conversation.tsx @@ -12,13 +12,13 @@ import type { AgentComposerModel } from '@/features/agent-v2/agent-composer/form import type { Inputs } from '@/models/debug' import { Avatar } from '@langgenius/dify-ui/avatar' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { AgentRosterResponseContent } from '@/app/components/base/chat/chat/answer/agent-roster-response-content' import { useChat } from '@/app/components/base/chat/chat/hooks' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils' +import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import { userProfileQueryOptions } from '@/features/account-profile/client' import dynamic from '@/next/dynamic' diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/working-directory-panel.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/working-directory-panel.tsx index 401f3f12392..49d2bc305ab 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/working-directory-panel.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/working-directory-panel.tsx @@ -13,11 +13,11 @@ import type { import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state' import { Dialog } from '@langgenius/dify-ui/dialog' import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { toast } from '@langgenius/dify-ui/toast' import { skipToken, useMutation, useQueries, useQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' +import { toast } from '@/app/notifications' import { consoleClient, consoleQuery } from '@/service/console' import { downloadUrl } from '@/utils/download' import { getFileIconType } from '../orchestrate/files/file-icon' diff --git a/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts b/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts index 01a2d7596e9..4c09def0579 100644 --- a/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts +++ b/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts @@ -11,11 +11,11 @@ import type { AgentFileNode, AgentSoulConfigFormState, } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import isEqual from 'fast-deep-equal' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions' import { consoleQuery } from '@/service/console' import { usePrepareAgentBuildDraftBeforeRun } from './use-agent-build-draft-run' diff --git a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts index f2098c18718..11706d36f65 100644 --- a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts +++ b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts @@ -3,7 +3,6 @@ import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen' import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { toast } from '@langgenius/dify-ui/toast' import { mutationOptions, useMutation, useQueryClient } from '@tanstack/react-query' import { debounce } from 'es-toolkit/compat' import isEqual from 'fast-deep-equal' @@ -12,6 +11,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' +import { toast } from '@/app/notifications' import { getAgentACLCapabilities } from '@/features/agent-v2/acl' import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions' import { diff --git a/web/features/agent-v2/agent-detail/logs/__tests__/log-detail-panel.spec.tsx b/web/features/agent-v2/agent-detail/logs/__tests__/log-detail-panel.spec.tsx index d620e5026c3..cdef32279b3 100644 --- a/web/features/agent-v2/agent-detail/logs/__tests__/log-detail-panel.spec.tsx +++ b/web/features/agent-v2/agent-detail/logs/__tests__/log-detail-panel.spec.tsx @@ -39,7 +39,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mocks.toastError, success: mocks.toastSuccess, diff --git a/web/features/agent-v2/agent-detail/logs/components/log-detail-panel.tsx b/web/features/agent-v2/agent-detail/logs/components/log-detail-panel.tsx index f3158fc49eb..4041a413a55 100644 --- a/web/features/agent-v2/agent-detail/logs/components/log-detail-panel.tsx +++ b/web/features/agent-v2/agent-detail/logs/components/log-detail-panel.tsx @@ -5,13 +5,13 @@ import type { import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { ChatConfig, OnFeedback } from '@/app/components/base/chat/types' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import Chat from '@/app/components/base/chat/chat' import CopyIcon from '@/app/components/base/copy-icon' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' import { consoleQuery } from '@/service/console' diff --git a/web/features/agent-v2/agent-detail/sidebar-actions.tsx b/web/features/agent-v2/agent-detail/sidebar-actions.tsx index c4d4ea4db98..41a5db6448d 100644 --- a/web/features/agent-v2/agent-detail/sidebar-actions.tsx +++ b/web/features/agent-v2/agent-detail/sidebar-actions.tsx @@ -10,10 +10,10 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useExportAppDsl } from '@/app/components/app/use-export-app-dsl' +import { toast } from '@/app/notifications' import { getAgentACLCapabilities } from '@/features/agent-v2/acl' import { useCanCreateAgents } from '@/features/agent-v2/permissions' import { DeleteAgentDialog } from '@/features/agent-v2/roster/components/delete-agent-dialog' diff --git a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx index 568929e7f29..fe86aae536c 100644 --- a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx @@ -1,9 +1,9 @@ import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen' import type { AgentRosterListState } from '../agent-roster-list' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/notifications' import { AgentPermission } from '@/features/agent-v2/acl' import { AgentRosterList } from '../agent-roster-list' diff --git a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx index 379c4b5c07c..01d2655ef45 100644 --- a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx @@ -24,7 +24,7 @@ vi.mock('@tanstack/react-query', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx index c6995c1e539..5de6fa9044a 100644 --- a/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx @@ -20,7 +20,7 @@ vi.mock('@tanstack/react-query', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMock, })) diff --git a/web/features/agent-v2/roster/components/agent-roster-list.tsx b/web/features/agent-v2/roster/components/agent-roster-list.tsx index 77ff2d6371b..a16a4a1936b 100644 --- a/web/features/agent-v2/roster/components/agent-roster-list.tsx +++ b/web/features/agent-v2/roster/components/agent-roster-list.tsx @@ -19,13 +19,13 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { useExportAppDsl } from '@/app/components/app/use-export-app-dsl' import AppIcon from '@/app/components/base/app-icon' import { SkeletonRectangle } from '@/app/components/base/skeleton' import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid' +import { toast } from '@/app/notifications' import { getAgentACLCapabilities, getAgentDefaultSection } from '@/features/agent-v2/acl' import { useCanCreateAgents } from '@/features/agent-v2/permissions' import useTimestamp from '@/hooks/use-timestamp' diff --git a/web/features/agent-v2/roster/components/delete-agent-dialog.tsx b/web/features/agent-v2/roster/components/delete-agent-dialog.tsx index 0d48d3c2fee..2dc67992da9 100644 --- a/web/features/agent-v2/roster/components/delete-agent-dialog.tsx +++ b/web/features/agent-v2/roster/components/delete-agent-dialog.tsx @@ -9,9 +9,9 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' type DeleteAgentDialogProps = { diff --git a/web/features/agent-v2/roster/components/duplicate-agent-dialog.tsx b/web/features/agent-v2/roster/components/duplicate-agent-dialog.tsx index 5da78c8b859..53d9469068b 100644 --- a/web/features/agent-v2/roster/components/duplicate-agent-dialog.tsx +++ b/web/features/agent-v2/roster/components/duplicate-agent-dialog.tsx @@ -12,11 +12,11 @@ import { } from '@langgenius/dify-ui/dialog' import { Form } from '@langgenius/dify-ui/form' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIconPicker from '@/app/components/base/app-icon-picker' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { createAgentIconSelection } from './agent-form' import { AgentFormFields } from './agent-form-fields' diff --git a/web/features/home/continue-work/__tests__/item.spec.tsx b/web/features/home/continue-work/__tests__/item.spec.tsx index 8bb5c886e1a..0e1f4dab7bc 100644 --- a/web/features/home/continue-work/__tests__/item.spec.tsx +++ b/web/features/home/continue-work/__tests__/item.spec.tsx @@ -28,7 +28,7 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { warning: toastMocks.warning, }, diff --git a/web/features/home/continue-work/item.tsx b/web/features/home/continue-work/item.tsx index 9b1d2bbdf77..2f5167ec18a 100644 --- a/web/features/home/continue-work/item.tsx +++ b/web/features/home/continue-work/item.tsx @@ -2,13 +2,13 @@ import type { RecentAppResponse } from '@dify/contracts/api/console/apps/types.gen' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/features/home/home-content/__tests__/home-content.spec.tsx b/web/features/home/home-content/__tests__/home-content.spec.tsx index 130e7bb3908..ed6f4d83801 100644 --- a/web/features/home/home-content/__tests__/home-content.spec.tsx +++ b/web/features/home/home-content/__tests__/home-content.spec.tsx @@ -203,7 +203,7 @@ const toastMocks = vi.hoisted(() => { return { record, api } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks.api, })) diff --git a/web/features/new-rag/__tests__/add-source-page.spec.tsx b/web/features/new-rag/__tests__/add-source-page.spec.tsx index c8954647001..25997282586 100644 --- a/web/features/new-rag/__tests__/add-source-page.spec.tsx +++ b/web/features/new-rag/__tests__/add-source-page.spec.tsx @@ -18,7 +18,7 @@ vi.mock('@/next/navigation', () => ({ useRouter: () => routerMock })) const toastInfoMock = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { info: toastInfoMock }, })) diff --git a/web/features/new-rag/__tests__/document-detail-page.spec.tsx b/web/features/new-rag/__tests__/document-detail-page.spec.tsx index 6c402bdee50..030d1d2605e 100644 --- a/web/features/new-rag/__tests__/document-detail-page.spec.tsx +++ b/web/features/new-rag/__tests__/document-detail-page.spec.tsx @@ -176,7 +176,7 @@ vi.mock('@/context/permission-state', () => ({ refreshWorkspacePermissionKeysAfterMutationDenialAtom: permissionState.refreshAtom, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastState })) +vi.mock('@/app/notifications', () => ({ toast: toastState })) vi.mock('copy-to-clipboard', () => ({ default: vi.fn(() => true) })) vi.mock('@tanstack/react-virtual', () => ({ diff --git a/web/features/new-rag/__tests__/documents-page.spec.tsx b/web/features/new-rag/__tests__/documents-page.spec.tsx index 0c2c1b5d7eb..9993d15c294 100644 --- a/web/features/new-rag/__tests__/documents-page.spec.tsx +++ b/web/features/new-rag/__tests__/documents-page.spec.tsx @@ -143,7 +143,7 @@ vi.mock('jotai', async (importOriginal) => { } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastMock })) +vi.mock('@/app/notifications', () => ({ toast: toastMock })) const documentsInfiniteOptions = vi.hoisted(() => vi.fn((options: Omit) => ({ ...options, queryKind: 'documents' })), diff --git a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx index fff892f4d76..84433110d43 100644 --- a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx +++ b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx @@ -18,7 +18,7 @@ type ListKnowledgeSpacesInfiniteOptions = { const toastInfoMock = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { info: toastInfoMock }, })) diff --git a/web/features/new-rag/__tests__/sources-page.spec.tsx b/web/features/new-rag/__tests__/sources-page.spec.tsx index cd31c3a7d0f..dfaca911eda 100644 --- a/web/features/new-rag/__tests__/sources-page.spec.tsx +++ b/web/features/new-rag/__tests__/sources-page.spec.tsx @@ -11,7 +11,7 @@ const permissionState = vi.hoisted(() => ({ workspacePermissionKeys: ['dataset.acl.edit', 'dataset.external.connect'], })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: toastErrorMock, info: toastInfoMock }, })) diff --git a/web/features/new-rag/create-knowledge-page.tsx b/web/features/new-rag/create-knowledge-page.tsx index 758d74253a0..17062ce875d 100644 --- a/web/features/new-rag/create-knowledge-page.tsx +++ b/web/features/new-rag/create-knowledge-page.tsx @@ -27,11 +27,11 @@ import { SelectTrigger, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' diff --git a/web/features/new-rag/document-chunk-detail.tsx b/web/features/new-rag/document-chunk-detail.tsx index eca7f5214b2..c1ba713e1e4 100644 --- a/web/features/new-rag/document-chunk-detail.tsx +++ b/web/features/new-rag/document-chunk-detail.tsx @@ -4,10 +4,10 @@ import type { LogicalDocumentRevision, } from '@dify/contracts/knowledge-fs/types.gen' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import copy from 'copy-to-clipboard' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { chunkCharacterCount, chunkContentParts, diff --git a/web/features/new-rag/document-list.tsx b/web/features/new-rag/document-list.tsx index 2a578d5a04f..e6b72e7edda 100644 --- a/web/features/new-rag/document-list.tsx +++ b/web/features/new-rag/document-list.tsx @@ -13,10 +13,10 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' import { sourceName } from './document-model' diff --git a/web/features/new-rag/documents-page.tsx b/web/features/new-rag/documents-page.tsx index b31c777593f..bc5933eefd4 100644 --- a/web/features/new-rag/documents-page.tsx +++ b/web/features/new-rag/documents-page.tsx @@ -7,13 +7,13 @@ import type { } from './services/processing-task-events' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue, useSetAtom } from 'jotai' import { debounce, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { datasetDefaultPermissionKeysAtom, refreshWorkspacePermissionKeysAfterMutationDenialAtom, diff --git a/web/features/new-rag/knowledge-space-shell.tsx b/web/features/new-rag/knowledge-space-shell.tsx index 7d6b3a20b1b..9ab62898a3d 100644 --- a/web/features/new-rag/knowledge-space-shell.tsx +++ b/web/features/new-rag/knowledge-space-shell.tsx @@ -3,10 +3,10 @@ import type { ReactNode } from 'react' import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { usePathname } from '@/next/navigation' diff --git a/web/features/new-rag/new-knowledge-list.tsx b/web/features/new-rag/new-knowledge-list.tsx index 189c920f2b5..549238700c5 100644 --- a/web/features/new-rag/new-knowledge-list.tsx +++ b/web/features/new-rag/new-knowledge-list.tsx @@ -1,7 +1,6 @@ 'use client' import { Button, buttonVariants } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useState } from 'react' @@ -9,6 +8,7 @@ import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import ExternalAPIPanel from '@/app/components/datasets/external-api/external-api-panel' import { ServiceApi } from '@/app/components/datasets/extra-info/service-api' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import Link from '@/next/link' import { consoleQuery } from '@/service/console' diff --git a/web/features/new-rag/sources-page.tsx b/web/features/new-rag/sources-page.tsx index fcb7d9224b9..60fcdc4c2d7 100644 --- a/web/features/new-rag/sources-page.tsx +++ b/web/features/new-rag/sources-page.tsx @@ -23,12 +23,12 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { StatusDot } from '@langgenius/dify-ui/status-dot' -import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import Link from '@/next/link' import { consoleClient, consoleQuery } from '@/service/console' diff --git a/web/features/new-rag/use-document-reindex.ts b/web/features/new-rag/use-document-reindex.ts index a4f7a7bd87f..5b9a8f3d611 100644 --- a/web/features/new-rag/use-document-reindex.ts +++ b/web/features/new-rag/use-document-reindex.ts @@ -1,10 +1,10 @@ 'use client' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useSetAtom } from 'jotai' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { refreshWorkspacePermissionKeysAfterMutationDenialAtom } from '@/context/permission-state' import { consoleQuery } from '@/service/console' import { DatasetACLPermission, hasPermission } from '@/utils/permission' diff --git a/web/features/skills/__tests__/detail-page.fixture.tsx b/web/features/skills/__tests__/detail-page.fixture.tsx index 595c1959233..a845467feda 100644 --- a/web/features/skills/__tests__/detail-page.fixture.tsx +++ b/web/features/skills/__tests__/detail-page.fixture.tsx @@ -60,7 +60,7 @@ const mocks = vi.hoisted(() => ({ versionPatchMutationFn: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: mocks.toastError, info: mocks.toastInfo, diff --git a/web/features/skills/__tests__/page.spec.tsx b/web/features/skills/__tests__/page.spec.tsx index 16e581165a2..b935abe32a9 100644 --- a/web/features/skills/__tests__/page.spec.tsx +++ b/web/features/skills/__tests__/page.spec.tsx @@ -5,11 +5,11 @@ import type { SkillTagResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { ReactNode } from 'react' -import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import SkillsPage from '../page' type SkillsInfiniteOptions = { @@ -45,7 +45,7 @@ const mocks = vi.hoisted(() => ({ tagsQueryOptions: vi.fn((_options: unknown) => ({})), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), success: vi.fn(), diff --git a/web/features/skills/detail/builder-panel.tsx b/web/features/skills/detail/builder-panel.tsx index 443189102fd..7d0f066aaa0 100644 --- a/web/features/skills/detail/builder-panel.tsx +++ b/web/features/skills/detail/builder-panel.tsx @@ -8,7 +8,6 @@ import type { BuilderChatMessage, SkillBuilderAttachment, SkillBuilderModel } fr import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,6 +19,7 @@ import { import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { sendSkillAssistMessage, uploadSkillFile } from '../client' import { SkillBuilderGridTexture } from './builder-grid-texture' diff --git a/web/features/skills/detail/file-editor.tsx b/web/features/skills/detail/file-editor.tsx index fbe752440c4..38170101705 100644 --- a/web/features/skills/detail/file-editor.tsx +++ b/web/features/skills/detail/file-editor.tsx @@ -9,13 +9,13 @@ import type { ChangeEvent, FormEvent, KeyboardEvent } from 'react' import type { SkillFileMutationCoordinator } from './shared' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SkeletonRectangle } from '@/app/components/base/skeleton' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { toast } from '@/app/notifications' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import dynamic from '@/next/dynamic' import { consoleQuery } from '@/service/console' diff --git a/web/features/skills/detail/file-tree.tsx b/web/features/skills/detail/file-tree.tsx index 7c546f15e7a..39beea3b2de 100644 --- a/web/features/skills/detail/file-tree.tsx +++ b/web/features/skills/detail/file-tree.tsx @@ -50,7 +50,6 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { formatForDisplay, matchesKeyboardEvent, useHotkey } from '@tanstack/react-hotkeys' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' @@ -63,6 +62,7 @@ import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog- import { GOTO_ANYTHING_HOTKEY } from '@/app/components/goto-anything/hotkeys' import AccountSection from '@/app/components/main-nav/components/account-section' import HelpMenu from '@/app/components/main-nav/components/help-menu' +import { toast } from '@/app/notifications' import Link from '@/next/link' import { consoleQuery } from '@/service/console' import { fetchSkillFileBlob, uploadSkillFile } from '../client' diff --git a/web/features/skills/detail/page.tsx b/web/features/skills/detail/page.tsx index 1b918b5727a..5b9af3506b1 100644 --- a/web/features/skills/detail/page.tsx +++ b/web/features/skills/detail/page.tsx @@ -10,10 +10,10 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { consoleQuery } from '@/service/console' diff --git a/web/features/skills/detail/shared.tsx b/web/features/skills/detail/shared.tsx index 2afa1dd2933..e740e8d084a 100644 --- a/web/features/skills/detail/shared.tsx +++ b/web/features/skills/detail/shared.tsx @@ -12,8 +12,8 @@ import type { FormValue, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { toast } from '@/app/notifications' import { getFileIconType } from '@/features/agent-v2/agent-detail/configure/components/orchestrate/files/file-icon' import { consoleClient, consoleQuery } from '@/service/console' import { diff --git a/web/features/skills/detail/sidebar-actions.tsx b/web/features/skills/detail/sidebar-actions.tsx index 611bad6c07c..191dc923d62 100644 --- a/web/features/skills/detail/sidebar-actions.tsx +++ b/web/features/skills/detail/sidebar-actions.tsx @@ -19,10 +19,10 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { downloadBlob } from '@/utils/download' diff --git a/web/features/skills/detail/skill-display-name-editor.tsx b/web/features/skills/detail/skill-display-name-editor.tsx index d7c519e6139..54a38a1f519 100644 --- a/web/features/skills/detail/skill-display-name-editor.tsx +++ b/web/features/skills/detail/skill-display-name-editor.tsx @@ -2,10 +2,10 @@ import type { SkillDetailResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { SkillFileMutationCoordinator } from './shared' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' import { getAsyncSkillErrorPayload, diff --git a/web/features/skills/detail/skill-metadata.tsx b/web/features/skills/detail/skill-metadata.tsx index 461d7e895a2..529605cc946 100644 --- a/web/features/skills/detail/skill-metadata.tsx +++ b/web/features/skills/detail/skill-metadata.tsx @@ -17,12 +17,12 @@ import { ComboboxTrigger, createComboboxItems, } from '@langgenius/dify-ui/combobox' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import { SkeletonRectangle } from '@/app/components/base/skeleton' +import { toast } from '@/app/notifications' import { isCreateTagOption } from '@/features/tag-management/components/tag-combobox-item' import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal' import { TagSearchContentView } from '@/features/tag-management/components/tag-search-content' diff --git a/web/features/skills/detail/version-panel.tsx b/web/features/skills/detail/version-panel.tsx index 207733753d6..c63badf2104 100644 --- a/web/features/skills/detail/version-panel.tsx +++ b/web/features/skills/detail/version-panel.tsx @@ -31,10 +31,10 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Textarea } from '@langgenius/dify-ui/textarea' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' import { consoleQuery } from '@/service/console' import { getSkillVersionTitle, invalidateSkillDetail } from './shared' diff --git a/web/features/skills/page.tsx b/web/features/skills/page.tsx index 1e256f976e4..2f6816b4dd6 100644 --- a/web/features/skills/page.tsx +++ b/web/features/skills/page.tsx @@ -31,7 +31,6 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import { useQueryState } from 'nuqs' @@ -40,6 +39,7 @@ import { Trans, useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { SkeletonRectangle } from '@/app/components/base/skeleton' import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid' +import { toast } from '@/app/notifications' import { SkillCardTags } from '@/features/tag-management/components/skill-card-tags' import { TagFilter } from '@/features/tag-management/components/tag-filter' import useDocumentTitle from '@/hooks/use-document-title' diff --git a/web/features/tag-management/__tests__/tag-item-editor.spec.tsx b/web/features/tag-management/__tests__/tag-item-editor.spec.tsx index 4efdf35ff99..c40536c7634 100644 --- a/web/features/tag-management/__tests__/tag-item-editor.spec.tsx +++ b/web/features/tag-management/__tests__/tag-item-editor.spec.tsx @@ -70,7 +70,7 @@ vi.mock('@/service/console', () => ({ }, })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: tagMocks.api, })) diff --git a/web/features/tag-management/__tests__/tag-management-modal.spec.tsx b/web/features/tag-management/__tests__/tag-management-modal.spec.tsx index 5bd1f6fdb32..42d41572a5e 100644 --- a/web/features/tag-management/__tests__/tag-management-modal.spec.tsx +++ b/web/features/tag-management/__tests__/tag-management-modal.spec.tsx @@ -20,7 +20,7 @@ const { mockNotify, mockToast } = vi.hoisted(() => { return { mockNotify, mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: mockToast, })) diff --git a/web/features/tag-management/__tests__/tag-selector.spec.tsx b/web/features/tag-management/__tests__/tag-selector.spec.tsx index 653fae669ec..65cb82ae6e5 100644 --- a/web/features/tag-management/__tests__/tag-selector.spec.tsx +++ b/web/features/tag-management/__tests__/tag-selector.spec.tsx @@ -19,7 +19,7 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@langgenius/dify-ui/toast', () => ({ toast: mockToast })) +vi.mock('@/app/notifications', () => ({ toast: mockToast })) const { mockUseQueryData, createTag, bindTag, unBindTag } = vi.hoisted(() => { const mockUseQueryData: { current: Tag[] } = { current: [] } diff --git a/web/features/tag-management/components/tag-item-editor.tsx b/web/features/tag-management/components/tag-item-editor.tsx index 434ea325e68..ca531e69d25 100644 --- a/web/features/tag-management/components/tag-item-editor.tsx +++ b/web/features/tag-management/components/tag-item-editor.tsx @@ -9,12 +9,12 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useMutation } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' type TagItemEditorProps = { diff --git a/web/features/tag-management/components/tag-management-modal.tsx b/web/features/tag-management/components/tag-management-modal.tsx index 8b81e18d178..8e6eb9434ec 100644 --- a/web/features/tag-management/components/tag-management-modal.tsx +++ b/web/features/tag-management/components/tag-management-modal.tsx @@ -2,11 +2,11 @@ import type { TagType } from '@dify/contracts/api/console/tags/types.gen' import { Dialog, DialogClose, DialogContent } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' diff --git a/web/features/tag-management/components/tag-selector.tsx b/web/features/tag-management/components/tag-selector.tsx index 77719877b6d..35859accf66 100644 --- a/web/features/tag-management/components/tag-selector.tsx +++ b/web/features/tag-management/components/tag-selector.tsx @@ -10,11 +10,11 @@ import { ComboboxTrigger, createComboboxItems, } from '@langgenius/dify-ui/combobox' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' diff --git a/web/hooks/use-import-dsl.spec.tsx b/web/hooks/use-import-dsl.spec.tsx index 65b0a141f15..d82a98aa12d 100644 --- a/web/hooks/use-import-dsl.spec.tsx +++ b/web/hooks/use-import-dsl.spec.tsx @@ -16,7 +16,7 @@ const toastMocks = vi.hoisted(() => ({ warning: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: toastMocks, })) diff --git a/web/hooks/use-import-dsl.ts b/web/hooks/use-import-dsl.ts index b7b05c8edae..b137dc6c554 100644 --- a/web/hooks/use-import-dsl.ts +++ b/web/hooks/use-import-dsl.ts @@ -1,12 +1,12 @@ import type { AppImportPayload, Import } from '@dify/contracts/api/console/apps/types.gen' import type { AppIconType } from '@/types/app' -import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { createElement, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import DSLImportWarningDescription from '@/app/components/app/create-from-dsl-modal/dsl-import-warning-description' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' diff --git a/web/i18n/locales/ar-TN/common.json b/web/i18n/locales/ar-TN/common.json index 2b1f439734d..b32a43550c9 100644 --- a/web/i18n/locales/ar-TN/common.json +++ b/web/i18n/locales/ar-TN/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "يرجى تأكيد الإجراء الخاص بك.", "operation.copied": "تم النسخ", "operation.copy": "نسخ", + "operation.copyErrorDetails": "نسخ تفاصيل الخطأ", + "operation.copyErrorFailed": "فشل النسخ. أعد المحاولة", "operation.copyImage": "نسخ الصورة", "operation.create": "إنشاء", "operation.deSelectAll": "إلغاء تحديد الكل", diff --git a/web/i18n/locales/az-AZ/common.json b/web/i18n/locales/az-AZ/common.json index ffb361bed99..8bd7f8d9f0b 100644 --- a/web/i18n/locales/az-AZ/common.json +++ b/web/i18n/locales/az-AZ/common.json @@ -449,6 +449,8 @@ "operation.confirmAction": "Əməliyyatını təsdiqlə.", "operation.copied": "Kopyalandı", "operation.copy": "Kopyala", + "operation.copyErrorDetails": "Xəta təfərrüatlarını kopyala", + "operation.copyErrorFailed": "Kopyalama alınmadı. Yenidən cəhd et", "operation.copyImage": "Şəkli kopyala", "operation.create": "Yarat", "operation.deSelectAll": "Bütün seçimləri ləğv et", diff --git a/web/i18n/locales/de-DE/common.json b/web/i18n/locales/de-DE/common.json index 58cb17c3ea0..dd7e08e858b 100644 --- a/web/i18n/locales/de-DE/common.json +++ b/web/i18n/locales/de-DE/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Bitte bestätigen Sie Ihre Aktion.", "operation.copied": "Kopiert", "operation.copy": "Kopieren", + "operation.copyErrorDetails": "Fehlerdetails kopieren", + "operation.copyErrorFailed": "Kopieren fehlgeschlagen. Erneut versuchen", "operation.copyImage": "Bild kopieren", "operation.create": "Erstellen", "operation.deSelectAll": "Alle abwählen", diff --git a/web/i18n/locales/en-US/common.json b/web/i18n/locales/en-US/common.json index 7179f8078af..a79bf6625f8 100644 --- a/web/i18n/locales/en-US/common.json +++ b/web/i18n/locales/en-US/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Please confirm your action.", "operation.copied": "Copied", "operation.copy": "Copy", + "operation.copyErrorDetails": "Copy error details", + "operation.copyErrorFailed": "Copy failed. Retry", "operation.copyImage": "Copy Image", "operation.create": "Create", "operation.deSelectAll": "Deselect All", diff --git a/web/i18n/locales/es-ES/common.json b/web/i18n/locales/es-ES/common.json index c3c49934658..75056e85d02 100644 --- a/web/i18n/locales/es-ES/common.json +++ b/web/i18n/locales/es-ES/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Por favor, confirme su acción.", "operation.copied": "Copiado", "operation.copy": "Copiar", + "operation.copyErrorDetails": "Copiar detalles del error", + "operation.copyErrorFailed": "Error al copiar. Reintentar", "operation.copyImage": "Copiar imagen", "operation.create": "Crear", "operation.deSelectAll": "Deseleccionar todo", diff --git a/web/i18n/locales/fa-IR/common.json b/web/i18n/locales/fa-IR/common.json index 944032a9fd7..f3a6381e4de 100644 --- a/web/i18n/locales/fa-IR/common.json +++ b/web/i18n/locales/fa-IR/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "لطفاً اقدام خود را تأیید کنید.", "operation.copied": "کپی", "operation.copy": "کپی", + "operation.copyErrorDetails": "کپی جزئیات خطا", + "operation.copyErrorFailed": "کپی ناموفق بود. تلاش مجدد", "operation.copyImage": "کپی تصویر", "operation.create": "ایجاد", "operation.deSelectAll": "همه را انتخاب نکنید", diff --git a/web/i18n/locales/fr-FR/common.json b/web/i18n/locales/fr-FR/common.json index 0efc9dc1dfd..f72fe237736 100644 --- a/web/i18n/locales/fr-FR/common.json +++ b/web/i18n/locales/fr-FR/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Veuillez confirmer votre action.", "operation.copied": "Copied", "operation.copy": "Copier", + "operation.copyErrorDetails": "Copier les détails de l’erreur", + "operation.copyErrorFailed": "Échec de la copie. Réessayer", "operation.copyImage": "Copier l’image", "operation.create": "Créer", "operation.deSelectAll": "Désélectionner tout", diff --git a/web/i18n/locales/hi-IN/common.json b/web/i18n/locales/hi-IN/common.json index a1e86a2238d..d97f31dad19 100644 --- a/web/i18n/locales/hi-IN/common.json +++ b/web/i18n/locales/hi-IN/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "कृपया अपनी क्रिया की पुष्टि करें।", "operation.copied": "कॉपी किया गया", "operation.copy": "कॉपी करें", + "operation.copyErrorDetails": "त्रुटि का विवरण कॉपी करें", + "operation.copyErrorFailed": "कॉपी नहीं हुआ। फिर कोशिश करें", "operation.copyImage": "छवि कॉपी करें", "operation.create": "बनाएं", "operation.deSelectAll": "सभी चयन हटाएँ", diff --git a/web/i18n/locales/id-ID/common.json b/web/i18n/locales/id-ID/common.json index 5126857bb74..c8e27ba8989 100644 --- a/web/i18n/locales/id-ID/common.json +++ b/web/i18n/locales/id-ID/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Silakan konfirmasi tindakan Anda.", "operation.copied": "Disalin", "operation.copy": "Menyalin", + "operation.copyErrorDetails": "Salin detail kesalahan", + "operation.copyErrorFailed": "Gagal menyalin. Coba lagi", "operation.copyImage": "Salin Gambar", "operation.create": "Menciptakan", "operation.deSelectAll": "Batalkan pilihan Semua", diff --git a/web/i18n/locales/it-IT/common.json b/web/i18n/locales/it-IT/common.json index ed517d0292d..5f37d415ee3 100644 --- a/web/i18n/locales/it-IT/common.json +++ b/web/i18n/locales/it-IT/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Per favore conferma la tua azione.", "operation.copied": "Copiato", "operation.copy": "Copia", + "operation.copyErrorDetails": "Copia dettagli errore", + "operation.copyErrorFailed": "Copia non riuscita. Riprova", "operation.copyImage": "Copia immagine", "operation.create": "Crea", "operation.deSelectAll": "Deseleziona tutto", diff --git a/web/i18n/locales/ja-JP/common.json b/web/i18n/locales/ja-JP/common.json index c36b3978350..796264cdf6c 100644 --- a/web/i18n/locales/ja-JP/common.json +++ b/web/i18n/locales/ja-JP/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "操作を確認してください。", "operation.copied": "コピーしました", "operation.copy": "コピー", + "operation.copyErrorDetails": "エラーの詳細をコピー", + "operation.copyErrorFailed": "コピーに失敗しました。再試行", "operation.copyImage": "画像をコピー", "operation.create": "作成", "operation.deSelectAll": "すべて選択解除", diff --git a/web/i18n/locales/ko-KR/common.json b/web/i18n/locales/ko-KR/common.json index b7eefa7eb50..6b987bbf03e 100644 --- a/web/i18n/locales/ko-KR/common.json +++ b/web/i18n/locales/ko-KR/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "귀하의 행동을 확인해 주세요.", "operation.copied": "복사", "operation.copy": "복사", + "operation.copyErrorDetails": "오류 세부 정보 복사", + "operation.copyErrorFailed": "복사 실패. 다시 시도", "operation.copyImage": "이미지 복사", "operation.create": "생성", "operation.deSelectAll": "모두 선택 해제", diff --git a/web/i18n/locales/lo-LA/common.json b/web/i18n/locales/lo-LA/common.json index edad7073829..a9c1e13e536 100644 --- a/web/i18n/locales/lo-LA/common.json +++ b/web/i18n/locales/lo-LA/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "ກະລຸນາຢືນຢັນການດຳເນີນການຂອງທ່ານ.", "operation.copied": "ຄັດລອກແລ້ວ", "operation.copy": "ຄັດລອກ", + "operation.copyErrorDetails": "ສຳເນົາລາຍລະອຽດຂໍ້ຜິດພາດ", + "operation.copyErrorFailed": "ສຳເນົາບໍ່ສຳເລັດ. ລອງໃໝ່", "operation.copyImage": "ຄັດລອກຮູບພາບ", "operation.create": "ສ້າງ", "operation.deSelectAll": "ຍົກເລີກການເລືອກທັງໝົດ", diff --git a/web/i18n/locales/nl-NL/common.json b/web/i18n/locales/nl-NL/common.json index 2fd3b5d38ef..87bc98c3a5a 100644 --- a/web/i18n/locales/nl-NL/common.json +++ b/web/i18n/locales/nl-NL/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Please confirm your action.", "operation.copied": "Copied", "operation.copy": "Copy", + "operation.copyErrorDetails": "Foutdetails kopiëren", + "operation.copyErrorFailed": "Kopiëren mislukt. Opnieuw proberen", "operation.copyImage": "Copy Image", "operation.create": "Create", "operation.deSelectAll": "Deselect All", diff --git a/web/i18n/locales/pl-PL/common.json b/web/i18n/locales/pl-PL/common.json index ce37d533d55..897481aca3a 100644 --- a/web/i18n/locales/pl-PL/common.json +++ b/web/i18n/locales/pl-PL/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Proszę potwierdzić swoją akcję.", "operation.copied": "Kopiowane", "operation.copy": "Kopiuj", + "operation.copyErrorDetails": "Kopiuj szczegóły błędu", + "operation.copyErrorFailed": "Kopiowanie nie powiodło się. Spróbuj ponownie", "operation.copyImage": "Kopiuj obraz", "operation.create": "Utwórz", "operation.deSelectAll": "Odznacz wszystkie", diff --git a/web/i18n/locales/pt-BR/common.json b/web/i18n/locales/pt-BR/common.json index b88ab8c7049..089f40bda76 100644 --- a/web/i18n/locales/pt-BR/common.json +++ b/web/i18n/locales/pt-BR/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Por favor, confirme sua ação.", "operation.copied": "Copiado", "operation.copy": "Copiar", + "operation.copyErrorDetails": "Copiar detalhes do erro", + "operation.copyErrorFailed": "Falha ao copiar. Tentar novamente", "operation.copyImage": "Copiar imagem", "operation.create": "Criar", "operation.deSelectAll": "Desmarcar tudo", diff --git a/web/i18n/locales/ro-RO/common.json b/web/i18n/locales/ro-RO/common.json index 64d0c2b6f00..6c0e3da3bba 100644 --- a/web/i18n/locales/ro-RO/common.json +++ b/web/i18n/locales/ro-RO/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Vă rugăm să confirmați acțiunea dumneavoastră.", "operation.copied": "Copiat", "operation.copy": "Copiază", + "operation.copyErrorDetails": "Copiază detaliile erorii", + "operation.copyErrorFailed": "Copierea a eșuat. Reîncearcă", "operation.copyImage": "Copiere imagine", "operation.create": "Creează", "operation.deSelectAll": "Deselectați tot", diff --git a/web/i18n/locales/ru-RU/common.json b/web/i18n/locales/ru-RU/common.json index c1c94c8d92a..7573cf2347a 100644 --- a/web/i18n/locales/ru-RU/common.json +++ b/web/i18n/locales/ru-RU/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Пожалуйста, подтвердите ваше действие.", "operation.copied": "Скопированы", "operation.copy": "Копировать", + "operation.copyErrorDetails": "Копировать сведения об ошибке", + "operation.copyErrorFailed": "Не удалось скопировать. Повторить", "operation.copyImage": "Скопировать изображение", "operation.create": "Создать", "operation.deSelectAll": "Снять выделение со всех", diff --git a/web/i18n/locales/sl-SI/common.json b/web/i18n/locales/sl-SI/common.json index e4cd5e7ccab..af9f0c968cb 100644 --- a/web/i18n/locales/sl-SI/common.json +++ b/web/i18n/locales/sl-SI/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Prosimo, potrdite svoje dejanje.", "operation.copied": "Kopirati", "operation.copy": "Kopiraj", + "operation.copyErrorDetails": "Kopiraj podrobnosti napake", + "operation.copyErrorFailed": "Kopiranje ni uspelo. Poskusi znova", "operation.copyImage": "Kopiraj sliko", "operation.create": "Ustvari", "operation.deSelectAll": "Odberi vse", diff --git a/web/i18n/locales/th-TH/common.json b/web/i18n/locales/th-TH/common.json index 1293111607c..be75457107b 100644 --- a/web/i18n/locales/th-TH/common.json +++ b/web/i18n/locales/th-TH/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "กรุณายืนยันการกระทำของคุณ", "operation.copied": "คัด ลอก", "operation.copy": "ลอก", + "operation.copyErrorDetails": "คัดลอกรายละเอียดข้อผิดพลาด", + "operation.copyErrorFailed": "คัดลอกไม่สำเร็จ ลองอีกครั้ง", "operation.copyImage": "คัดลอกรูปภาพ", "operation.create": "สร้าง", "operation.deSelectAll": "ยกเลิกการเลือกทั้งหมด", diff --git a/web/i18n/locales/tr-TR/common.json b/web/i18n/locales/tr-TR/common.json index 64ccdeb7196..2be6c1e708a 100644 --- a/web/i18n/locales/tr-TR/common.json +++ b/web/i18n/locales/tr-TR/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Lütfen işleminizi onaylayın.", "operation.copied": "Kopyalandı", "operation.copy": "Kopyala", + "operation.copyErrorDetails": "Hata ayrıntılarını kopyala", + "operation.copyErrorFailed": "Kopyalama başarısız. Yeniden dene", "operation.copyImage": "Resmi Kopyala", "operation.create": "Oluştur", "operation.deSelectAll": "Hepsini Seçme", diff --git a/web/i18n/locales/uk-UA/common.json b/web/i18n/locales/uk-UA/common.json index fb11fb3df95..39d7e01999b 100644 --- a/web/i18n/locales/uk-UA/common.json +++ b/web/i18n/locales/uk-UA/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Будь ласка, підтвердіть свої дії.", "operation.copied": "Скопійовані", "operation.copy": "Копіювати", + "operation.copyErrorDetails": "Копіювати відомості про помилку", + "operation.copyErrorFailed": "Не вдалося скопіювати. Повторити", "operation.copyImage": "Скопіювати зображення", "operation.create": "Створити", "operation.deSelectAll": "Вимкнути все", diff --git a/web/i18n/locales/vi-VN/common.json b/web/i18n/locales/vi-VN/common.json index 71e1cccce04..44401890f64 100644 --- a/web/i18n/locales/vi-VN/common.json +++ b/web/i18n/locales/vi-VN/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "Vui lòng xác nhận hành động của bạn.", "operation.copied": "Sao chép", "operation.copy": "Sao chép", + "operation.copyErrorDetails": "Sao chép chi tiết lỗi", + "operation.copyErrorFailed": "Sao chép thất bại. Thử lại", "operation.copyImage": "Sao chép hình ảnh", "operation.create": "Tạo mới", "operation.deSelectAll": "Bỏ chọn tất cả", diff --git a/web/i18n/locales/zh-Hans/common.json b/web/i18n/locales/zh-Hans/common.json index ea81c7d584e..76b326894a8 100644 --- a/web/i18n/locales/zh-Hans/common.json +++ b/web/i18n/locales/zh-Hans/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "请确认您的操作。", "operation.copied": " 已复制", "operation.copy": "复制", + "operation.copyErrorDetails": "复制错误详情", + "operation.copyErrorFailed": "复制失败,重试", "operation.copyImage": "复制图片", "operation.create": "创建", "operation.deSelectAll": "取消全选", diff --git a/web/i18n/locales/zh-Hant/common.json b/web/i18n/locales/zh-Hant/common.json index f476ada0932..d3d23450632 100644 --- a/web/i18n/locales/zh-Hant/common.json +++ b/web/i18n/locales/zh-Hant/common.json @@ -450,6 +450,8 @@ "operation.confirmAction": "請確認您的操作。", "operation.copied": "複製", "operation.copy": "複製", + "operation.copyErrorDetails": "複製錯誤詳細資訊", + "operation.copyErrorFailed": "複製失敗,重試", "operation.copyImage": "複製圖像", "operation.create": "建立", "operation.deSelectAll": "全不選", diff --git a/web/service/__tests__/request-error-toast.spec.ts b/web/service/__tests__/request-error-toast.spec.ts index c76780b4c29..ce0542e6fe9 100644 --- a/web/service/__tests__/request-error-toast.spec.ts +++ b/web/service/__tests__/request-error-toast.spec.ts @@ -1,10 +1,10 @@ -import { toast } from '@langgenius/dify-ui/toast' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' // oxlint-disable-next-line no-restricted-imports -- Exercise the shared request transport and its notification behavior. import { base } from '../fetch' import { clearRequestErrorToasts, notifyRequestError } from '../request-error-toast' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() }, })) diff --git a/web/service/base.spec.ts b/web/service/base.spec.ts index 2360eefc26a..586e4fcb9f4 100644 --- a/web/service/base.spec.ts +++ b/web/service/base.spec.ts @@ -1,6 +1,6 @@ -import { toast } from '@langgenius/dify-ui/toast' import { waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { toast } from '@/app/notifications' import { PUBLIC_API_PREFIX } from '@/config' // oxlint-disable-next-line no-restricted-imports import { @@ -18,7 +18,7 @@ import { const refreshAccessTokenOrReLoginMock = vi.hoisted(() => vi.fn()) -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn(), }, diff --git a/web/service/base.ts b/web/service/base.ts index 4d0d2f9c9f9..a50169071d6 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -28,9 +28,9 @@ import type { WorkflowPausedResponse, WorkflowStartedResponse, } from '@/types/workflow' -import { toast } from '@langgenius/dify-ui/toast' import Cookies from 'js-cookie' import { discardRegistrationSessionState } from '@/app/components/base/amplitude/registration-session-state' +import { toast } from '@/app/notifications' import { API_PREFIX, CSRF_COOKIE_NAME, diff --git a/web/service/fetch.spec.ts b/web/service/fetch.spec.ts index 55c7cc613cc..e74a774911b 100644 --- a/web/service/fetch.spec.ts +++ b/web/service/fetch.spec.ts @@ -8,14 +8,14 @@ import { // oxlint-disable-next-line no-restricted-imports import { base } from './fetch' -vi.mock('@langgenius/dify-ui/toast', () => ({ +vi.mock('@/app/notifications', () => ({ toast: { add: vi.fn(), error: vi.fn(), }, })) -const { toast } = await import('@langgenius/dify-ui/toast') +const { toast } = await import('@/app/notifications') describe('base', () => { beforeEach(() => { diff --git a/web/service/request-error-toast.ts b/web/service/request-error-toast.ts index 5588e0753c1..f1d9743fd32 100644 --- a/web/service/request-error-toast.ts +++ b/web/service/request-error-toast.ts @@ -1,4 +1,4 @@ -import { toast } from '@langgenius/dify-ui/toast' +import { toast } from '@/app/notifications' const DEDUPE_WINDOW_MS = 10000 From 13c12c1b386e6e8c5dae3619b0786ab61530c33e Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 18 Sep 2026 07:15:22 +0000 Subject: [PATCH 19/21] fix(web): show full unavailable model names on hover (#42492) --- .../model-selector/model-selector-trigger.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx index bf4f65e6681..1de9301f240 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx @@ -159,7 +159,10 @@ function ModelSelectorTrigger({ /> )} {isDeprecated && ( - + {defaultModel.model} )} From 5fa40f1a5f5c2175618f7eedd3c4f81d71a6ecbb Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:35:05 +0000 Subject: [PATCH 20/21] fix(web): keep agent configure model selection explicit (#42488) --- .../__tests__/index.spec.tsx | 14 ++ .../model-parameter-modal/index.tsx | 4 + .../model-settings-trigger.tsx | 2 +- .../__tests__/model-selector-trigger.spec.tsx | 29 ++- .../model-selector/index.tsx | 9 +- .../model-selector/model-selector-trigger.tsx | 76 ++++--- .../agent-orchestrate-panel-content.tsx | 16 +- .../configure/__tests__/page.spec.tsx | 35 ++- .../use-agent-configure-sync.spec.tsx | 128 +++++++---- .../configure/components/composer-session.tsx | 9 +- .../components/orchestrate/index.tsx | 9 +- .../model-config/__tests__/field.spec.tsx | 205 ++++++++++++++---- .../orchestrate/model-config/field.tsx | 48 +++- .../agent-v2/agent-detail/configure/hooks.ts | 28 +-- .../configure/use-agent-configure-sync.ts | 26 +-- 15 files changed, 442 insertions(+), 196 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx index 2535bc36a03..d23c1665984 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx @@ -351,6 +351,20 @@ describe('ModelParameterModal', () => { expect(screen.getByTestId('model-selector')).toBeInTheDocument() }) + it('exposes popup state on the settings button and restores focus after Escape', async () => { + const user = userEvent.setup() + render() + const trigger = screen.getByRole('button', { name: /modelProvider\.modelSettings/i }) + expect(trigger).not.toHaveAttribute('data-popup-open') + await user.click(trigger) + expect(trigger).toHaveAttribute('data-popup-open', '') + await user.keyboard('{Escape}') + await waitFor(() => { + expect(trigger).toHaveAttribute('aria-expanded', 'false') + }) + expect(trigger).toHaveFocus() + }) + it('should not open content when readonly is true', () => { render() expect(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })).toBeDisabled() diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index cfc62826278..cdb1b03c9d9 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -58,6 +58,7 @@ export type ModelParameterModalProps = Pick & nodesOutputVars?: NodeOutPutVar[] availableNodes?: Node[] modelList?: ModelSelectorProvider[] + modelListLoading?: boolean showModelMeta?: boolean modelPredicate?: ModelSelectorModelPredicate modelSuggestionPredicate?: ModelSelectorModelPredicate @@ -84,6 +85,7 @@ const ModelParameterModal: FC = ({ nodesOutputVars, availableNodes, modelList, + modelListLoading, showModelMeta, modelPredicate, modelSuggestionPredicate, @@ -173,6 +175,7 @@ const ModelParameterModal: FC = ({ = ({ setOpen(false)} diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger.tsx index 57d15d5a39c..e536813927c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger.tsx @@ -22,7 +22,7 @@ function ModelSettingsTrigger({ disabled, surface = 'default' }: ModelSettingsTr render={ { beforeEach(() => { vi.clearAllMocks() - mockUseQuery.mockReturnValue({ data: createModel() }) + mockUseQuery.mockReturnValue({ data: { data: [createModel()] } }) mockUseCredentialPanelState.mockReturnValue({ variant: 'credits-active', priority: 'credits', @@ -76,6 +76,33 @@ describe('ModelSelectorTrigger', () => { }) }) + it('keeps an existing model neutral while its catalog is unresolved', () => { + const { rerender } = render( + , + ) + expect(screen.getByRole('button', { name: 'legacy-model' })).toBeDisabled() + expect(screen.queryByText('common.modelProvider.selector.incompatible')).not.toBeInTheDocument() + rerender( + + + , + ) + expect( + screen.getByRole('button', { + name: 'legacy-model common.modelProvider.selector.incompatible', + }), + ).toBeEnabled() + }) + + it('does not report incompatibility before provider metadata is available', () => { + mockUseQuery.mockReturnValue({ data: undefined }) + render( + , + ) + expect(screen.getByRole('button', { name: 'GPT-4' })).toBeEnabled() + expect(screen.queryByText('common.modelProvider.selector.incompatible')).not.toBeInTheDocument() + }) + describe('Rendering', () => { it('should render empty state when no model is selected', () => { render() diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx index 6b67372ebae..03194f327a4 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx @@ -33,6 +33,7 @@ type ModelSelectorBaseProps = { onValueChange?: (model: ModelSelectorValue) => void onHide?: () => void disabled?: boolean + loading?: boolean scopeFeatures?: readonly string[] showDeprecatedWarnIcon?: boolean hideProviderSettingsFooter?: boolean @@ -58,6 +59,7 @@ function ModelSelectorRoot({ onValueChange, onHide, disabled, + loading, size, surface, shape, @@ -85,12 +87,12 @@ function ModelSelectorRoot({ const handleOpenChange = useCallback( (newOpen: boolean) => { - if (disabled && newOpen) return + if ((disabled || loading) && newOpen) return setOpen(newOpen) if (!newOpen) setInputValue('') }, - [disabled], + [disabled, loading], ) const handleSelect = useCallback( @@ -140,7 +142,8 @@ function ModelSelectorRoot({ currentProvider={currentProvider} currentModel={currentModel} defaultModel={value} - disabled={disabled} + disabled={disabled || loading} + loading={loading} size={size} surface={surface} shape={shape} diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx index 1de9301f240..0148368e5ac 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx @@ -19,6 +19,7 @@ type ModelSelectorTriggerProps = { currentModel?: ModelSelectorModel defaultModel?: ModelSelectorValue disabled?: boolean + loading?: boolean size?: 'small' | 'medium' surface?: 'default' | 'workflow' shape?: 'standalone' | 'split' @@ -33,6 +34,7 @@ function ModelSelectorTrigger({ currentModel, defaultModel, disabled, + loading = false, size = 'medium', surface = 'default', shape = 'standalone', @@ -47,63 +49,67 @@ function ModelSelectorTrigger({ const isDeprecated = !isSelected && !!defaultModel const isEmpty = !isSelected && !defaultModel const providerId = isSelected ? currentProvider.provider : defaultModel?.provider - const { data: resolvedProvider } = useQuery({ + const { data: providerSummary } = useQuery({ ...consoleQuery.workspaces.current.modelProviders.summary.get.queryOptions(), enabled: !!providerId, - select: ({ data }) => data.find((provider) => provider.provider === providerId), }) + const resolvedProvider = providerSummary?.data.find( + (provider) => provider.provider === providerId, + ) + const isStatusUnavailable = loading || (!!providerId && providerSummary === undefined) + const isDisabled = disabled || loading const credentialPanel = useCredentialPanelInfo(resolvedProvider) - const status = deriveModelStatus( - isSelected ? currentModel?.model : defaultModel?.model, - isSelected ? currentProvider?.provider : defaultModel?.provider, - resolvedProvider, - currentModel, - credentialPanel, - ) + const status = isStatusUnavailable + ? undefined + : deriveModelStatus( + isSelected ? currentModel?.model : defaultModel?.model, + isSelected ? currentProvider?.provider : defaultModel?.provider, + resolvedProvider, + currentModel, + credentialPanel, + ) const isActive = status === 'active' const statusI18nKey = DERIVED_MODEL_STATUS_BADGE_I18N[status as keyof typeof DERIVED_MODEL_STATUS_BADGE_I18N] const tooltipI18nKey = DERIVED_MODEL_STATUS_TOOLTIP_I18N[status as keyof typeof DERIVED_MODEL_STATUS_TOOLTIP_I18N] - const statusLabel = - isModelCompatible && statusI18nKey + const statusLabel = !isModelCompatible + ? t(($) => $['modelProvider.selector.incompatible'], { ns: 'common' }) + : statusI18nKey ? t(($) => $[statusI18nKey], { ns: 'common' }) - : t(($) => $['modelProvider.selector.incompatible'], { ns: 'common' }) - const tooltipLabel = - isModelCompatible && tooltipI18nKey + : undefined + const tooltipLabel = !isModelCompatible + ? t(($) => $['modelProvider.selector.incompatibleTip'], { ns: 'common' }) + : tooltipI18nKey ? t(($) => $[tooltipI18nKey], { ns: 'common' }) - : t(($) => $['modelProvider.selector.incompatibleTip'], { ns: 'common' }) + : statusLabel const isCreditsExhausted = status === 'credits-exhausted' const shouldShowModelMeta = showModelMeta && status === 'active' && isModelCompatible - const deprecatedStatusLabel = - statusLabel || t(($) => $['modelProvider.selector.incompatible'], { ns: 'common' }) - const deprecatedTooltipLabel = - tooltipLabel || t(($) => $['modelProvider.selector.incompatibleTip'], { ns: 'common' }) const triggerTooltipLabel = - isDeprecated && showDeprecatedWarnIcon - ? deprecatedTooltipLabel - : isSelected && ((!isActive && statusI18nKey) || !isModelCompatible) + isDeprecated && !isStatusUnavailable && showDeprecatedWarnIcon + ? tooltipLabel + : isSelected && !isStatusUnavailable && ((!isActive && statusI18nKey) || !isModelCompatible) ? tooltipLabel : undefined return ( )} {isDeprecated && ( {defaultModel.model} @@ -173,7 +184,8 @@ function ModelSelectorTrigger({ )} {isSelected && - !disabled && + !isDisabled && + !isStatusUnavailable && ((!isActive && statusI18nKey) || !isModelCompatible) && ( )} - {isDeprecated && showDeprecatedWarnIcon && ( + {isDeprecated && !isStatusUnavailable && showDeprecatedWarnIcon && ( - {deprecatedStatusLabel} + {statusLabel} )} - {!disabled && shape !== 'split' && (isActive || isEmpty) && ( + {!isDisabled && shape !== 'split' && (isActive || isEmpty) && (