mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 21:11:16 +08:00
fix: add is_cloud_only for templates (#37846)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
ce6297bed2
commit
fe62177ba5
@ -36,6 +36,9 @@ FILES_ACCESS_TIMEOUT=300
|
||||
# Collaboration mode toggle
|
||||
ENABLE_COLLABORATION_MODE=true
|
||||
|
||||
# Learn app feature toggle
|
||||
ENABLE_LEARN_APP=true
|
||||
|
||||
# Access token expiration time in minutes
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
|
||||
|
||||
@ -1073,6 +1073,12 @@ class MailConfig(BaseSettings):
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
class HomepageConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for homepage feature toggles exposed through system features.
|
||||
"""
|
||||
|
||||
ENABLE_TRIAL_APP: bool = Field(
|
||||
description="Enable trial app",
|
||||
default=False,
|
||||
@ -1083,6 +1089,11 @@ class MailConfig(BaseSettings):
|
||||
default=False,
|
||||
)
|
||||
|
||||
ENABLE_LEARN_APP: bool = Field(
|
||||
description="Enable Learn App",
|
||||
default=True,
|
||||
)
|
||||
|
||||
|
||||
class RagEtlConfig(BaseSettings):
|
||||
"""
|
||||
@ -1489,6 +1500,7 @@ class FeatureConfig(
|
||||
EndpointConfig,
|
||||
FileAccessConfig,
|
||||
FileUploadConfig,
|
||||
HomepageConfig,
|
||||
HttpConfig,
|
||||
InnerAPIConfig,
|
||||
IndexingConfig,
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
"""add cloud only flag to recommended apps
|
||||
|
||||
Revision ID: d9e8f7a6b5c4
|
||||
Revises: c8f4a6b2d3e1
|
||||
Create Date: 2026-06-23 18:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d9e8f7a6b5c4"
|
||||
down_revision = "c8f4a6b2d3e1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("is_cloud_only", sa.Boolean(), server_default=sa.text("false"), nullable=False))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
|
||||
batch_op.drop_column("is_cloud_only")
|
||||
@ -925,6 +925,9 @@ class RecommendedApp(TypeBase):
|
||||
is_learn_dify: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
is_cloud_only: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
||||
language: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
|
||||
@ -19605,6 +19605,7 @@ Model class for provider system configuration response.
|
||||
| enable_email_code_login | boolean | | Yes |
|
||||
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_explore_banner | boolean | | Yes |
|
||||
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_marketplace | boolean | | Yes |
|
||||
| enable_social_oauth_login | boolean | | Yes |
|
||||
| enable_trial_app | boolean | | Yes |
|
||||
|
||||
@ -1603,6 +1603,7 @@ Default configuration for form inputs.
|
||||
| enable_email_code_login | boolean | | Yes |
|
||||
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_explore_banner | boolean | | Yes |
|
||||
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_marketplace | boolean | | Yes |
|
||||
| enable_social_oauth_login | boolean | | Yes |
|
||||
| enable_trial_app | boolean | | Yes |
|
||||
|
||||
@ -181,6 +181,7 @@ class SystemFeatureModel(FeatureResponseModel):
|
||||
enable_creators_platform: bool = False
|
||||
enable_trial_app: bool = False
|
||||
enable_explore_banner: bool = False
|
||||
enable_learn_app: bool = True
|
||||
rbac_enabled: bool = False
|
||||
|
||||
|
||||
@ -282,6 +283,7 @@ class FeatureService:
|
||||
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
||||
system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP
|
||||
system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER
|
||||
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
||||
|
||||
@classmethod
|
||||
def _fulfill_trial_models_from_env(cls) -> list[str]:
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import Any, override
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
|
||||
from services.recommend_app.recommend_app_base import RecommendAppRetrievalBase
|
||||
from services.recommend_app.recommend_app_type import RecommendAppType
|
||||
|
||||
@ -25,6 +26,11 @@ class BuildInRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
result = self.fetch_recommended_apps_from_builtin(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_learn_dify_apps(self, language: str):
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_recommend_app_detail(self, app_id: str):
|
||||
result = self.fetch_recommended_app_detail_from_builtin(app_id)
|
||||
|
||||
@ -49,6 +49,11 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
result = self.fetch_recommended_apps_from_db(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_learn_dify_apps(self, language: str) -> RecommendedAppsResultDict:
|
||||
result = self.fetch_learn_dify_apps_from_db(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_recommend_app_detail(self, app_id: str) -> RecommendedAppDetailDict | None:
|
||||
result = self.fetch_recommended_app_detail_from_db(app_id)
|
||||
|
||||
@ -6,6 +6,8 @@ class RecommendAppRetrievalBase(Protocol):
|
||||
|
||||
def get_recommended_apps_and_categories(self, language: str) -> Any: ...
|
||||
|
||||
def get_learn_dify_apps(self, language: str) -> Any: ...
|
||||
|
||||
def get_recommend_app_detail(self, app_id: str) -> Any: ...
|
||||
|
||||
def get_type(self) -> str: ...
|
||||
|
||||
@ -2,15 +2,28 @@ import logging
|
||||
from typing import Any, override
|
||||
|
||||
import httpx
|
||||
from flask import has_request_context, request
|
||||
|
||||
from configs import dify_config
|
||||
from services.recommend_app.buildin.buildin_retrieval import BuildInRecommendAppRetrieval
|
||||
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
|
||||
from services.recommend_app.recommend_app_base import RecommendAppRetrievalBase
|
||||
from services.recommend_app.recommend_app_type import RecommendAppType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _current_origin_headers() -> dict[str, str]:
|
||||
origin = request.headers.get("Origin") if has_request_context() else None
|
||||
if origin:
|
||||
return {"Origin": origin}
|
||||
|
||||
console_web_url = getattr(dify_config, "CONSOLE_WEB_URL", "")
|
||||
if not isinstance(console_web_url, str) or not console_web_url:
|
||||
return {}
|
||||
return {"Origin": console_web_url}
|
||||
|
||||
|
||||
class RemoteRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
"""
|
||||
Retrieval recommended app from dify official.
|
||||
@ -37,6 +50,15 @@ class RemoteRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
result = BuildInRecommendAppRetrieval.fetch_recommended_apps_from_builtin(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_learn_dify_apps(self, language: str):
|
||||
try:
|
||||
result = self.fetch_learn_dify_apps_from_dify_official(language)
|
||||
except Exception as e:
|
||||
logger.warning("fetch learn dify apps from dify official failed: %s, switch to database.", e)
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)
|
||||
return result
|
||||
|
||||
@override
|
||||
def get_type(self) -> str:
|
||||
return RecommendAppType.REMOTE
|
||||
@ -50,7 +72,7 @@ class RemoteRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
"""
|
||||
domain = dify_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN
|
||||
url = f"{domain}/apps/{app_id}"
|
||||
response = httpx.get(url, timeout=httpx.Timeout(10.0, connect=3.0))
|
||||
response = httpx.get(url, headers=_current_origin_headers(), timeout=httpx.Timeout(10.0, connect=3.0))
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
data: dict[str, Any] = response.json()
|
||||
@ -65,9 +87,25 @@ class RemoteRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
"""
|
||||
domain = dify_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN
|
||||
url = f"{domain}/apps?language={language}"
|
||||
response = httpx.get(url, timeout=httpx.Timeout(10.0, connect=3.0))
|
||||
response = httpx.get(url, headers=_current_origin_headers(), timeout=httpx.Timeout(10.0, connect=3.0))
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"fetch recommended apps failed, status code: {response.status_code}")
|
||||
|
||||
result: dict[str, Any] = response.json()
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def fetch_learn_dify_apps_from_dify_official(cls, language: str):
|
||||
"""
|
||||
Fetch Learn Dify apps from dify official.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
domain = dify_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN
|
||||
url = f"{domain}/apps/learn-dify?language={language}"
|
||||
response = httpx.get(url, headers=_current_origin_headers(), timeout=httpx.Timeout(10.0, connect=3.0))
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"fetch learn dify apps failed, status code: {response.status_code}")
|
||||
|
||||
result: dict[str, Any] = response.json()
|
||||
return result
|
||||
|
||||
@ -6,7 +6,6 @@ from sqlalchemy.orm import scoped_session
|
||||
from configs import dify_config
|
||||
from models.model import AccountTrialAppRecord, TrialApp
|
||||
from services.feature_service import FeatureService
|
||||
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
|
||||
from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory
|
||||
|
||||
|
||||
@ -38,11 +37,13 @@ class RecommendedAppService:
|
||||
@classmethod
|
||||
def get_learn_dify_apps(cls, session: scoped_session, language: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get database-backed recommended apps marked as Learn Dify.
|
||||
Get recommended apps marked for the Learn Dify section.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)
|
||||
mode = dify_config.HOSTED_FETCH_APP_TEMPLATES_MODE
|
||||
retrieval_instance = RecommendAppRetrievalFactory.get_recommend_app_factory(mode)()
|
||||
result = retrieval_instance.get_learn_dify_apps(language)
|
||||
|
||||
if FeatureService.get_system_features().enable_trial_app:
|
||||
for app in result["recommended_apps"]:
|
||||
|
||||
@ -267,36 +267,45 @@ class TestRecommendedAppServiceGetDetail:
|
||||
|
||||
|
||||
class TestRecommendedAppServiceGetLearnDifyApps:
|
||||
def test_returns_database_learn_dify_apps_without_remote_factory(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@patch("services.recommended_app_service.FeatureService", autospec=True)
|
||||
@patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True)
|
||||
@patch("services.recommended_app_service.dify_config")
|
||||
def test_uses_configured_retrieval_source(
|
||||
self, mock_config: MagicMock, mock_factory_class: MagicMock, mock_feature_service: MagicMock
|
||||
) -> None:
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote"
|
||||
mock_feature_service.get_system_features.return_value = SimpleNamespace(enable_trial_app=False)
|
||||
expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow")
|
||||
mock_database_retrieval = MagicMock()
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.get_learn_dify_apps.return_value = {
|
||||
"recommended_apps": [expected_app],
|
||||
"categories": ["Workflow"],
|
||||
}
|
||||
monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval)
|
||||
monkeypatch.setattr(
|
||||
service_module.FeatureService,
|
||||
"get_system_features",
|
||||
MagicMock(return_value=SimpleNamespace(enable_trial_app=False)),
|
||||
)
|
||||
factory_mock = MagicMock()
|
||||
monkeypatch.setattr(service_module.RecommendAppRetrievalFactory, "get_recommend_app_factory", factory_mock)
|
||||
mock_factory_class.get_recommend_app_factory.return_value = MagicMock(return_value=mock_instance)
|
||||
|
||||
result = RecommendedAppService.get_learn_dify_apps(db.session, "en-US")
|
||||
|
||||
assert result == {"recommended_apps": [expected_app]}
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.assert_called_once_with("en-US")
|
||||
factory_mock.assert_not_called()
|
||||
mock_factory_class.get_recommend_app_factory.assert_called_once_with("remote")
|
||||
mock_instance.get_learn_dify_apps.assert_called_once_with("en-US")
|
||||
|
||||
def test_sets_can_trial_when_trial_feature_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@patch("services.recommended_app_service.dify_config")
|
||||
def test_sets_can_trial_when_trial_feature_enabled(
|
||||
self, mock_config: MagicMock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "db"
|
||||
app = RecommendedAppPayload(app_id="app-1", category="Workflow")
|
||||
mock_database_retrieval = MagicMock()
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
|
||||
mock_retrieval_instance = MagicMock()
|
||||
mock_retrieval_instance.get_learn_dify_apps.return_value = {
|
||||
"recommended_apps": [app],
|
||||
"categories": ["Workflow"],
|
||||
}
|
||||
monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval)
|
||||
mock_retrieval_factory = MagicMock(return_value=mock_retrieval_instance)
|
||||
monkeypatch.setattr(
|
||||
service_module.RecommendAppRetrievalFactory,
|
||||
"get_recommend_app_factory",
|
||||
MagicMock(return_value=mock_retrieval_factory),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service_module.FeatureService,
|
||||
"get_system_features",
|
||||
|
||||
@ -94,7 +94,7 @@ class TestSystemFeatureApi:
|
||||
"controllers.console.feature.current_account_with_tenant_optional",
|
||||
return_value=(account, "tenant-123"),
|
||||
)
|
||||
system_features = SystemFeatureModel(is_allow_register=True)
|
||||
system_features = SystemFeatureModel(is_allow_register=True, enable_learn_app=True)
|
||||
get_system_features = mocker.patch(
|
||||
"controllers.console.feature.FeatureService.get_system_features",
|
||||
return_value=system_features,
|
||||
@ -104,6 +104,7 @@ class TestSystemFeatureApi:
|
||||
result = api.get()
|
||||
|
||||
assert result == system_features.model_dump()
|
||||
assert result["enable_learn_app"] is True
|
||||
current_account.assert_called_once_with()
|
||||
get_system_features.assert_called_once_with(is_authenticated=True)
|
||||
|
||||
|
||||
@ -42,6 +42,16 @@ class TestBuildInRecommendAppRetrieval:
|
||||
mock_fetch.assert_called_once_with("en-US")
|
||||
assert result == {"apps": []}
|
||||
|
||||
@patch("services.recommend_app.buildin.buildin_retrieval.DatabaseRecommendAppRetrieval")
|
||||
def test_get_learn_dify_apps_delegates_to_database(self, mock_database_retrieval):
|
||||
expected = {"recommended_apps": [{"id": "learn-dify-app"}]}
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = expected
|
||||
|
||||
result = BuildInRecommendAppRetrieval().get_learn_dify_apps("en-US")
|
||||
|
||||
assert result == expected
|
||||
mock_database_retrieval.fetch_learn_dify_apps_from_db.assert_called_once_with("en-US")
|
||||
|
||||
def test_get_recommend_app_detail_delegates(self):
|
||||
with patch.object(
|
||||
BuildInRecommendAppRetrieval,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from services.recommend_app.recommend_app_type import RecommendAppType
|
||||
from services.recommend_app.remote.remote_retrieval import RemoteRecommendAppRetrieval
|
||||
@ -58,6 +59,32 @@ class TestRemoteRecommendAppRetrieval:
|
||||
result = RemoteRecommendAppRetrieval().get_recommended_apps_and_categories("en-US")
|
||||
assert result == {"recommended_apps": [{"id": "builtin"}]}
|
||||
|
||||
@patch.object(
|
||||
RemoteRecommendAppRetrieval,
|
||||
"fetch_learn_dify_apps_from_dify_official",
|
||||
return_value={"recommended_apps": [{"id": "learn-dify-app"}]},
|
||||
)
|
||||
def test_get_learn_dify_apps_success(self, mock_fetch):
|
||||
result = RemoteRecommendAppRetrieval().get_learn_dify_apps("en-US")
|
||||
|
||||
assert result == {"recommended_apps": [{"id": "learn-dify-app"}]}
|
||||
mock_fetch.assert_called_once_with("en-US")
|
||||
|
||||
@patch(
|
||||
"services.recommend_app.remote.remote_retrieval.DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db",
|
||||
return_value={"recommended_apps": [{"id": "db-fallback"}]},
|
||||
)
|
||||
@patch.object(
|
||||
RemoteRecommendAppRetrieval,
|
||||
"fetch_learn_dify_apps_from_dify_official",
|
||||
side_effect=ValueError("server error"),
|
||||
)
|
||||
def test_get_learn_dify_apps_falls_back_to_database_on_error(self, mock_fetch, mock_database):
|
||||
result = RemoteRecommendAppRetrieval().get_learn_dify_apps("en-US")
|
||||
|
||||
assert result == {"recommended_apps": [{"id": "db-fallback"}]}
|
||||
mock_database.assert_called_once_with("en-US")
|
||||
|
||||
|
||||
class TestFetchFromDifyOfficial:
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@ -118,3 +145,84 @@ class TestFetchFromDifyOfficial:
|
||||
result = RemoteRecommendAppRetrieval.fetch_recommended_apps_from_dify_official("en-US")
|
||||
|
||||
assert "categories" not in result
|
||||
assert mock_get.call_args.kwargs["headers"] == {}
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_apps_forwards_request_origin_header(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_config.CONSOLE_WEB_URL = "https://saas.dify.dev"
|
||||
mock_response = MagicMock(status_code=200)
|
||||
mock_response.json.return_value = {"recommended_apps": []}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
with flask_app.test_request_context(headers={"Origin": "https://cloud.example.com"}):
|
||||
RemoteRecommendAppRetrieval.fetch_recommended_apps_from_dify_official("en-US")
|
||||
|
||||
assert mock_get.call_args.kwargs["headers"] == {"Origin": "https://cloud.example.com"}
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_apps_falls_back_to_console_web_url_origin(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_config.CONSOLE_WEB_URL = "https://saas.dify.dev/console"
|
||||
mock_response = MagicMock(status_code=200)
|
||||
mock_response.json.return_value = {"recommended_apps": []}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
with flask_app.test_request_context():
|
||||
RemoteRecommendAppRetrieval.fetch_recommended_apps_from_dify_official("en-US")
|
||||
|
||||
assert mock_get.call_args.kwargs["headers"] == {"Origin": "https://saas.dify.dev/console"}
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_apps_falls_back_to_console_web_url_without_request_context(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_config.CONSOLE_WEB_URL = "http://localhost:3000/console"
|
||||
mock_response = MagicMock(status_code=200)
|
||||
mock_response.json.return_value = {"recommended_apps": []}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
RemoteRecommendAppRetrieval.fetch_recommended_apps_from_dify_official("en-US")
|
||||
|
||||
assert mock_get.call_args.kwargs["headers"] == {"Origin": "http://localhost:3000/console"}
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_apps_uses_console_web_url_without_scheme(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_config.CONSOLE_WEB_URL = "saas.dify.dev"
|
||||
mock_response = MagicMock(status_code=200)
|
||||
mock_response.json.return_value = {"recommended_apps": []}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
with flask_app.test_request_context():
|
||||
RemoteRecommendAppRetrieval.fetch_recommended_apps_from_dify_official("en-US")
|
||||
|
||||
assert mock_get.call_args.kwargs["headers"] == {"Origin": "saas.dify.dev"}
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_learn_dify_apps_returns_json_on_200(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_response = MagicMock(status_code=200)
|
||||
mock_response.json.return_value = {"recommended_apps": [{"id": "learn-dify-app"}]}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = RemoteRecommendAppRetrieval.fetch_learn_dify_apps_from_dify_official("en-US")
|
||||
|
||||
assert result == {"recommended_apps": [{"id": "learn-dify-app"}]}
|
||||
assert mock_get.call_args.args[0] == "https://example.com/apps/learn-dify?language=en-US"
|
||||
|
||||
@patch("services.recommend_app.remote.remote_retrieval.dify_config")
|
||||
@patch("services.recommend_app.remote.remote_retrieval.httpx.get")
|
||||
def test_learn_dify_apps_raises_on_non_200(self, mock_get, mock_config):
|
||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_REMOTE_DOMAIN = "https://example.com"
|
||||
mock_get.return_value = MagicMock(status_code=500)
|
||||
|
||||
with pytest.raises(ValueError, match="fetch learn dify apps failed"):
|
||||
RemoteRecommendAppRetrieval.fetch_learn_dify_apps_from_dify_official("en-US")
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from services import feature_service as feature_service_module
|
||||
from services.feature_service import FeatureService, SystemFeatureModel
|
||||
|
||||
|
||||
def test_system_feature_model_defaults_enable_learn_app():
|
||||
assert SystemFeatureModel().enable_learn_app is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [True, False])
|
||||
def test_get_system_features_reads_enable_learn_app(monkeypatch: pytest.MonkeyPatch, enabled: bool):
|
||||
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LEARN_APP", enabled)
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
|
||||
assert result.enable_learn_app is enabled
|
||||
@ -41,6 +41,9 @@ FILES_ACCESS_TIMEOUT=300
|
||||
# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service.
|
||||
ENABLE_COLLABORATION_MODE=true
|
||||
|
||||
# Learn app feature toggle
|
||||
ENABLE_LEARN_APP=true
|
||||
|
||||
# Logging and server workers
|
||||
LOG_LEVEL=INFO
|
||||
LOG_OUTPUT_FORMAT=text
|
||||
|
||||
@ -18,6 +18,9 @@ MIGRATION_ENABLED=true
|
||||
FILES_ACCESS_TIMEOUT=300
|
||||
# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service.
|
||||
ENABLE_COLLABORATION_MODE=true
|
||||
|
||||
# Learn app feature toggle
|
||||
ENABLE_LEARN_APP=true
|
||||
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
||||
CELERY_TASK_ANNOTATIONS=null
|
||||
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
||||
|
||||
@ -13,6 +13,7 @@ export type SystemFeatureModel = {
|
||||
enable_email_code_login: boolean
|
||||
enable_email_password_login: boolean
|
||||
enable_explore_banner: boolean
|
||||
enable_learn_app: boolean
|
||||
enable_marketplace: boolean
|
||||
enable_social_oauth_login: boolean
|
||||
enable_trial_app: boolean
|
||||
|
||||
@ -105,6 +105,7 @@ export const zSystemFeatureModel = z.object({
|
||||
enable_email_code_login: z.boolean().default(false),
|
||||
enable_email_password_login: z.boolean().default(true),
|
||||
enable_explore_banner: z.boolean().default(false),
|
||||
enable_learn_app: z.boolean().default(true),
|
||||
enable_marketplace: z.boolean().default(false),
|
||||
enable_social_oauth_login: z.boolean().default(false),
|
||||
enable_trial_app: z.boolean().default(false),
|
||||
|
||||
@ -556,6 +556,7 @@ export type SystemFeatureModel = {
|
||||
enable_email_code_login: boolean
|
||||
enable_email_password_login: boolean
|
||||
enable_explore_banner: boolean
|
||||
enable_learn_app: boolean
|
||||
enable_marketplace: boolean
|
||||
enable_social_oauth_login: boolean
|
||||
enable_trial_app: boolean
|
||||
|
||||
@ -840,6 +840,7 @@ export const zSystemFeatureModel = z.object({
|
||||
enable_email_code_login: z.boolean().default(false),
|
||||
enable_email_password_login: z.boolean().default(true),
|
||||
enable_explore_banner: z.boolean().default(false),
|
||||
enable_learn_app: z.boolean().default(true),
|
||||
enable_marketplace: z.boolean().default(false),
|
||||
enable_social_oauth_login: z.boolean().default(false),
|
||||
enable_trial_app: z.boolean().default(false),
|
||||
|
||||
@ -379,6 +379,22 @@ describe('Apps', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide categories without templates even when the API returns them', () => {
|
||||
mockUseExploreAppList.mockReturnValueOnce({
|
||||
data: {
|
||||
categories: ['Cat A', 'v'],
|
||||
allList: [createAppEntry('Alpha', 'Cat A')],
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
|
||||
expect(screen.getByText('Cat A'))!.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'v' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Alpha'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should clear the search, hide the sidebar during search, and close the modal when requested', async () => {
|
||||
render(<Apps />)
|
||||
|
||||
|
||||
@ -77,14 +77,30 @@ const Apps = ({
|
||||
isLoading,
|
||||
} = useExploreAppList()
|
||||
|
||||
const visibleCategories = useMemo(() => {
|
||||
if (!data)
|
||||
return []
|
||||
|
||||
const categoriesWithApps = new Set<string>()
|
||||
data.allList.forEach((app) => {
|
||||
app.categories.forEach(category => categoriesWithApps.add(category))
|
||||
})
|
||||
|
||||
return data.categories.filter(category => categoriesWithApps.has(category))
|
||||
}, [data])
|
||||
|
||||
const activeCategory = visibleCategories.includes(currCategory)
|
||||
? currCategory
|
||||
: allCategoriesEn
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!data)
|
||||
return []
|
||||
const { allList } = data
|
||||
const filteredByCategory = allList.filter((item) => {
|
||||
if (currCategory === allCategoriesEn)
|
||||
if (activeCategory === allCategoriesEn)
|
||||
return true
|
||||
return item.categories?.includes(currCategory) ?? false
|
||||
return item.categories?.includes(activeCategory) ?? false
|
||||
})
|
||||
if (currentType.length === 0)
|
||||
return filteredByCategory
|
||||
@ -101,7 +117,7 @@ const Apps = ({
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [currentType, currCategory, allCategoriesEn, data])
|
||||
}, [currentType, activeCategory, allCategoriesEn, data])
|
||||
|
||||
const searchFilteredList = useMemo(() => {
|
||||
if (!searchKeywords || !filteredList || filteredList.length === 0)
|
||||
@ -189,7 +205,7 @@ const Apps = ({
|
||||
<div className="relative flex flex-1 overflow-y-auto">
|
||||
{!searchKeywords && (
|
||||
<div className="h-full w-[200px] p-4">
|
||||
<Sidebar current={currCategory as AppCategories} categories={data?.categories || []} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
<Sidebar current={activeCategory as AppCategories} categories={visibleCategories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full flex-1 shrink-0 grow overflow-auto border-l border-divider-burn p-6 pt-2">
|
||||
@ -200,7 +216,7 @@ const Apps = ({
|
||||
? <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>
|
||||
: (
|
||||
<div className="flex h-[22px] items-center">
|
||||
<AppCategoryLabel category={currCategory as AppCategories} className="title-md-semi-bold text-text-primary" />
|
||||
<AppCategoryLabel category={activeCategory as AppCategories} className="title-md-semi-bold text-text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
@ -324,10 +325,14 @@ beforeAll(() => {
|
||||
|
||||
// Render helper wrapping with shared nuqs testing helper plus a seeded
|
||||
// systemFeatures cache so List can resolve its useSuspenseQuery.
|
||||
const renderList = (searchParams = '') => {
|
||||
type RenderListOptions = {
|
||||
systemFeatures?: Partial<GetSystemFeaturesResponse>
|
||||
}
|
||||
|
||||
const renderList = (searchParams = '', options: RenderListOptions = {}) => {
|
||||
mockSearchParams = new URLSearchParams(searchParams)
|
||||
const { wrapper: SystemFeaturesWrapper } = createSystemFeaturesWrapper({
|
||||
systemFeatures: { branding: { enabled: false } },
|
||||
systemFeatures: { branding: { enabled: false }, ...options.systemFeatures },
|
||||
})
|
||||
return renderWithNuqs(<SystemFeaturesWrapper><List /></SystemFeaturesWrapper>, { searchParams })
|
||||
}
|
||||
@ -502,7 +507,7 @@ describe('List', () => {
|
||||
it('should render first empty state when there are no apps and no active filters', () => {
|
||||
mockAppData = { pages: [{ data: [], total: 0 }] }
|
||||
|
||||
renderList()
|
||||
renderList('', { systemFeatures: { enable_learn_app: true } })
|
||||
|
||||
expect(screen.getByText('app.firstEmpty.title'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('app.firstEmpty.learnDifyTitle'))!.toBeInTheDocument()
|
||||
@ -512,6 +517,15 @@ describe('List', () => {
|
||||
expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide learn dify in first empty state when learn app is disabled', () => {
|
||||
mockAppData = { pages: [{ data: [], total: 0 }] }
|
||||
|
||||
renderList('', { systemFeatures: { enable_learn_app: false } })
|
||||
|
||||
expect(screen.getByText('app.firstEmpty.title'))!.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.firstEmpty.learnDifyTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render first empty state before the first app list page resolves', () => {
|
||||
mockAppData = { pages: [] }
|
||||
|
||||
|
||||
@ -19,12 +19,14 @@ type Props = {
|
||||
onCreateBlank: () => void
|
||||
onCreateTemplate: () => void
|
||||
onImportDSL: () => void
|
||||
showLearnDify: boolean
|
||||
}
|
||||
|
||||
function FirstEmptyState({
|
||||
onCreateBlank,
|
||||
onCreateTemplate,
|
||||
onImportDSL,
|
||||
showLearnDify,
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -102,13 +104,15 @@ function FirstEmptyState({
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<LearnDify
|
||||
className="px-4 pt-2 pb-0 [&_div.grid]:gap-3 [&>div]:mx-0 [&>div]:rounded-t-2xl [&>div]:rounded-b-none [&>div]:px-5 [&>div]:pt-4 [&>div]:pb-5"
|
||||
dismissible={false}
|
||||
itemLimit={4}
|
||||
showDescription
|
||||
title={t('firstEmpty.learnDifyTitle', { ns: 'app' })}
|
||||
/>
|
||||
{showLearnDify && (
|
||||
<LearnDify
|
||||
className="px-4 pt-2 pb-0 [&_div.grid]:gap-3 [&>div]:mx-0 [&>div]:rounded-t-2xl [&>div]:rounded-b-none [&>div]:px-5 [&>div]:pt-4 [&>div]:pb-5"
|
||||
dismissible={false}
|
||||
itemLimit={4}
|
||||
showDescription
|
||||
title={t('firstEmpty.learnDifyTitle', { ns: 'app' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -263,6 +263,7 @@ function List({
|
||||
onCreateBlank={openCreateBlankModal}
|
||||
onCreateTemplate={openCreateTemplateDialog}
|
||||
onImportDSL={openCreateFromDSLModal}
|
||||
showLearnDify={systemFeatures.enable_learn_app}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
||||
@ -286,6 +286,7 @@ const mockAppCreatePermission = (hasEditPermission: boolean) => {
|
||||
|
||||
type RenderOptions = {
|
||||
enableExploreBanner?: boolean
|
||||
enableLearnApp?: boolean
|
||||
isCloudEdition?: boolean
|
||||
}
|
||||
|
||||
@ -302,7 +303,10 @@ const renderAppList = (
|
||||
mockConfig.isCloudEdition = options.isCloudEdition ?? false
|
||||
mockAppCreatePermission(hasEditPermission)
|
||||
const { wrapper: SystemFeaturesWrapper, queryClient } = createSystemFeaturesWrapper({
|
||||
systemFeatures: { enable_explore_banner: options.enableExploreBanner ?? false },
|
||||
systemFeatures: {
|
||||
enable_explore_banner: options.enableExploreBanner ?? false,
|
||||
enable_learn_app: options.enableLearnApp ?? true,
|
||||
},
|
||||
})
|
||||
if (!mockIsLoading && !mockIsError && mockExploreData)
|
||||
queryClient.setQueryData(exploreAppListQueryKey, mockExploreData)
|
||||
@ -542,6 +546,18 @@ describe('AppList', () => {
|
||||
expect(screen.queryByText('3 min')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide learn dify templates when learn app is disabled', () => {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
}
|
||||
|
||||
renderAppList(false, undefined, undefined, { enableLearnApp: false })
|
||||
|
||||
expect(screen.queryByRole('heading', { name: 'explore.learnDify.title' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Learn Workflow Basics')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should collapse learn dify and persist hidden state when hide is clicked', async () => {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
@ -578,6 +594,18 @@ describe('AppList', () => {
|
||||
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide categories without apps even when the API returns them', () => {
|
||||
mockExploreData = {
|
||||
categories: ['Writing', 'c'],
|
||||
allList: [createApp()],
|
||||
}
|
||||
|
||||
renderAppList(false, undefined, { category: 'c' })
|
||||
|
||||
expect(screen.queryByRole('radio', { name: 'c' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep selected category when clearing search text', async () => {
|
||||
mockExploreData = {
|
||||
categories: ['Writing', 'Translate'],
|
||||
|
||||
@ -142,15 +142,31 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
defaultValue: allCategoriesEn,
|
||||
})
|
||||
|
||||
const visibleCategories = useMemo(() => {
|
||||
if (!homeQueries.appListData)
|
||||
return []
|
||||
|
||||
const categoriesWithApps = new Set<string>()
|
||||
homeQueries.appListData.allList.forEach((app) => {
|
||||
app.categories.forEach(category => categoriesWithApps.add(category))
|
||||
})
|
||||
|
||||
return homeQueries.appListData.categories.filter(category => categoriesWithApps.has(category))
|
||||
}, [homeQueries.appListData])
|
||||
|
||||
const activeCategory = visibleCategories.includes(currCategory)
|
||||
? currCategory
|
||||
: allCategoriesEn
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!homeQueries.appListData)
|
||||
return []
|
||||
return homeQueries.appListData.allList.filter(
|
||||
item =>
|
||||
currCategory === allCategoriesEn
|
||||
|| item.categories?.includes(currCategory),
|
||||
activeCategory === allCategoriesEn
|
||||
|| item.categories?.includes(activeCategory),
|
||||
)
|
||||
}, [homeQueries.appListData, currCategory, allCategoriesEn])
|
||||
}, [homeQueries.appListData, activeCategory, allCategoriesEn])
|
||||
|
||||
const searchFilteredList = useMemo(() => {
|
||||
if (!searchKeywords || !filteredList || filteredList.length === 0)
|
||||
@ -292,8 +308,8 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
|
||||
<ExploreAppListHeader
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
categories={homeQueries.appListData?.categories ?? []}
|
||||
currCategory={currCategory}
|
||||
categories={visibleCategories}
|
||||
currCategory={activeCategory}
|
||||
keywords={keywords}
|
||||
onCategoryChange={setCurrCategory}
|
||||
onKeywordsChange={handleKeywordsChange}
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
import type { App } from '@/models/explore'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useLearnDifyAppList } from '@/service/use-explore'
|
||||
import LearnDifyItem from './item'
|
||||
import { useLearnDifyHiddenValue, useSetLearnDifyHidden } from './storage'
|
||||
@ -142,6 +144,11 @@ const DismissibleLearnDify = (props: LearnDifyProps) => {
|
||||
}
|
||||
|
||||
const LearnDify = (props: LearnDifyProps) => {
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
|
||||
if (!systemFeatures.enable_learn_app)
|
||||
return null
|
||||
|
||||
if (props.dismissible === false)
|
||||
return <LearnDifyContent {...props} />
|
||||
|
||||
|
||||
@ -905,7 +905,7 @@ describe('MainNav', () => {
|
||||
it('shows Learn Dify switch in help menu and restores it from localStorage', async () => {
|
||||
localStorage.setItem(LEARN_DIFY_HIDDEN_STORAGE_KEY, 'true')
|
||||
|
||||
renderMainNav()
|
||||
renderMainNav({ enable_learn_app: true })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
const learnDifyItem = await screen.findByRole('menuitemcheckbox', { name: 'common.mainNav.help.learnDify' })
|
||||
@ -919,8 +919,17 @@ describe('MainNav', () => {
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides Learn Dify switch in help menu when learn app is disabled', async () => {
|
||||
renderMainNav({ enable_learn_app: false })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
|
||||
await screen.findByText('common.mainNav.help.docs')
|
||||
expect(screen.queryByRole('menuitemcheckbox', { name: 'common.mainNav.help.learnDify' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('orders help menu items to match the nav shell design', async () => {
|
||||
renderMainNav()
|
||||
renderMainNav({ enable_learn_app: true })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ const HelpMenu = ({
|
||||
const setLearnDifyHidden = useSetLearnDifyHidden()
|
||||
const [aboutVisible, setAboutVisible] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const shouldShowLearnDifySwitch = systemFeatures.enable_learn_app
|
||||
|
||||
if (systemFeatures.branding.enabled)
|
||||
return null
|
||||
@ -95,31 +96,33 @@ const HelpMenu = ({
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={!learnDifyHidden}
|
||||
closeOnClick={false}
|
||||
className="mx-0 h-8 gap-1 px-0 py-1 pr-2 pl-3"
|
||||
onCheckedChange={checked => setLearnDifyHidden(!checked)}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-workflow-docs-extractor size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="min-w-0 flex-1 truncate px-1 py-0.5 system-md-regular text-text-secondary">
|
||||
{t('mainNav.help.learnDify', { ns: 'common' })}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'relative inline-flex h-4 w-7 shrink-0 items-center rounded-[5px] p-0.5 transition-colors',
|
||||
!learnDifyHidden ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
|
||||
)}
|
||||
{shouldShowLearnDifySwitch && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={!learnDifyHidden}
|
||||
closeOnClick={false}
|
||||
className="mx-0 h-8 gap-1 px-0 py-1 pr-2 pl-3"
|
||||
onCheckedChange={checked => setLearnDifyHidden(!checked)}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-workflow-docs-extractor size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="min-w-0 flex-1 truncate px-1 py-0.5 system-md-regular text-text-secondary">
|
||||
{t('mainNav.help.learnDify', { ns: 'common' })}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'block h-3 w-2.5 rounded-[3px] bg-components-toggle-knob shadow-sm transition-transform',
|
||||
!learnDifyHidden && 'translate-x-3.5',
|
||||
'relative inline-flex h-4 w-7 shrink-0 items-center rounded-[5px] p-0.5 transition-colors',
|
||||
!learnDifyHidden ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'block h-3 w-2.5 rounded-[3px] bg-components-toggle-knob shadow-sm transition-transform',
|
||||
!learnDifyHidden && 'translate-x-3.5',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
|
||||
@ -42,6 +42,7 @@ export NEXT_PUBLIC_ENABLE_CHANGE_EMAIL=${NEXT_PUBLIC_ENABLE_CHANGE_EMAIL:-${ENAB
|
||||
export NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED=${NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED:-${CREATORS_PLATFORM_FEATURES_ENABLED}}
|
||||
export NEXT_PUBLIC_ENABLE_TRIAL_APP=${NEXT_PUBLIC_ENABLE_TRIAL_APP:-${ENABLE_TRIAL_APP}}
|
||||
export NEXT_PUBLIC_ENABLE_EXPLORE_BANNER=${NEXT_PUBLIC_ENABLE_EXPLORE_BANNER:-${ENABLE_EXPLORE_BANNER}}
|
||||
export NEXT_PUBLIC_ENABLE_LEARN_APP=${NEXT_PUBLIC_ENABLE_LEARN_APP:-${ENABLE_LEARN_APP}}
|
||||
export NEXT_PUBLIC_RBAC_ENABLED=${NEXT_PUBLIC_RBAC_ENABLED:-${RBAC_ENABLED}}
|
||||
|
||||
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
||||
|
||||
@ -87,6 +87,7 @@ const clientSchema = {
|
||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_LEARN_APP: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_RBAC_ENABLED: coercedBoolean.default(false),
|
||||
|
||||
/**
|
||||
@ -217,6 +218,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED: isServer ? process.env.NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED : getRuntimeEnvFromBody('creatorsPlatformFeaturesEnabled'),
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: isServer ? process.env.NEXT_PUBLIC_ENABLE_TRIAL_APP : getRuntimeEnvFromBody('enableTrialApp'),
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: isServer ? process.env.NEXT_PUBLIC_ENABLE_EXPLORE_BANNER : getRuntimeEnvFromBody('enableExploreBanner'),
|
||||
NEXT_PUBLIC_ENABLE_LEARN_APP: isServer ? process.env.NEXT_PUBLIC_ENABLE_LEARN_APP : getRuntimeEnvFromBody('enableLearnApp'),
|
||||
NEXT_PUBLIC_RBAC_ENABLED: isServer ? process.env.NEXT_PUBLIC_RBAC_ENABLED : getRuntimeEnvFromBody('rbacEnabled'),
|
||||
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: isServer ? process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX : getRuntimeEnvFromBody('enableSingleDollarLatex'),
|
||||
|
||||
@ -18,6 +18,7 @@ const defaultCloudEnv = {
|
||||
NEXT_PUBLIC_ENABLE_EMAIL_CODE_LOGIN: true,
|
||||
NEXT_PUBLIC_ENABLE_EMAIL_PASSWORD_LOGIN: false,
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: true,
|
||||
NEXT_PUBLIC_ENABLE_LEARN_APP: true,
|
||||
NEXT_PUBLIC_ENABLE_MARKETPLACE: true,
|
||||
NEXT_PUBLIC_ENABLE_SOCIAL_OAUTH_LOGIN: true,
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: true,
|
||||
@ -140,6 +141,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
enable_email_password_login: false,
|
||||
enable_social_oauth_login: true,
|
||||
enable_trial_app: true,
|
||||
enable_learn_app: true,
|
||||
rbac_enabled: false,
|
||||
})
|
||||
})
|
||||
@ -153,6 +155,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
NEXT_PUBLIC_ENABLE_COLLABORATION_MODE: true,
|
||||
NEXT_PUBLIC_ALLOW_REGISTER: false,
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: false,
|
||||
NEXT_PUBLIC_ENABLE_LEARN_APP: true,
|
||||
NEXT_PUBLIC_RBAC_ENABLED: true,
|
||||
},
|
||||
})
|
||||
@ -166,6 +169,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
enable_collaboration_mode: true,
|
||||
is_allow_register: false,
|
||||
enable_explore_banner: false,
|
||||
enable_learn_app: true,
|
||||
rbac_enabled: true,
|
||||
branding: {
|
||||
enabled: false,
|
||||
@ -219,6 +223,7 @@ describe('serverSystemFeaturesQueryOptions', () => {
|
||||
cloudEnv: {
|
||||
NEXT_PUBLIC_ENABLE_MARKETPLACE: false,
|
||||
NEXT_PUBLIC_ENABLE_EMAIL_PASSWORD_LOGIN: true,
|
||||
NEXT_PUBLIC_ENABLE_LEARN_APP: true,
|
||||
},
|
||||
})
|
||||
|
||||
@ -231,6 +236,7 @@ describe('serverSystemFeaturesQueryOptions', () => {
|
||||
expect(data).toMatchObject({
|
||||
enable_marketplace: false,
|
||||
enable_email_password_login: true,
|
||||
enable_learn_app: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -52,6 +52,7 @@ export const defaultSystemFeatures = {
|
||||
enable_creators_platform: false,
|
||||
enable_trial_app: false,
|
||||
enable_explore_banner: false,
|
||||
enable_learn_app: true,
|
||||
} satisfies GetSystemFeaturesResponse
|
||||
|
||||
export const cloudSystemFeatures = {
|
||||
@ -101,5 +102,6 @@ export const cloudSystemFeatures = {
|
||||
enable_creators_platform: env.NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED,
|
||||
enable_trial_app: env.NEXT_PUBLIC_ENABLE_TRIAL_APP,
|
||||
enable_explore_banner: env.NEXT_PUBLIC_ENABLE_EXPLORE_BANNER,
|
||||
enable_learn_app: env.NEXT_PUBLIC_ENABLE_LEARN_APP,
|
||||
rbac_enabled: env.NEXT_PUBLIC_RBAC_ENABLED,
|
||||
} satisfies GetSystemFeaturesResponse
|
||||
|
||||
Loading…
Reference in New Issue
Block a user