mirror of
https://github.com/langgenius/dify.git
synced 2026-06-11 02:31:13 +08:00
fix: validate conversation variable description length to prevent varchar(255) truncation error (#33038)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
parent
09bfbf386e
commit
4fb3210f9a
@ -41,3 +41,13 @@ class NoFileUploadedError(BaseHTTPException):
|
||||
error_code = "no_file_uploaded"
|
||||
description = "Please upload your file."
|
||||
code = 400
|
||||
|
||||
|
||||
class NotFoundError(BaseHTTPException):
|
||||
error_code = "not_found"
|
||||
code = 404
|
||||
|
||||
|
||||
class InvalidArgumentError(BaseHTTPException):
|
||||
error_code = "invalid_param"
|
||||
code = 400
|
||||
|
||||
@ -12,6 +12,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotF
|
||||
|
||||
import services
|
||||
from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload
|
||||
from controllers.common.errors import InvalidArgumentError
|
||||
from controllers.common.fields import NewAppResponse, SimpleResultResponse
|
||||
from controllers.common.schema import (
|
||||
register_response_schema_model,
|
||||
@ -19,7 +20,11 @@ from controllers.common.schema import (
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||
from controllers.console.app.error import (
|
||||
ConversationCompletedError,
|
||||
DraftWorkflowNotExist,
|
||||
DraftWorkflowNotSync,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
@ -56,6 +61,7 @@ from graphon.file import helpers as file_helpers
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from graphon.variables import SecretVariable, SegmentType, VariableBase
|
||||
from graphon.variables.exc import VariableError
|
||||
from libs import helper
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.helper import TimestampField, dump_response, to_timestamp, uuid_value
|
||||
@ -452,6 +458,8 @@ class DraftWorkflowApi(Resource):
|
||||
)
|
||||
except WorkflowHashNotEqualError:
|
||||
raise DraftWorkflowNotSync()
|
||||
except VariableError as e:
|
||||
raise InvalidArgumentError(description=str(e))
|
||||
|
||||
return {
|
||||
"result": "success",
|
||||
|
||||
@ -9,6 +9,7 @@ from flask_restx import Resource, fields, marshal, marshal_with
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import (
|
||||
@ -21,7 +22,6 @@ from controllers.console.wraps import (
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
from extensions.ext_database import db
|
||||
|
||||
@ -9,6 +9,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
@ -18,7 +19,6 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from controllers.web.error import NotFoundError
|
||||
from core.workflow.human_input_forms import load_form_tokens_by_form_id as _load_form_tokens_by_form_id
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
|
||||
@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import (
|
||||
@ -23,7 +24,6 @@ from controllers.console.app.workflow_draft_variable import (
|
||||
)
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
from extensions.ext_database import db
|
||||
|
||||
@ -11,6 +11,7 @@ from flask_restx import Resource
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.common.human_input import HumanInputFormSubmitPayload
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
@ -21,7 +22,6 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
|
||||
from core.app.apps.base_app_generator import BaseAppGenerator
|
||||
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
|
||||
|
||||
@ -18,6 +18,7 @@ from flask import Response, request
|
||||
from flask_restx import Resource, marshal, marshal_with
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import DraftWorkflowNotExist
|
||||
from controllers.console.app.workflow_draft_variable import (
|
||||
@ -36,7 +37,6 @@ from controllers.console.wraps import (
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
from extensions.ext_database import db
|
||||
|
||||
@ -8,9 +8,9 @@ from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.common.fields import SimpleResultResponse
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.web.error import NotFoundError
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
from core.plugin.impl.oauth import OAuthHandler
|
||||
from core.trigger.entities.entities import SubscriptionBuilderUpdater
|
||||
|
||||
@ -121,13 +121,3 @@ class WebFormRateLimitExceededError(BaseHTTPException):
|
||||
error_code = "web_form_rate_limit_exceeded"
|
||||
description = "Too many form requests. Please try again later."
|
||||
code = 429
|
||||
|
||||
|
||||
class NotFoundError(BaseHTTPException):
|
||||
error_code = "not_found"
|
||||
code = 404
|
||||
|
||||
|
||||
class InvalidArgumentError(BaseHTTPException):
|
||||
error_code = "invalid_param"
|
||||
code = 400
|
||||
|
||||
@ -15,10 +15,11 @@ from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import NotFoundError, WebFormRateLimitExceededError
|
||||
from controllers.web.error import WebFormRateLimitExceededError
|
||||
from controllers.web.site import serialize_app_site_payload
|
||||
from extensions.ext_database import db
|
||||
from graphon.nodes.human_input.entities import FormInputConfig
|
||||
|
||||
@ -8,8 +8,8 @@ from collections.abc import Generator
|
||||
from flask import Response, request
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.web import api
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
|
||||
from core.app.apps.base_app_generator import BaseAppGenerator
|
||||
|
||||
@ -48,9 +48,18 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
_MAX_VARIABLE_DESCRIPTION_LENGTH = 255
|
||||
|
||||
|
||||
def build_conversation_variable_from_mapping(mapping: Mapping[str, Any], /) -> VariableBase:
|
||||
if not mapping.get("name"):
|
||||
raise VariableError("missing name")
|
||||
description = mapping.get("description", "")
|
||||
if len(description) > _MAX_VARIABLE_DESCRIPTION_LENGTH:
|
||||
raise VariableError(
|
||||
f"description of variable '{mapping['name']}' is too long"
|
||||
f" (max {_MAX_VARIABLE_DESCRIPTION_LENGTH} characters)"
|
||||
)
|
||||
return _build_variable_from_mapping(mapping=mapping, selector=[CONVERSATION_VARIABLE_NODE_ID, mapping["name"]])
|
||||
|
||||
|
||||
|
||||
@ -1861,6 +1861,8 @@ class WorkflowDraftVariable(Base):
|
||||
variable.file_id = file_id
|
||||
variable._set_selector(list(variable_utils.to_selector(node_id, name)))
|
||||
variable.node_execution_id = node_execution_id
|
||||
variable.visible = True
|
||||
variable.is_default_value = False
|
||||
return variable
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -749,6 +749,7 @@ class _InsertionDict(TypedDict):
|
||||
file_id: str | None
|
||||
visible: NotRequired[bool]
|
||||
editable: NotRequired[bool]
|
||||
is_default_value: NotRequired[bool]
|
||||
created_at: NotRequired[datetime]
|
||||
updated_at: NotRequired[datetime]
|
||||
description: NotRequired[str]
|
||||
@ -778,6 +779,8 @@ def _model_to_insertion_dict(model: WorkflowDraftVariable) -> _InsertionDict:
|
||||
d["updated_at"] = model.updated_at
|
||||
if model.description is not None:
|
||||
d["description"] = model.description
|
||||
if model.is_default_value is not None:
|
||||
d["is_default_value"] = model.is_default_value
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import pytest
|
||||
from flask import Flask
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.console.workspace.trigger_providers import (
|
||||
TriggerOAuthAuthorizeApi,
|
||||
TriggerOAuthCallbackApi,
|
||||
@ -26,7 +27,6 @@ from controllers.console.workspace.trigger_providers import (
|
||||
TriggerSubscriptionUpdateApi,
|
||||
TriggerSubscriptionVerifyApi,
|
||||
)
|
||||
from controllers.web.error import NotFoundError
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
from models.account import Account
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ from flask import Flask
|
||||
from pydantic import ValidationError
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError
|
||||
from controllers.console.app import workflow as workflow_module
|
||||
from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||
from graphon.file import File, FileTransferMethod, FileType
|
||||
@ -180,6 +181,29 @@ def test_sync_draft_workflow_hash_mismatch(app: Flask, monkeypatch: pytest.Monke
|
||||
handler(api, "t1", app_model=SimpleNamespace(id="app"))
|
||||
|
||||
|
||||
def test_sync_draft_workflow_variable_validation_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def _raise(*_args, **_kwargs):
|
||||
raise workflow_module.VariableError("description too long")
|
||||
|
||||
monkeypatch.setattr(workflow_module.variable_factory, "build_conversation_variable_from_mapping", _raise)
|
||||
monkeypatch.setattr(
|
||||
workflow_module, "WorkflowService", lambda: SimpleNamespace(sync_draft_workflow=lambda **_kwargs: None)
|
||||
)
|
||||
|
||||
api = workflow_module.DraftWorkflowApi()
|
||||
handler = inspect.unwrap(api.post)
|
||||
|
||||
with app.test_request_context(
|
||||
"/apps/app/workflows/draft",
|
||||
method="POST",
|
||||
json={"graph": {}, "features": {}, "hash": "h", "conversation_variables": [{"name": "topic"}]},
|
||||
):
|
||||
with pytest.raises(InvalidArgumentError) as exc:
|
||||
handler(api, "t1", app_model=SimpleNamespace(id="app"))
|
||||
|
||||
assert exc.value.description == "description too long"
|
||||
|
||||
|
||||
def test_restore_published_workflow_to_draft_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow = SimpleNamespace(
|
||||
unique_hash="restored-hash",
|
||||
|
||||
@ -8,8 +8,8 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.console.app import workflow_run as workflow_run_module
|
||||
from controllers.web.error import NotFoundError
|
||||
from graphon.entities.pause_reason import HumanInputRequired
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig
|
||||
|
||||
@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from flask import Flask, Response
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import DraftWorkflowNotExist
|
||||
from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import (
|
||||
@ -13,7 +14,6 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable impor
|
||||
RagPipelineVariableCollectionApi,
|
||||
RagPipelineVariableResetApi,
|
||||
)
|
||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
||||
from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID
|
||||
from graphon.variables.types import SegmentType
|
||||
from models.account import Account, TenantAccountRole
|
||||
|
||||
@ -9,6 +9,7 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
from flask import Flask, Response
|
||||
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.common.human_input import HumanInputFormSubmitPayload
|
||||
from controllers.console.human_input_form import (
|
||||
ConsoleHumanInputFormApi,
|
||||
@ -17,7 +18,6 @@ from controllers.console.human_input_form import (
|
||||
WorkflowResponseConverter,
|
||||
_jsonify_form_definition,
|
||||
)
|
||||
from controllers.web.error import NotFoundError
|
||||
from models.account import AccountStatus
|
||||
from models.enums import CreatorUserRole
|
||||
from models.human_input import RecipientType
|
||||
|
||||
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from controllers.common.errors import InvalidArgumentError, NotFoundError
|
||||
from controllers.web.error import (
|
||||
AppMoreLikeThisDisabledError,
|
||||
AppSuggestedQuestionsAfterAnswerDisabledError,
|
||||
@ -11,12 +12,10 @@ from controllers.web.error import (
|
||||
AudioTooLargeError,
|
||||
CompletionRequestError,
|
||||
ConversationCompletedError,
|
||||
InvalidArgumentError,
|
||||
InvokeRateLimitError,
|
||||
NoAudioUploadedError,
|
||||
NotChatAppError,
|
||||
NotCompletionAppError,
|
||||
NotFoundError,
|
||||
NotWorkflowAppError,
|
||||
ProviderModelCurrentlyNotSupportError,
|
||||
ProviderNotInitializeError,
|
||||
|
||||
@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.web.error import NotFoundError
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.web.workflow_events import WorkflowEventsApi
|
||||
from models.enums import CreatorUserRole
|
||||
|
||||
|
||||
@ -200,6 +200,19 @@ def test_variable_cannot_large_than_200_kb():
|
||||
)
|
||||
|
||||
|
||||
def test_conversation_variable_description_cannot_exceed_255_chars():
|
||||
with pytest.raises(VariableError, match="description of variable 'test_text' is too long"):
|
||||
variable_factory.build_conversation_variable_from_mapping(
|
||||
{
|
||||
"id": str(uuid4()),
|
||||
"value_type": "string",
|
||||
"name": "test_text",
|
||||
"value": "value",
|
||||
"description": "a" * 256,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_array_none_variable():
|
||||
var = variable_factory.build_segment([None, None, None, None])
|
||||
assert isinstance(var, ArrayAnySegment)
|
||||
|
||||
@ -31,6 +31,7 @@ from services.workflow_draft_variable_service import (
|
||||
DraftVariableSaver,
|
||||
VariableResetError,
|
||||
WorkflowDraftVariableService,
|
||||
_model_to_insertion_dict,
|
||||
)
|
||||
|
||||
|
||||
@ -643,3 +644,53 @@ class TestWorkflowDraftVariableService:
|
||||
assert node_var.visible == True
|
||||
assert node_var.editable == True
|
||||
assert node_var.node_execution_id == "exec-id"
|
||||
|
||||
|
||||
class TestModelToInsertionDict:
|
||||
"""Reproduce two production errors in _model_to_insertion_dict / _new()."""
|
||||
|
||||
def test_visible_and_is_default_value_always_present(self):
|
||||
"""Problem 1: _new() did not set visible/is_default_value, causing
|
||||
inconsistent dict keys across rows in multi-row INSERT and missing
|
||||
is_default_value in the insertion dict entirely.
|
||||
"""
|
||||
conv_var = WorkflowDraftVariable.new_conversation_variable(
|
||||
app_id="app-1",
|
||||
name="counter",
|
||||
value=StringSegment(value="0"),
|
||||
)
|
||||
# _new() should explicitly set these fields so they are not None
|
||||
assert conv_var.visible is not None
|
||||
assert conv_var.is_default_value is not None
|
||||
|
||||
d = _model_to_insertion_dict(conv_var)
|
||||
# visible must appear in every row's dict
|
||||
assert "visible" in d
|
||||
# is_default_value must always be present
|
||||
assert "is_default_value" in d
|
||||
|
||||
def test_description_passthrough(self):
|
||||
"""_model_to_insertion_dict passes description as-is;
|
||||
length validation is enforced earlier in build_conversation_variable_from_mapping.
|
||||
"""
|
||||
desc = "a" * 200
|
||||
conv_var = WorkflowDraftVariable.new_conversation_variable(
|
||||
app_id="app-1",
|
||||
name="counter",
|
||||
value=StringSegment(value="0"),
|
||||
description=desc,
|
||||
)
|
||||
d = _model_to_insertion_dict(conv_var)
|
||||
assert d["description"] == desc
|
||||
|
||||
def test_is_default_value_omitted_when_none(self):
|
||||
conv_var = WorkflowDraftVariable.new_conversation_variable(
|
||||
app_id="app-1",
|
||||
name="counter",
|
||||
value=StringSegment(value="0"),
|
||||
)
|
||||
conv_var.is_default_value = None
|
||||
|
||||
d = _model_to_insertion_dict(conv_var)
|
||||
|
||||
assert "is_default_value" not in d
|
||||
|
||||
@ -195,6 +195,34 @@ describe('useVariableModalState', () => {
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should notify and stop saving when description exceeds the limit', () => {
|
||||
const notify = vi.fn()
|
||||
const onSave = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
const { result } = renderHook(() => useVariableModalState(createOptions({
|
||||
notify,
|
||||
onClose,
|
||||
onSave,
|
||||
})))
|
||||
|
||||
act(() => {
|
||||
result.current.handleVarNameChange({ target: { value: 'greeting' } } as ChangeEvent<HTMLInputElement>)
|
||||
result.current.handleStringOrNumberChange(['hello'])
|
||||
result.current.setDescription('a'.repeat(256))
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSave()
|
||||
})
|
||||
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'chatVariable.modal.descriptionTooLong',
|
||||
})
|
||||
expect(onSave).not.toHaveBeenCalled()
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save a new variable and close when state is valid', () => {
|
||||
const onSave = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
@ -24,6 +24,7 @@ describe('variable-modal sections', () => {
|
||||
/>
|
||||
<DescriptionSection
|
||||
description="original description"
|
||||
maxLength={255}
|
||||
onChange={onDescriptionChange}
|
||||
placeholder="description-placeholder"
|
||||
title="Description"
|
||||
@ -43,6 +44,20 @@ describe('variable-modal sections', () => {
|
||||
expect(onDescriptionChange).toHaveBeenCalledWith('updated-description')
|
||||
})
|
||||
|
||||
it('should show description length against the configured limit', () => {
|
||||
render(
|
||||
<DescriptionSection
|
||||
description="abc"
|
||||
maxLength={255}
|
||||
onChange={vi.fn()}
|
||||
placeholder="description-placeholder"
|
||||
title="Description"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('3/255')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders type and value sections and forwards toggle and value changes', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const onArrayChange = vi.fn()
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
getEditorMinHeight,
|
||||
getPlaceholderByType,
|
||||
getTypeChangeState,
|
||||
MAX_DESCRIPTION_LENGTH,
|
||||
parseEditorContent,
|
||||
validateVariableName,
|
||||
} from './variable-modal.helpers'
|
||||
@ -196,6 +197,17 @@ export const useVariableModalState = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (state.description.length > MAX_DESCRIPTION_LENGTH) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('chatVariable.modal.descriptionTooLong', {
|
||||
maxLength: MAX_DESCRIPTION_LENGTH,
|
||||
ns: 'workflow',
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
onSave({
|
||||
description: state.description,
|
||||
id: chatVar ? chatVar.id : uuid4(),
|
||||
|
||||
@ -29,6 +29,8 @@ export type ToastPayload = {
|
||||
customComponent?: ReactNode
|
||||
}
|
||||
|
||||
export const MAX_DESCRIPTION_LENGTH = 255
|
||||
|
||||
export const typeList = [
|
||||
ChatVarTypeEnum.String,
|
||||
ChatVarTypeEnum.Number,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ObjectValueItem } from './variable-modal.helpers'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiDraftLine, RiInputField } from '@remixicon/react'
|
||||
import Input from '@/app/components/base/input'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
@ -195,6 +196,7 @@ export const ValueSection = ({
|
||||
|
||||
type DescriptionSectionProps = {
|
||||
description: string
|
||||
maxLength: number
|
||||
onChange: (value: string) => void
|
||||
placeholder: string
|
||||
title: string
|
||||
@ -202,6 +204,7 @@ type DescriptionSectionProps = {
|
||||
|
||||
export const DescriptionSection = ({
|
||||
description,
|
||||
maxLength,
|
||||
onChange,
|
||||
placeholder,
|
||||
title,
|
||||
@ -216,5 +219,10 @@ export const DescriptionSection = ({
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('mt-1 text-right system-xs-regular', description.length > maxLength ? 'text-text-destructive' : 'text-text-quaternary')}>
|
||||
{description.length}
|
||||
/
|
||||
{maxLength}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -12,6 +12,7 @@ import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
|
||||
import { useVariableModalState } from './use-variable-modal-state'
|
||||
import {
|
||||
getEditorToggleLabelKey,
|
||||
MAX_DESCRIPTION_LENGTH,
|
||||
typeList,
|
||||
validateVariableName,
|
||||
} from './variable-modal.helpers'
|
||||
@ -72,6 +73,7 @@ const ChatVariableModal = ({
|
||||
return
|
||||
handleVarNameChange(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex h-full w-[360px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl', type === ChatVarType.Object && 'w-[480px]')}
|
||||
@ -127,6 +129,7 @@ const ChatVariableModal = ({
|
||||
/>
|
||||
<DescriptionSection
|
||||
description={description}
|
||||
maxLength={MAX_DESCRIPTION_LENGTH}
|
||||
onChange={setDescription}
|
||||
placeholder={t('chatVariable.modal.descriptionPlaceholder', { ns: 'workflow' }) || ''}
|
||||
title={t('chatVariable.modal.description', { ns: 'workflow' })}
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"chatVariable.modal.arrayValue": "Value",
|
||||
"chatVariable.modal.description": "Description",
|
||||
"chatVariable.modal.descriptionPlaceholder": "Describe the variable",
|
||||
"chatVariable.modal.descriptionTooLong": "Description must be {{maxLength}} characters or less",
|
||||
"chatVariable.modal.editInForm": "Edit in Form",
|
||||
"chatVariable.modal.editInJSON": "Edit in JSON",
|
||||
"chatVariable.modal.editTitle": "Edit Conversation Variable",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"chatVariable.modal.arrayValue": "值",
|
||||
"chatVariable.modal.description": "描述",
|
||||
"chatVariable.modal.descriptionPlaceholder": "变量的描述",
|
||||
"chatVariable.modal.descriptionTooLong": "描述不能超过 {{maxLength}} 个字符",
|
||||
"chatVariable.modal.editInForm": "在表单中编辑",
|
||||
"chatVariable.modal.editInJSON": "在 JSON 中编辑",
|
||||
"chatVariable.modal.editTitle": "编辑会话变量",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user