mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
Merge upstream/release/e-1.11.2 into 1.11.4
# Conflicts: # .github/workflows/tool-test-sdks.yaml # .github/workflows/translate-i18n-claude.yml # api/core/rag/datasource/retrieval_service.py # web/app/components/app/create-app-dialog/app-list/index.tsx # web/app/components/header/account-setting/members-page/operation/index.tsx # web/i18n-config/server.ts # web/package.json # web/pnpm-lock.yaml
This commit is contained in:
@@ -90,7 +90,7 @@ jobs:
|
||||
uses: actions/setup-node@v6
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -16,23 +16,24 @@ jobs:
|
||||
name: unit test for Node.js SDK
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 18, 20, 22]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdks/nodejs-client
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
<<<<<<< HEAD
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
=======
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
>>>>>>> 328897f81c (build: require node 24.13.0 (#30945))
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 24
|
||||
cache: ''
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
name: Translate i18n Files with Claude Code
|
||||
|
||||
# Note: claude-code-action doesn't support push events directly.
|
||||
# Push events are handled by trigger-i18n-sync.yml which sends repository_dispatch.
|
||||
# See: https://github.com/langgenius/dify/issues/30743
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.json'
|
||||
repository_dispatch:
|
||||
types: [i18n-sync]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
files:
|
||||
@@ -55,7 +57,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
@@ -87,26 +89,35 @@ jobs:
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Push trigger - detect changed files from the push
|
||||
BEFORE_SHA="${{ github.event.before }}"
|
||||
# Handle edge case: first push or force push may have null/zero SHA
|
||||
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
# Fallback to comparing with parent commit
|
||||
BEFORE_SHA="HEAD~1"
|
||||
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
|
||||
# Triggered by push via trigger-i18n-sync.yml workflow
|
||||
# Validate required payload fields
|
||||
if [ -z "${{ github.event.client_payload.changed_files }}" ]; then
|
||||
echo "Error: repository_dispatch payload missing required 'changed_files' field" >&2
|
||||
exit 1
|
||||
fi
|
||||
changed=$(git diff --name-only "$BEFORE_SHA" ${{ github.sha }} -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
|
||||
echo "CHANGED_FILES=$changed" >> $GITHUB_OUTPUT
|
||||
echo "CHANGED_FILES=${{ github.event.client_payload.changed_files }}" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=incremental" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=${{ github.event.client_payload.sync_mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate detailed diff for the push
|
||||
git diff "$BEFORE_SHA"..${{ github.sha }} -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
||||
if [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
# Decode the base64-encoded diff from the trigger workflow
|
||||
if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then
|
||||
if ! echo "${{ github.event.client_payload.diff_base64 }}" | base64 -d > /tmp/i18n-diff.txt 2>&1; then
|
||||
echo "Warning: Failed to decode base64 diff payload" >&2
|
||||
echo "" > /tmp/i18n-diff.txt
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
elif [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "" > /tmp/i18n-diff.txt
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Unsupported event type: ${{ github.event_name }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Truncate diff if too large (keep first 50KB)
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import concurrent.futures
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any
|
||||
|
||||
@@ -14,7 +13,7 @@ from core.model_runtime.entities.model_entities import ModelType
|
||||
from core.rag.data_post_processor.data_post_processor import DataPostProcessor
|
||||
from core.rag.datasource.keyword.keyword_factory import Keyword
|
||||
from core.rag.datasource.vdb.vector_factory import Vector
|
||||
from core.rag.embedding.retrieval import RetrievalChildChunk, RetrievalSegments
|
||||
from core.rag.embedding.retrieval import RetrievalSegments
|
||||
from core.rag.entities.metadata_entities import MetadataCondition
|
||||
from core.rag.index_processor.constant.doc_type import DocType
|
||||
from core.rag.index_processor.constant.index_type import IndexStructureType
|
||||
@@ -37,8 +36,6 @@ default_retrieval_model = {
|
||||
"score_threshold_enabled": False,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RetrievalService:
|
||||
# Cache precompiled regular expressions to avoid repeated compilation
|
||||
@@ -109,12 +106,7 @@ class RetrievalService:
|
||||
)
|
||||
)
|
||||
|
||||
if futures:
|
||||
for future in concurrent.futures.as_completed(futures, timeout=3600):
|
||||
if exceptions:
|
||||
for f in futures:
|
||||
f.cancel()
|
||||
break
|
||||
concurrent.futures.wait(futures, timeout=3600, return_when=concurrent.futures.ALL_COMPLETED)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError(";\n".join(exceptions))
|
||||
@@ -218,7 +210,6 @@ class RetrievalService:
|
||||
)
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@classmethod
|
||||
@@ -312,7 +303,6 @@ class RetrievalService:
|
||||
else:
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@classmethod
|
||||
@@ -361,7 +351,6 @@ class RetrievalService:
|
||||
else:
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@staticmethod
|
||||
@@ -427,12 +416,12 @@ class RetrievalService:
|
||||
child_index_node_ids = [i for i in child_index_node_ids if i]
|
||||
index_node_ids = [i for i in index_node_ids if i]
|
||||
|
||||
segment_ids: list[str] = []
|
||||
segment_ids = []
|
||||
index_node_segments: list[DocumentSegment] = []
|
||||
segments: list[DocumentSegment] = []
|
||||
attachment_map: dict[str, list[dict[str, Any]]] = {}
|
||||
child_chunk_map: dict[str, list[ChildChunk]] = {}
|
||||
doc_segment_map: dict[str, list[str]] = {}
|
||||
attachment_map = {}
|
||||
child_chunk_map: dict[Any, Any] = {}
|
||||
doc_segment_map = {}
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
attachments = cls.get_segment_attachment_infos(image_doc_ids, session)
|
||||
@@ -443,7 +432,7 @@ class RetrievalService:
|
||||
attachment_map[attachment["segment_id"]].append(attachment["attachment_info"])
|
||||
else:
|
||||
attachment_map[attachment["segment_id"]] = [attachment["attachment_info"]]
|
||||
if attachment["segment_id"] in doc_segment_map:
|
||||
if attachment["attachment_id"] in doc_segment_map:
|
||||
doc_segment_map[attachment["segment_id"]].append(attachment["attachment_id"])
|
||||
else:
|
||||
doc_segment_map[attachment["segment_id"]] = [attachment["attachment_id"]]
|
||||
@@ -513,7 +502,7 @@ class RetrievalService:
|
||||
"child_chunks": child_chunk_details,
|
||||
}
|
||||
segment_child_map[segment.id] = map_detail
|
||||
record: dict[str, Any] = {
|
||||
record = {
|
||||
"segment": segment,
|
||||
}
|
||||
records.append(record)
|
||||
@@ -521,13 +510,13 @@ class RetrievalService:
|
||||
if segment.id not in include_segment_ids:
|
||||
include_segment_ids.add(segment.id)
|
||||
max_score = 0.0
|
||||
segment_document = doc_to_document_map.get(segment.index_node_id)
|
||||
if segment_document:
|
||||
max_score = max(max_score, segment_document.metadata.get("score", 0.0))
|
||||
document = doc_to_document_map.get(segment.index_node_id)
|
||||
if document:
|
||||
max_score = max(max_score, document.metadata.get("score", 0.0))
|
||||
for attachment_info in attachment_infos:
|
||||
file_doc = doc_to_document_map.get(attachment_info["id"])
|
||||
if file_doc:
|
||||
max_score = max(max_score, file_doc.metadata.get("score", 0.0))
|
||||
file_document = doc_to_document_map.get(attachment_info["id"])
|
||||
if file_document:
|
||||
max_score = max(max_score, file_document.metadata.get("score", 0.0))
|
||||
record = {
|
||||
"segment": segment,
|
||||
"score": max_score,
|
||||
@@ -542,26 +531,18 @@ class RetrievalService:
|
||||
if record["segment"].id in attachment_map:
|
||||
record["files"] = attachment_map[record["segment"].id] # type: ignore[assignment]
|
||||
|
||||
result: list[RetrievalSegments] = []
|
||||
result = []
|
||||
for record in records:
|
||||
# Extract segment
|
||||
segment = record["segment"]
|
||||
|
||||
# Extract child_chunks, ensuring it's a list or None
|
||||
raw_child_chunks = record.get("child_chunks")
|
||||
child_chunks_list: list[RetrievalChildChunk] | None = None
|
||||
if isinstance(raw_child_chunks, list):
|
||||
# Sort by score descending
|
||||
sorted_chunks = sorted(raw_child_chunks, key=lambda x: x.get("score", 0.0), reverse=True)
|
||||
child_chunks_list = [
|
||||
RetrievalChildChunk(
|
||||
id=chunk["id"],
|
||||
content=chunk["content"],
|
||||
score=chunk.get("score", 0.0),
|
||||
position=chunk["position"],
|
||||
)
|
||||
for chunk in sorted_chunks
|
||||
]
|
||||
child_chunks = record.get("child_chunks")
|
||||
if not isinstance(child_chunks, list):
|
||||
child_chunks = None
|
||||
|
||||
if child_chunks:
|
||||
child_chunks = sorted(child_chunks, key=lambda x: x.get("score", 0.0), reverse=True)
|
||||
|
||||
# Extract files, ensuring it's a list or None
|
||||
files = record.get("files")
|
||||
@@ -578,11 +559,11 @@ class RetrievalService:
|
||||
|
||||
# Create RetrievalSegments object
|
||||
retrieval_segment = RetrievalSegments(
|
||||
segment=segment, child_chunks=child_chunks_list, score=score, files=files
|
||||
segment=segment, child_chunks=child_chunks, score=score, files=files
|
||||
)
|
||||
result.append(retrieval_segment)
|
||||
|
||||
return sorted(result, key=lambda x: x.score if x.score is not None else 0.0, reverse=True)
|
||||
return sorted(result, key=lambda x: x.score, reverse=True)
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
@@ -674,14 +655,7 @@ class RetrievalService:
|
||||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
)
|
||||
# Use as_completed for early error propagation - cancel remaining futures on first error
|
||||
if futures:
|
||||
for future in concurrent.futures.as_completed(futures, timeout=300):
|
||||
if future.exception():
|
||||
# Cancel remaining futures to avoid unnecessary waiting
|
||||
for f in futures:
|
||||
f.cancel()
|
||||
break
|
||||
concurrent.futures.wait(futures, timeout=300, return_when=concurrent.futures.ALL_COMPLETED)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError(";\n".join(exceptions))
|
||||
|
||||
@@ -6,6 +6,7 @@ from .create_site_record_when_app_created import handle as handle_create_site_re
|
||||
from .delete_tool_parameters_cache_when_sync_draft_workflow import (
|
||||
handle as handle_delete_tool_parameters_cache_when_sync_draft_workflow,
|
||||
)
|
||||
from .queue_credential_sync_when_tenant_created import handle as handle_queue_credential_sync_when_tenant_created
|
||||
from .sync_plugin_trigger_when_app_created import handle as handle_sync_plugin_trigger_when_app_created
|
||||
from .sync_webhook_when_app_created import handle as handle_sync_webhook_when_app_created
|
||||
from .sync_workflow_schedule_when_app_published import handle as handle_sync_workflow_schedule_when_app_published
|
||||
@@ -33,6 +34,7 @@ __all__ = [
|
||||
"handle_create_installed_app_when_app_created",
|
||||
"handle_create_site_record_when_app_created",
|
||||
"handle_delete_tool_parameters_cache_when_sync_draft_workflow",
|
||||
"handle_queue_credential_sync_when_tenant_created",
|
||||
"handle_sync_plugin_trigger_when_app_created",
|
||||
"handle_sync_webhook_when_app_created",
|
||||
"handle_sync_workflow_schedule_when_app_published",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from configs import dify_config
|
||||
from events.tenant_event import tenant_was_created
|
||||
from services.enterprise.workspace_sync import WorkspaceSyncService
|
||||
|
||||
|
||||
@tenant_was_created.connect
|
||||
def handle(sender, **kwargs):
|
||||
"""Queue credential sync when a tenant/workspace is created."""
|
||||
# Only queue sync tasks if plugin manager (enterprise feature) is enabled
|
||||
if not dify_config.ENTERPRISE_ENABLED:
|
||||
return
|
||||
|
||||
tenant = sender
|
||||
|
||||
# Determine source from kwargs if available, otherwise use generic
|
||||
source = kwargs.get("source", "tenant_created")
|
||||
|
||||
# Queue credential sync task to Redis for enterprise backend to process
|
||||
WorkspaceSyncService.queue_credential_sync(tenant.id, source=source)
|
||||
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from redis import RedisError
|
||||
|
||||
from extensions.ext_redis import redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WORKSPACE_SYNC_QUEUE = "enterprise:workspace:sync:queue"
|
||||
WORKSPACE_SYNC_PROCESSING = "enterprise:workspace:sync:processing"
|
||||
|
||||
|
||||
class WorkspaceSyncService:
|
||||
"""Service to publish workspace sync tasks to Redis queue for enterprise backend consumption"""
|
||||
|
||||
@staticmethod
|
||||
def queue_credential_sync(workspace_id: str, *, source: str) -> bool:
|
||||
"""
|
||||
Queue a credential sync task for a newly created workspace.
|
||||
|
||||
This publishes a task to Redis that will be consumed by the enterprise backend
|
||||
worker to sync credentials with the plugin-manager.
|
||||
|
||||
Args:
|
||||
workspace_id: The workspace/tenant ID to sync credentials for
|
||||
source: Source of the sync request (for debugging/tracking)
|
||||
|
||||
Returns:
|
||||
bool: True if task was queued successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
task = {
|
||||
"task_id": str(uuid.uuid4()),
|
||||
"workspace_id": workspace_id,
|
||||
"retry_count": 0,
|
||||
"created_at": datetime.now(UTC).isoformat(),
|
||||
"source": source,
|
||||
}
|
||||
|
||||
# Push to Redis list (queue) - LPUSH adds to the head, worker consumes from tail with RPOP
|
||||
redis_client.lpush(WORKSPACE_SYNC_QUEUE, json.dumps(task))
|
||||
|
||||
logger.info(
|
||||
"Queued credential sync task for workspace %s, task_id: %s, source: %s",
|
||||
workspace_id,
|
||||
task["task_id"],
|
||||
source,
|
||||
)
|
||||
return True
|
||||
|
||||
except (RedisError, TypeError) as e:
|
||||
logger.error("Failed to queue credential sync for workspace %s: %s", workspace_id, str(e), exc_info=True)
|
||||
# Don't raise - we don't want to fail workspace creation if queueing fails
|
||||
# The scheduled task will catch it later
|
||||
return False
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# base image
|
||||
FROM node:22-alpine3.21 AS base
|
||||
FROM node:24-alpine AS base
|
||||
LABEL maintainer="takatost@gmail.com"
|
||||
|
||||
# if you located in China, you can use aliyun mirror to speed up
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
|
||||
Before starting the web frontend service, please make sure the following environment is ready.
|
||||
|
||||
- [Node.js](https://nodejs.org) >= v22.11.x
|
||||
- [pnpm](https://pnpm.io) v10.x
|
||||
- [Node.js](https://nodejs.org)
|
||||
- [pnpm](https://pnpm.io)
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
@@ -60,7 +61,10 @@ const Apps = ({
|
||||
}
|
||||
|
||||
const [currentType, setCurrentType] = useState<AppModeEnum[]>([])
|
||||
const [currCategory, setCurrCategory] = useState<AppCategories | string>(allCategoriesEn)
|
||||
const [currCategory, setCurrCategory] = useTabSearchParams({
|
||||
defaultTab: allCategoriesEn,
|
||||
disableSearchParams: true,
|
||||
})
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -139,7 +143,7 @@ const Apps = ({
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
@@ -149,7 +153,7 @@ const Apps = ({
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +169,7 @@ const Apps = ({
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between border-b border-divider-burn py-3">
|
||||
<div className="min-w-[180px] pl-5">
|
||||
<span className="title-xl-semi-bold text-text-primary">{t('newApp.startFromTemplate', { ns: 'app' })}</span>
|
||||
<span className="title-xl-semi-bold text-text-primary">{t('app.newApp.startFromTemplate')}</span>
|
||||
</div>
|
||||
<div className="flex max-w-[548px] flex-1 items-center rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-1.5 shadow-md">
|
||||
<AppTypeSelector value={currentType} onChange={setCurrentType} />
|
||||
@@ -176,7 +180,7 @@ const Apps = ({
|
||||
showClearIcon
|
||||
wrapperClassName="w-full flex-1"
|
||||
className="bg-transparent hover:border-transparent hover:bg-transparent focus:border-transparent focus:bg-transparent focus:shadow-none"
|
||||
placeholder={t('newAppFromTemplate.searchAllTemplate', { ns: 'app' }) as string}
|
||||
placeholder={t('app.newAppFromTemplate.searchAllTemplate') as string}
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
@@ -195,7 +199,7 @@ const Apps = ({
|
||||
<>
|
||||
<div className="pb-1 pt-4">
|
||||
{searchKeywords
|
||||
? <p className="title-md-semi-bold text-text-tertiary">{searchFilteredList.length > 1 ? t('newApp.foundResults', { ns: 'app', count: searchFilteredList.length }) : t('newApp.foundResult', { ns: 'app', count: searchFilteredList.length })}</p>
|
||||
? <p className="title-md-semi-bold text-text-tertiary">{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}</p>
|
||||
: (
|
||||
<div className="flex h-[22px] items-center">
|
||||
<AppCategoryLabel category={currCategory as AppCategories} className="title-md-semi-bold text-text-primary" />
|
||||
@@ -250,8 +254,8 @@ function NoTemplateFound() {
|
||||
<div className="mb-2 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-components-card-bg shadow-lg">
|
||||
<RiRobot2Line className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
<p className="title-md-semi-bold text-text-primary">{t('newApp.noTemplateFound', { ns: 'app' })}</p>
|
||||
<p className="system-sm-regular text-text-tertiary">{t('newApp.noTemplateFoundTip', { ns: 'app' })}</p>
|
||||
<p className="title-md-semi-bold text-text-primary">{t('app.newApp.noTemplateFound')}</p>
|
||||
<p className="system-sm-regular text-text-tertiary">{t('app.newApp.noTemplateFoundTip')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,15 +20,6 @@ type IOperationProps = {
|
||||
onOperate: () => void
|
||||
}
|
||||
|
||||
const roleI18nKeyMap = {
|
||||
admin: { label: 'members.admin', tip: 'members.adminTip' },
|
||||
editor: { label: 'members.editor', tip: 'members.editorTip' },
|
||||
normal: { label: 'members.normal', tip: 'members.normalTip' },
|
||||
dataset_operator: { label: 'members.datasetOperator', tip: 'members.datasetOperatorTip' },
|
||||
} as const
|
||||
|
||||
type OperationRoleKey = keyof typeof roleI18nKeyMap
|
||||
|
||||
const Operation = ({
|
||||
member,
|
||||
operatorRole,
|
||||
@@ -38,37 +29,38 @@ const Operation = ({
|
||||
const { t } = useTranslation()
|
||||
const { datasetOperatorEnabled } = useProviderContext()
|
||||
const RoleMap = {
|
||||
owner: t('members.owner', { ns: 'common' }),
|
||||
admin: t('members.admin', { ns: 'common' }),
|
||||
editor: t('members.editor', { ns: 'common' }),
|
||||
normal: t('members.normal', { ns: 'common' }),
|
||||
dataset_operator: t('members.datasetOperator', { ns: 'common' }),
|
||||
owner: t('common.members.owner'),
|
||||
admin: t('common.members.admin'),
|
||||
editor: t('common.members.editor'),
|
||||
normal: t('common.members.normal'),
|
||||
dataset_operator: t('common.members.datasetOperator'),
|
||||
}
|
||||
const roleList = useMemo((): OperationRoleKey[] => {
|
||||
const roleList = useMemo(() => {
|
||||
if (operatorRole === 'owner') {
|
||||
return [
|
||||
'admin',
|
||||
'editor',
|
||||
'normal',
|
||||
...(datasetOperatorEnabled ? ['dataset_operator'] as const : []),
|
||||
...(datasetOperatorEnabled ? ['dataset_operator'] : []),
|
||||
]
|
||||
}
|
||||
if (operatorRole === 'admin') {
|
||||
return [
|
||||
'editor',
|
||||
'normal',
|
||||
...(datasetOperatorEnabled ? ['dataset_operator'] as const : []),
|
||||
...(datasetOperatorEnabled ? ['dataset_operator'] : []),
|
||||
]
|
||||
}
|
||||
return []
|
||||
}, [operatorRole, datasetOperatorEnabled])
|
||||
const { notify } = useContext(ToastContext)
|
||||
const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
|
||||
const handleDeleteMemberOrCancelInvitation = async () => {
|
||||
setOpen(false)
|
||||
try {
|
||||
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
|
||||
onOperate()
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -82,7 +74,7 @@ const Operation = ({
|
||||
body: { role },
|
||||
})
|
||||
onOperate()
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -114,8 +106,8 @@ const Operation = ({
|
||||
: <div className="mr-1 mt-[2px] h-4 w-4 text-text-accent" />
|
||||
}
|
||||
<div>
|
||||
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t(roleI18nKeyMap[role].label, { ns: 'common' })}</div>
|
||||
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t(roleI18nKeyMap[role].tip, { ns: 'common' })}</div>
|
||||
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t(`common.members.${toHump(role)}` as any)}</div>
|
||||
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t(`common.members.${toHump(role)}Tip` as any)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@@ -125,8 +117,8 @@ const Operation = ({
|
||||
<div className="flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover" onClick={handleDeleteMemberOrCancelInvitation}>
|
||||
<div className="mr-1 mt-[2px] h-4 w-4 text-text-accent" />
|
||||
<div>
|
||||
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t('members.removeFromTeam', { ns: 'common' })}</div>
|
||||
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t('members.removeFromTeamTip', { ns: 'common' })}</div>
|
||||
<div className="system-sm-semibold whitespace-nowrap text-text-secondary">{t('common.members.removeFromTeam')}</div>
|
||||
<div className="system-xs-regular whitespace-nowrap text-text-tertiary">{t('common.members.removeFromTeamTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-51
@@ -1,60 +1,37 @@
|
||||
import type { i18n as I18nInstance, Resource, ResourceLanguage } from 'i18next'
|
||||
import type { Locale } from '.'
|
||||
import type { NamespaceCamelCase, NamespaceKebabCase } from './resources'
|
||||
import type { Namespace } from './i18next-config'
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
import { kebabCase } from 'es-toolkit/compat'
|
||||
import { camelCase } from 'es-toolkit/string'
|
||||
import { createInstance } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
import Negotiator from 'negotiator'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { cache } from 'react'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import { serverOnlyContext } from '@/utils/server-only-context'
|
||||
import { i18n } from '.'
|
||||
import { namespacesKebabCase } from './resources'
|
||||
import { getInitOptions } from './settings'
|
||||
|
||||
const [getLocaleCache, setLocaleCache] = serverOnlyContext<Locale | null>(null)
|
||||
const [getI18nInstance, setI18nInstance] = serverOnlyContext<I18nInstance | null>(null)
|
||||
|
||||
const getOrCreateI18next = async (lng: Locale) => {
|
||||
let instance = getI18nInstance()
|
||||
if (instance)
|
||||
return instance
|
||||
|
||||
instance = createInstance()
|
||||
await instance
|
||||
// https://locize.com/blog/next-13-app-dir-i18n/
|
||||
const initI18next = async (lng: Locale, ns: Namespace) => {
|
||||
const i18nInstance = createInstance()
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(resourcesToBackend((language: Locale, namespace: NamespaceCamelCase | NamespaceKebabCase) => {
|
||||
const fileNamespace = kebabCase(namespace) as NamespaceKebabCase
|
||||
return import(`../i18n/${language}/${fileNamespace}.json`)
|
||||
}))
|
||||
.use(resourcesToBackend((language: Locale, namespace: Namespace) => import(`../i18n/${language}/${namespace}.ts`)))
|
||||
.init({
|
||||
...getInitOptions(),
|
||||
lng,
|
||||
lng: lng === 'zh-Hans' ? 'zh-Hans' : lng,
|
||||
ns,
|
||||
fallbackLng: 'en-US',
|
||||
})
|
||||
setI18nInstance(instance)
|
||||
return instance
|
||||
return i18nInstance
|
||||
}
|
||||
|
||||
export async function getTranslation(lng: Locale, ns?: NamespaceCamelCase) {
|
||||
const i18nextInstance = await getOrCreateI18next(lng)
|
||||
|
||||
if (ns && !i18nextInstance.hasLoadedNamespace(ns))
|
||||
await i18nextInstance.loadNamespaces(ns)
|
||||
|
||||
export async function getTranslation(lng: Locale, ns: Namespace, options: Record<string, any> = {}) {
|
||||
const i18nextInstance = await initI18next(lng, ns)
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, ns),
|
||||
// @ts-expect-error types mismatch
|
||||
t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
|
||||
i18n: i18nextInstance,
|
||||
}
|
||||
}
|
||||
|
||||
export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
const cached = getLocaleCache()
|
||||
if (cached)
|
||||
return cached
|
||||
|
||||
const locales: string[] = i18n.locales
|
||||
|
||||
let languages: string[] | undefined
|
||||
@@ -76,19 +53,5 @@ export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
|
||||
// match locale
|
||||
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
|
||||
setLocaleCache(matchedLocale)
|
||||
return matchedLocale
|
||||
}
|
||||
|
||||
export const getResources = cache(async (lng: Locale): Promise<Resource> => {
|
||||
const messages = {} as ResourceLanguage
|
||||
|
||||
await Promise.all(
|
||||
(namespacesKebabCase).map(async (ns) => {
|
||||
const mod = await import(`../i18n/${lng}/${ns}.json`)
|
||||
messages[camelCase(ns)] = mod.default
|
||||
}),
|
||||
)
|
||||
|
||||
return { [lng]: messages }
|
||||
})
|
||||
|
||||
+22
-20
@@ -10,9 +10,6 @@
|
||||
"default": "./i18n-config/lib.client.ts"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v22.11.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 Chrome version",
|
||||
"last 1 Firefox version",
|
||||
@@ -37,14 +34,14 @@
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-check:tsgo": "tsgo --noEmit",
|
||||
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky",
|
||||
"gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/",
|
||||
"gen-icons": "node ./app/components/base/icons/script.mjs",
|
||||
"uglify-embed": "node ./bin/uglify-embed",
|
||||
"i18n:check": "tsx ./scripts/check-i18n.js",
|
||||
"check-i18n": "tsx ./i18n-config/check-i18n.js",
|
||||
"auto-gen-i18n": "tsx ./i18n-config/auto-gen-i18n.js",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:watch": "vitest --watch",
|
||||
"analyze-component": "node ./scripts/analyze-component.js",
|
||||
"refactor-component": "node ./scripts/refactor-component.js",
|
||||
"analyze-component": "node testing/analyze-component.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -52,13 +49,14 @@
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.33.1",
|
||||
"@amplitude/analytics-browser": "^2.31.3",
|
||||
"@amplitude/plugin-session-replay-browser": "^1.23.6",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@formatjs/intl-localematcher": "^0.5.10",
|
||||
"@headlessui/react": "2.2.1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lexical/code": "^0.38.2",
|
||||
"@lexical/link": "^0.38.2",
|
||||
"@lexical/list": "^0.38.2",
|
||||
@@ -75,6 +73,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-form": "^1.23.7",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"abcjs": "^6.5.2",
|
||||
"ahooks": "^3.9.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -93,7 +92,7 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"html-entities": "^2.6.0",
|
||||
"html-to-image": "1.11.13",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"immer": "^11.1.0",
|
||||
"jotai": "^2.16.1",
|
||||
@@ -116,13 +115,14 @@
|
||||
"nuqs": "^2.8.6",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"qs": "^6.14.1",
|
||||
"qs": "^6.14.0",
|
||||
"react": "19.2.3",
|
||||
"react-18-input-autosize": "^3.0.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-easy-crop": "^5.5.3",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-hotkeys-hook": "^4.6.2",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-multi-email": "^1.0.25",
|
||||
"react-papaparse": "^4.4.0",
|
||||
@@ -138,11 +138,11 @@
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"scheduler": "^0.27.0",
|
||||
"scheduler": "^0.26.0",
|
||||
"semver": "^7.7.3",
|
||||
"sharp": "^0.33.5",
|
||||
"sortablejs": "^1.15.6",
|
||||
"string-ts": "^2.3.1",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tldts": "^7.0.17",
|
||||
"use-context-selector": "^2.0.0",
|
||||
@@ -169,10 +169,6 @@
|
||||
"@storybook/addon-themes": "9.1.13",
|
||||
"@storybook/nextjs": "9.1.13",
|
||||
"@storybook/react": "9.1.13",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@tanstack/react-devtools": "^0.9.0",
|
||||
"@tanstack/react-form-devtools": "^0.2.9",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -196,6 +192,7 @@
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"bing-translate-api": "^4.1.0",
|
||||
"code-inspector-plugin": "1.2.9",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
@@ -205,10 +202,13 @@
|
||||
"eslint-plugin-storybook": "^10.1.10",
|
||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||
"husky": "^9.1.7",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"jsdom": "^27.3.0",
|
||||
"jsdom-testing-mocks": "^1.16.0",
|
||||
"knip": "^5.78.0",
|
||||
"knip": "^5.66.1",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash": "^4.17.21",
|
||||
"magicast": "^0.3.5",
|
||||
"nock": "^14.0.10",
|
||||
"postcss": "^8.5.6",
|
||||
"react-scan": "^0.4.3",
|
||||
@@ -220,7 +220,8 @@
|
||||
"uglify-js": "^3.19.3",
|
||||
"vite": "^7.3.0",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.16"
|
||||
"vitest": "^4.0.16",
|
||||
"vitest-localstorage-mock": "^0.1.2"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
@@ -237,7 +238,8 @@
|
||||
"brace-expansion@<2.0.2": "2.0.2",
|
||||
"devalue@<5.3.2": "5.3.2",
|
||||
"es-iterator-helpers": "npm:@nolyfill/es-iterator-helpers@^1",
|
||||
"esbuild@<0.25.0": "0.25.0",
|
||||
"esbuild@<0.27.2": "0.27.2",
|
||||
"glob@>=10.2.0,<10.5.0": "11.1.0",
|
||||
"hasown": "npm:@nolyfill/hasown@^1",
|
||||
"is-arguments": "npm:@nolyfill/is-arguments@^1",
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1",
|
||||
|
||||
Generated
+535
-1012
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user