mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-26 23:18:40 +08:00
feat: add Mustache template support for prompts (#9876)
* Add MustachePromptModal component for handling Mustache template prompts
- Implement MustachePromptModal component with support for editing and validating Mustache templates.
- Integrate with alert store for success, error, and notice alerts.
- Utilize Mustache library for parsing and highlighting template variables.
- Include UI components like Textarea, Badge, and Button for user interaction.
- Add functionality to handle variable checking and prompt validation via API.
* Add support for Mustache templates in prompt components and modals
- Introduced `MustachePromptModal` for handling Mustache-specific prompts.
- Updated `PromptAreaComponent` to conditionally render `MustachePromptModal` based on the `mustache` prop.
- Enhanced `parameterRenderComponent` to recognize and handle "mustache" type templates.
- Modified `varHighlightHTML` utility to optionally add curly braces around variable names.
- Extended `usePostValidatePrompt` API call to include a `mustache` flag.
- Updated component types to include `mustache` and `addCurlyBraces` options.
* Add mustache and @types/mustache dependencies to frontend package files
* Add support for mustache template format in message schema and input mixin
- Updated `format_text` and `from_template_and_variables` methods to accept a `template_format` parameter, allowing for different template formats.
- Modified `ChatPromptTemplate.from_messages` to include `template_format` parameter.
- Added `MUSTACHE_PROMPT` to the `InputTypes` enum in `input_mixin.py`.
* Add 'mustache' boolean field to API request model in base.py
* Add MustachePromptComponent and enhance prompt validation for mustache templates
- Introduce `MustachePromptComponent` class to create prompt templates with dynamic variables using mustache syntax.
- Add `build_prompt`, `_update_template`, `post_code_processing`, and `_get_fallback_input` methods to handle prompt creation and processing.
- Update `validate_prompt` function in `api_utils.py` to support mustache template variable extraction.
- Import `mustache_template_vars` for handling mustache-specific template variables.
* chore: update package dependencies in package.json and package-lock.json
- Bump @types/mustache from 4.2.5 to 4.2.6
- Add @xyflow/react at version 12.3.6
- Update ace-builds from 1.35.0 to 1.41.0
- Add moment-timezone at version 0.5.48
- Update mustache version to 4.2.0
- Add rehype-raw at version 6.1.1
- Remove unused @napi-rs/nice dependencies from package-lock.json
* feat: add 'mustache' to DIRECT_TYPES in constants.py
* fix: update field_type for template in MustachePromptComponent to MUSTACHE_PROMPT
* feat: add MustachePromptAreaComponent for rendering mustache prompts with enhanced styling and functionality
* refactor: update import paths for common components in mustachePromptModal
* feat: integrate MustachePromptAreaComponent into ParameterRenderComponent for mustache prompt rendering
* test: add comprehensive tests for MustachePromptComponent and template processing
- Introduced integration tests for MustachePromptComponent covering various scenarios including basic functionality, multiple variables, missing variables, and complex logic.
- Added unit tests for Message class to validate mustache template processing, including handling of special characters, lists, and conditional logic.
- Ensured robust coverage for edge cases such as empty templates and missing variables.
* test: add unit tests for ParameterRenderComponent and MustachePromptAreaComponent
- Introduced TypeScript tests for ParameterRenderComponent to validate props interface.
- Added comprehensive tests for MustachePromptAreaComponent focusing on mustache variable highlighting and props validation.
- Ensured robust coverage for various input scenarios, including handling of special characters and complex mustache variables.
* test: refine MustachePromptAreaComponent highlighting logic and add validation tests
- Updated the highlighting logic to only support simple mustache variables ({{variable_name}}) and exclude complex syntax.
- Enhanced unit tests to validate that complex mustache variables, variables with spaces, and invalid characters are not highlighted.
- Added tests for various edge cases, including variables starting with numbers and those containing special operators.
- Ensured comprehensive coverage for the MustachePromptAreaComponent's behavior in handling mustache templates.
* feat: implement secure mustache rendering in Message class
- Added a new utility module for mustache template security, including validation and safe rendering functions.
- Updated the Message class to support mustache template formatting using the new secure renderer.
- Ensured that only simple variable substitutions are allowed in mustache templates to enhance security.
* test: enhance MustachePromptComponent tests for variable handling and security
- Updated tests for MustachePromptComponent to ensure only simple variables are processed and complex syntax is rejected.
- Added new tests for validating mustache templates, including checks for invalid variable names and complex syntax.
- Introduced integration tests for mustache security utilities, ensuring robust validation and safe rendering of templates.
- Enhanced coverage for various edge cases, including nested objects and handling of None values.
* feat: add mustache prompt dialog subtitle and update modal reference
- Introduced a new constant for the mustache prompt dialog subtitle to guide users on using double curly brackets for variable introduction.
- Updated the MustachePromptModal to utilize the new subtitle constant, enhancing clarity and user experience.
* refactor: update display name in MustachePromptComponent for clarity
- Changed the display name from "Mustache Prompt" to "Prompt" to simplify the component's identification and improve user experience.
* refactor: move MustachePromptComponent to lfx
- Introduced MustachePromptComponent to create prompt templates with dynamic variables.
- Updated imports and exports in the processing module to include the new component.
- Implemented methods for building prompts and updating templates, enhancing flexibility in prompt generation.
* refactor: update input variable extraction logic in validate_prompt
- Adjusted the order of input variable extraction in the validate_prompt function to ensure correct handling of Mustache templates.
- Enhanced the process_prompt_template function to accept an is_mustache parameter, improving flexibility in template validation.
* refactor: enhance MustachePromptComponent with template validation and async updates
- Updated imports to reflect the new module structure under lfx.
- Added a validation step for Mustache templates to improve security.
- Refactored the post_code_processing method to an async update_frontend_node method for better performance and compatibility with async workflows.
* refactor: enhance Message class template handling with format support
- Updated the from_template_and_variables and from_template methods to accept a template_format parameter, improving flexibility in template processing.
- Ensured backward compatibility with previous versions while enhancing the functionality for template formatting options.
* feat: add 'mustache' to DIRECT_TYPES in constants
- Introduced 'mustache' as a new type in the DIRECT_TYPES list to support enhanced template processing capabilities.
- This addition aligns with recent updates to template handling and validation in the codebase.
* test: add unit tests for MustachePromptComponent
- Introduced comprehensive unit tests for the MustachePromptComponent, covering template variable extraction, handling of multiple variables, dot notation, and rejection of complex syntax.
- Added async tests for the build_prompt method to ensure correct message generation with various templates and variable scenarios.
- Enhanced test coverage to validate the update_frontend_node method's processing of templates.
* refactor: remove MustachePromptComponent and related tests
- Deleted the MustachePromptComponent from the processing module, streamlining the codebase by removing unused components.
- Removed associated unit tests to maintain consistency and reduce clutter in the test suite.
- Updated imports and exports in the processing module to reflect these changes.
* refactor: enhance PromptComponent with mode selection and template validation
- Updated the PromptComponent to include a mode input for selecting variable syntax, allowing for both Mustache and f-string formats.
- Implemented logic in the update_build_config method to adjust the template field type based on the selected mode.
- Added validation for Mustache templates to ensure security during template processing.
- Refactored the build_prompt and update_frontend_node methods to accommodate the new mode functionality and maintain compatibility with async workflows.
* test: add unit tests for PromptComponent template modes
- Introduced comprehensive unit tests for the PromptComponent, covering both f-string and Mustache modes.
- Validated mode switching, template updates, and prompt building for various scenarios, including handling of multiple variables and rejection of complex syntax.
- Ensured robust testing of the update_build_config and update_frontend_node methods to maintain functionality across different template formats.
* feat: add MustachePromptInput class to support Mustache template processing
- Introduced MustachePromptInput as a subclass of PromptInput, enabling the use of Mustache syntax for prompt generation.
- Updated InputTypes to include MustachePromptInput, enhancing the flexibility of input handling in the codebase.
* refactor: update prompt components to support mode selection and enhanced metadata
- Changed the icon for various prompt components from braces to prompts for better visual representation.
- Updated metadata to include a code hash and dependencies for improved tracking and management.
- Introduced a mode input for selecting variable syntax (f-string or Mustache), enhancing flexibility in template processing.
- Refactored prompt component code to accommodate the new mode functionality, ensuring compatibility with async workflows and maintaining robust template validation.
* feat: add "mustache" to LANGFLOW_SUPPORTED_TYPES in constants
* feat: include mustache flag in post_validate_prompt function
* fix: prevent retries on validation errors in usePostValidatePrompt mutation
* fix: simplify mustache variable validation to only allow simple variable names
* fix: update mustache variable regex to only match simple variable names
* fix: enhance template processing and validation for mustache mode
- Improved handling of template variables when switching modes, ensuring old fields are cleaned up.
- Added error handling during template validation to allow component creation even if validation fails.
- Streamlined the process of re-validating and processing templates to maintain compatibility with existing functionality.
* fix: refine input variable extraction for mustache and f-string templates
- Enhanced the validation process to differentiate between mustache and f-string variables.
- Updated the logic to ensure only relevant variables are retained based on the template syntax.
- Improved error messaging for better clarity on invalid input variables.
* chore: update component index
* test: add unit tests for mustache prompt component functionality
- Implemented tests for mode switching between f-string and mustache syntax.
- Verified that old fields are cleaned up when switching modes.
- Added checks for validation errors and ensured component functionality remains intact.
- Included tests for mixed syntax handling and the absence of connection handles for mustache fields.
* test: remove mustache prompt component unit tests
- Deleted the unit tests for the mustache prompt component, which included functionality for mode switching, validation error handling, and mixed syntax scenarios.
- This cleanup is part of a broader refactor to streamline test coverage and focus on essential components.
* chore: update package-lock.json to add new dependencies and remove unused ones
- Added "@radix-ui/react-radio-group" and "use-stick-to-bottom" dependencies.
- Removed "@smakss/react-scroll-direction" dependency.
- Updated various other dependencies to their latest versions for improved stability and performance.
* test: update mustache security tests for variable naming conventions
- Modified tests to use underscore notation for variable names instead of dot notation, ensuring compliance with the updated validation rules.
- Removed tests for dot notation and nested object properties, which are no longer supported.
- Added new tests for rendering with multiple variables and underscore variable names, enhancing coverage for the updated functionality.
* update component index
* test: update mustache template processing tests
- Removed tests for dot notation and nested object properties, aligning with the updated validation rules.
- Modified existing tests to ensure dot notation is rejected, enhancing security checks for mustache templates.
- This update streamlines the test suite to focus on supported variable naming conventions.
* test: remove mustache prompt integration and unit tests
- Deleted integration and unit tests for the MustachePromptComponent, including various scenarios for template processing and variable handling.
- This cleanup is part of an effort to streamline the test suite and focus on essential components, aligning with recent changes in validation rules and functionality.
* test: refine mustache template variable regex in tests
- Updated the regex in the MustachePromptAreaComponent tests to only match simple mustache variables, removing support for complex syntax.
- This change aligns with the recent updates to validation rules and enhances the clarity of the test cases.
* test: update mustache prompt component tests for variable highlighting
- Modified tests to ensure that complex mustache syntax is not highlighted, aligning with the updated validation rules.
- Renamed a test to clarify that dot notation variables should not be highlighted, reflecting the current functionality.
- These changes enhance the accuracy and clarity of the test cases for mustache template processing.
* refactor: update addLegacyComponents function to improve selector checks
- Replaced the expect assertion with a waitForSelector call to ensure the sidebar legacy switch is checked, enhancing reliability in tests.
- Updated import statement for Page from "@playwright/test" for consistency with current practices.
* fix playwright imports
* fix: Add missing newline at end of package.json and correct syntax in test utility functions
* fix: Update placeholder text and prompt dialog subtitle in Mustache component
* feat: Enhance PromptComponent to support Mustache syntax and dynamic template updates
* test: Add tests for double brackets toggle in Prompt component
- Add Playwright E2E tests for double brackets variable extraction
- Add Python unit tests for use_double_brackets BoolInput
- Test single and multiple variable extraction with {{var}} syntax
- Verify toggle persistence and input field creation
* chore: Update starter projects with use_double_brackets field
Update all starter project templates to include the new
use_double_brackets field in Prompt components
* chore: Update package-lock.json to reflect dependency changes
* [autofix.ci] apply automated fixes
* Fix: enhance mustache security by adding triple braces pattern and improving safe rendering documentation
* Fix: add logging for template validation failures in PromptComponent
* Fix: update mustache interface to make 'mustache' optional and ensure default value is false; add mustache security utilities for template validation
* Fix: improve error handling in validate_prompt for mustache template parsing
* Fix: refactor variable extraction in MustachePromptModal for improved regex matching and state management
* chore: update package-lock.json to add peer dependencies and remove unused packages
- Added peer: true to several dependencies to indicate peer dependency requirements.
- Removed unused dependencies related to @napi-rs/nice and its variants.
* feat: add unit tests for MustachePromptAreaComponent and MustachePromptModal
* update package lock
* update package lock
* [autofix.ci] apply automated fixes
* update index
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
02cacd773e
commit
7c19477bc2
@ -1403,7 +1403,7 @@
|
||||
"filename": "src/frontend/src/constants/constants.ts",
|
||||
"hashed_secret": "19a2fbd0dd38b4097f419c962342ef5e109eab07",
|
||||
"is_verified": false,
|
||||
"line_number": 751,
|
||||
"line_number": 755,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@ -1411,7 +1411,7 @@
|
||||
"filename": "src/frontend/src/constants/constants.ts",
|
||||
"hashed_secret": "3806954324550e26ef5de85d007f1746825a073c",
|
||||
"is_verified": false,
|
||||
"line_number": 752,
|
||||
"line_number": 756,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@ -1419,7 +1419,7 @@
|
||||
"filename": "src/frontend/src/constants/constants.ts",
|
||||
"hashed_secret": "c04f8fbf55c9096907a982750b1c6b0e4c1dd658",
|
||||
"is_verified": false,
|
||||
"line_number": 926,
|
||||
"line_number": 930,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -1528,5 +1528,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-12-17T22:10:09Z"
|
||||
"generated_at": "2026-01-08T13:01:29Z"
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class ValidatePromptRequest(BaseModel):
|
||||
template: str
|
||||
custom_fields: dict | None = None
|
||||
frontend_node: FrontendNodeRequest | None = None
|
||||
mustache: bool = False
|
||||
|
||||
|
||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
||||
@ -40,6 +40,7 @@ async def post_validate_prompt(
|
||||
name=prompt_request.name,
|
||||
custom_fields=prompt_request.frontend_node.custom_fields,
|
||||
frontend_node_template=prompt_request.frontend_node.template,
|
||||
is_mustache=prompt_request.mustache,
|
||||
)
|
||||
|
||||
return PromptValidationResponse(
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
src/backend/base/langflow/utils/mustache_security.py
Normal file
3
src/backend/base/langflow/utils/mustache_security.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Backwards compatibility module for langflow.utils.mustache_security."""
|
||||
|
||||
from lfx.utils.mustache_security import * # noqa: F403
|
||||
119
src/backend/tests/unit/utils/test_mustache_security.py
Normal file
119
src/backend/tests/unit/utils/test_mustache_security.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Tests for mustache security utilities."""
|
||||
|
||||
import pytest
|
||||
from langflow.utils.mustache_security import safe_mustache_render, validate_mustache_template
|
||||
|
||||
|
||||
class TestMustacheSecurity:
|
||||
"""Test mustache security functions."""
|
||||
|
||||
def test_validate_accepts_simple_variables(self):
|
||||
"""Test that simple variables are accepted."""
|
||||
# Should not raise
|
||||
validate_mustache_template("Hello {{name}}!")
|
||||
validate_mustache_template("{{user_name}} - {{user_email}}")
|
||||
validate_mustache_template("Price: {{price_100}}")
|
||||
validate_mustache_template("")
|
||||
validate_mustache_template("No variables here")
|
||||
|
||||
def test_validate_rejects_complex_syntax(self):
|
||||
"""Test that complex mustache syntax is rejected."""
|
||||
# Conditionals
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{#if}}content{{/if}}")
|
||||
|
||||
# Inverted sections
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{^empty}}not empty{{/empty}}")
|
||||
|
||||
# Unescaped
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{&html}}")
|
||||
|
||||
# Partials
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{>header}}")
|
||||
|
||||
# Comments
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{!comment}}")
|
||||
|
||||
# Current context
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{.}}")
|
||||
|
||||
def test_validate_rejects_invalid_variable_names(self):
|
||||
"""Test that invalid variable names are rejected."""
|
||||
# Spaces in variable names
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{ name with spaces }}")
|
||||
|
||||
# Starting with numbers
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{123invalid}}")
|
||||
|
||||
# Special characters
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{price-$100}}")
|
||||
|
||||
# Empty variables
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{}}")
|
||||
|
||||
# Dot notation (not supported)
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{user.name}}")
|
||||
|
||||
def test_safe_render_simple_variables(self):
|
||||
"""Test safe rendering of simple variables."""
|
||||
template = "Hello {{name}}! You are {{age}} years old."
|
||||
variables = {"name": "Alice", "age": 25}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Hello Alice! You are 25 years old."
|
||||
|
||||
def test_safe_render_missing_variables(self):
|
||||
"""Test rendering with missing variables."""
|
||||
template = "Hello {{name}}! Your score is {{score}}."
|
||||
variables = {"name": "Charlie"}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Hello Charlie! Your score is ."
|
||||
|
||||
def test_safe_render_none_values(self):
|
||||
"""Test rendering with None values."""
|
||||
template = "Name: {{name}}, Age: {{age}}"
|
||||
variables = {"name": None, "age": None}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Name: , Age: "
|
||||
|
||||
def test_safe_render_numeric_values(self):
|
||||
"""Test rendering with numeric values."""
|
||||
template = "Price: ${{price}}, Quantity: {{qty}}"
|
||||
variables = {"price": 19.99, "qty": 3}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Price: $19.99, Quantity: 3"
|
||||
|
||||
def test_safe_render_rejects_complex_syntax(self):
|
||||
"""Test that safe_render rejects complex syntax."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
safe_mustache_render("{{#if}}test{{/if}}", {"if": True})
|
||||
|
||||
def test_safe_render_multiple_variables(self):
|
||||
"""Test rendering with multiple variables."""
|
||||
template = "{{greeting}} {{name}}, welcome to {{place}}!"
|
||||
variables = {"greeting": "Hello", "name": "Alice", "place": "Langflow"}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Hello Alice, welcome to Langflow!"
|
||||
|
||||
def test_safe_render_underscore_variables(self):
|
||||
"""Test rendering with underscore variable names."""
|
||||
template = "Private: {{_private_var}}, Public: {{public_var}}"
|
||||
variables = {"_private_var": "secret", "public_var": "visible"}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "Private: secret, Public: visible"
|
||||
|
||||
def test_safe_render_empty_string_values(self):
|
||||
"""Test rendering with empty string values."""
|
||||
template = "Start{{middle}}End"
|
||||
variables = {"middle": ""}
|
||||
result = safe_mustache_render(template, variables)
|
||||
assert result == "StartEnd"
|
||||
415
src/frontend/package-lock.json
generated
415
src/frontend/package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/react-query": "^5.49.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@xyflow/react": "^12.3.6",
|
||||
"ace-builds": "^1.41.0",
|
||||
"ag-grid-community": "^32.3.9",
|
||||
@ -58,6 +59,7 @@
|
||||
"lucide-react": "^0.503.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"openseadragon": "^4.1.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
@ -206,7 +208,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@ -856,9 +857,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@borewit/text-codec": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz",
|
||||
"integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz",
|
||||
"integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@ -1065,7 +1066,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz",
|
||||
"integrity": "sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/shared-utils": "2.0.5",
|
||||
"csstype": "^3.1.2",
|
||||
@ -1077,7 +1077,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.6.2.tgz",
|
||||
"integrity": "sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/color-mode": "2.2.0",
|
||||
"@chakra-ui/object-utils": "2.1.0",
|
||||
@ -1184,7 +1183,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
@ -1221,17 +1219,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
|
||||
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.5.tgz",
|
||||
"integrity": "sha512-A2MwwJB5vC0UeOKHTsB0ZX19DH3/0KMaPgz45r0wd+SRwjk8x3KvYVQ/lYHRdZXxE59H0eUea1ANPqIPxX7QTw==",
|
||||
"version": "6.39.8",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.8.tgz",
|
||||
"integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@ -1327,7 +1323,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -1351,7 +1346,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -5446,9 +5440,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.41",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
|
||||
"version": "0.34.46",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.46.tgz",
|
||||
"integrity": "sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -5492,16 +5486,16 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.1.10.tgz",
|
||||
"integrity": "sha512-PSJVtawnGNrEkeLJQn9TTdeqrtDij8onvmnFtfkDaFG5IaUdQaLX9ibJ4gfxYakq+BEtlCcYiWErNJcqDrDluQ==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.1.11.tgz",
|
||||
"integrity": "sha512-Jwm291Fhim2eVcZIVlkG1B2skb0ZI9oru6nqMbJxceQZlvZmcIa4oxvS1oaMTKw2DJnCv97gLm57P/YvRZ8eUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.1.10",
|
||||
"@storybook/csf-plugin": "10.1.11",
|
||||
"@storybook/icons": "^2.0.0",
|
||||
"@storybook/react-dom-shim": "10.1.10",
|
||||
"@storybook/react-dom-shim": "10.1.11",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@ -5511,13 +5505,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.1.10"
|
||||
"storybook": "^10.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.1.10.tgz",
|
||||
"integrity": "sha512-SVKFDb14mne16QMGkmOEk+T4NLvCuFJJ1ecebQ01cPiG5gM72LhzYkAro717Aizd6owyMqcWs0Rsfwl09qi5zA==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.1.11.tgz",
|
||||
"integrity": "sha512-PEC+Fn3fyBOlMlCcLX+AUunrQMcH7MEfiFtPkp7QnjfMGwBIyzCjgVxM2OyKyIslQnB1So1pY98uTI26fXz/yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -5529,7 +5523,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.1.10"
|
||||
"storybook": "^10.1.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@ -5538,13 +5532,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.1.10.tgz",
|
||||
"integrity": "sha512-6m6zOyDhHLynv3lvkH70s1YoIkIFPhbpGsBKvHchRLrZLe8hCPeafIFLfZRPoD4yIPwBS6rWbjMsSvBMFlR+ag==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.1.11.tgz",
|
||||
"integrity": "sha512-MMD09Ap7FyzDfWG961pkIMv/w684XXe1bBEi+wCEpHxvrgAd3j3A9w/Rqp9Am2uRDPCEdi1QgSzS3SGW3aGThQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "10.1.10",
|
||||
"@storybook/csf-plugin": "10.1.11",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
@ -5553,14 +5547,14 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.1.10",
|
||||
"storybook": "^10.1.11",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.1.10.tgz",
|
||||
"integrity": "sha512-2dri4TRU8uuj/skmx/ZBw+GnnXf8EZHiMDMeijVRdBQtYFWPeoYzNIrGRpNfbuGpnDP0dcxrqti/TsedoxwFkA==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.1.11.tgz",
|
||||
"integrity": "sha512-Ant0NhgqHKzQsseeVTSetZCuDHHs0W2HRkHt51Kg/sUl0T/sDtfVA+fWZT8nGzGZqYSFkxqYPWjauPmIhPtaRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -5573,7 +5567,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.1.10",
|
||||
"storybook": "^10.1.11",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@ -5611,14 +5605,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.1.10.tgz",
|
||||
"integrity": "sha512-9Rpr8/wX0p5/EaulrxpqrjKjhGaA/Ab9HgxzTqs2Shz0gvMAQHoiRnTEp7RCCkP49ruFYnIp0yGRSovu03LakQ==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.1.11.tgz",
|
||||
"integrity": "sha512-rmMGmEwBaM2YpB8oDk2moM0MNjNMqtwyoPPZxjyruY9WVhYca8EDPGKEdRzUlb4qZJsTgLi7VU4eqg6LD/mL3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.1.10",
|
||||
"@storybook/react-dom-shim": "10.1.11",
|
||||
"react-docgen": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
@ -5628,7 +5622,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.1.10",
|
||||
"storybook": "^10.1.11",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@ -5638,9 +5632,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.1.10.tgz",
|
||||
"integrity": "sha512-9pmUbEr1MeMHg9TG0c2jVUfHWr2AA86vqZGphY/nT6mbe/rGyWtBl5EnFLrz6WpI8mo3h+Kxs6p2oiuIYieRtw==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.1.11.tgz",
|
||||
"integrity": "sha512-o8WPhRlZbORUWG9lAgDgJP0pi905VHJUFJr1Kp8980gHqtlemtnzjPxKy5vFwj6glNhAlK8SS8OOYzWP7hloTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@ -5650,20 +5644,20 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.1.10"
|
||||
"storybook": "^10.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-vite": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.1.10.tgz",
|
||||
"integrity": "sha512-6kE4/88YuwO07P0DR6caKNDNvCB/VnpimPmj4Jv6qmqrBgnoOOiXHIKyHJD+EjNyrbbwv4ygG01RVEajpjQaDA==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.1.11.tgz",
|
||||
"integrity": "sha512-qh1BCD25nIoiDfqwha+qBkl7pcG4WuzM+c8tsE63YEm8AFIbNKg5K8lVUoclF+4CpFz7IwBpWe61YUTDfp+91w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.3",
|
||||
"@rollup/pluginutils": "^5.0.2",
|
||||
"@storybook/builder-vite": "10.1.10",
|
||||
"@storybook/react": "10.1.10",
|
||||
"@storybook/builder-vite": "10.1.11",
|
||||
"@storybook/react": "10.1.11",
|
||||
"empathic": "^2.0.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"react-docgen": "^8.0.0",
|
||||
@ -5677,7 +5671,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.1.10",
|
||||
"storybook": "^10.1.11",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
@ -6017,13 +6011,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.7.tgz",
|
||||
"integrity": "sha512-kTGB8XI7P+pTKW83tnUEDVP4zduF951u3UAOn5eTi0vyW6MvL56A3+ggMdfuVFtDI0/DsbSzf5z34HVBbuScWw==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz",
|
||||
"integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
@ -6036,16 +6029,16 @@
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.7",
|
||||
"@swc/core-darwin-x64": "1.15.7",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.7",
|
||||
"@swc/core-linux-arm64-musl": "1.15.7",
|
||||
"@swc/core-linux-x64-gnu": "1.15.7",
|
||||
"@swc/core-linux-x64-musl": "1.15.7",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.7",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.7",
|
||||
"@swc/core-win32-x64-msvc": "1.15.7"
|
||||
"@swc/core-darwin-arm64": "1.15.8",
|
||||
"@swc/core-darwin-x64": "1.15.8",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.8",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.8",
|
||||
"@swc/core-linux-arm64-musl": "1.15.8",
|
||||
"@swc/core-linux-x64-gnu": "1.15.8",
|
||||
"@swc/core-linux-x64-musl": "1.15.8",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.8",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.8",
|
||||
"@swc/core-win32-x64-msvc": "1.15.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
@ -6057,9 +6050,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.7.tgz",
|
||||
"integrity": "sha512-+hNVUfezUid7LeSHqnhoC6Gh3BROABxjlDNInuZ/fie1RUxaEX4qzDwdTgozJELgHhvYxyPIg1ro8ibnKtgO4g==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz",
|
||||
"integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -6074,9 +6067,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.7.tgz",
|
||||
"integrity": "sha512-ZAFuvtSYZTuXPcrhanaD5eyp27H8LlDzx2NAeVyH0FchYcuXf0h5/k3GL9ZU6Jw9eQ63R1E8KBgpXEJlgRwZUQ==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz",
|
||||
"integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -6091,9 +6084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.7.tgz",
|
||||
"integrity": "sha512-K3HTYocpqnOw8KcD8SBFxiDHjIma7G/X+bLdfWqf+qzETNBrzOub/IEkq9UaeupaJiZJkPptr/2EhEXXWryS/A==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz",
|
||||
"integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -6108,9 +6101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.7.tgz",
|
||||
"integrity": "sha512-HCnVIlsLnCtQ3uXcXgWrvQ6SAraskLA9QJo9ykTnqTH6TvUYqEta+TdTdGjzngD6TOE7XjlAiUs/RBtU8Z0t+Q==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz",
|
||||
"integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -6125,9 +6118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.7.tgz",
|
||||
"integrity": "sha512-/OOp9UZBg4v2q9+x/U21Jtld0Wb8ghzBScwhscI7YvoSh4E8RALaJ1msV8V8AKkBkZH7FUAFB7Vbv0oVzZsezA==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz",
|
||||
"integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -6142,9 +6135,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.7.tgz",
|
||||
"integrity": "sha512-VBbs4gtD4XQxrHuQ2/2+TDZpPQQgrOHYRnS6SyJW+dw0Nj/OomRqH+n5Z4e/TgKRRbieufipeIGvADYC/90PYQ==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz",
|
||||
"integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -6159,9 +6152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.7.tgz",
|
||||
"integrity": "sha512-kVuy2unodso6p0rMauS2zby8/bhzoGRYxBDyD6i2tls/fEYAE74oP0VPFzxIyHaIjK1SN6u5TgvV9MpyJ5xVug==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz",
|
||||
"integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -6176,9 +6169,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.7.tgz",
|
||||
"integrity": "sha512-uddYoo5Xmo1XKLhAnh4NBIyy5d0xk33x1sX3nIJboFySLNz878ksCFCZ3IBqrt1Za0gaoIWoOSSSk0eNhAc/sw==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz",
|
||||
"integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -6193,9 +6186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.7.tgz",
|
||||
"integrity": "sha512-rqq8JjNMLx3QNlh0aPTtN/4+BGLEHC94rj9mkH1stoNRf3ra6IksNHMHy+V1HUqElEgcZyx+0yeXx3eLOTcoFw==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz",
|
||||
"integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -6210,9 +6203,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.7.tgz",
|
||||
"integrity": "sha512-4BK06EGdPnuplgcNhmSbOIiLdRgHYX3v1nl4HXo5uo4GZMfllXaCyBUes+0ePRfwbn9OFgVhCWPcYYjMT6hycQ==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz",
|
||||
"integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -6234,9 +6227,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
@ -6266,9 +6259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.0.tgz",
|
||||
"integrity": "sha512-z9OfTEG6QbaQWM9KBOxxUdpgvMUn0atageXyiaSc2gmYm51ORO8Ua7eUcjlks+Dc0YMK4rrodAFdK9SfjJ4ZcA==",
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
|
||||
"integrity": "sha512-f4Jg3Fof/Vru5ioix/UO4GX+sdDsF9wQo47FbtvG+utIYYVQ/QVAC0QYgcBbAjQGfbdOh2CCf0BgiFOF9Ixtjw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6276,12 +6269,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons-react": {
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.0.tgz",
|
||||
"integrity": "sha512-sSZ00bEjTdTTskVFykq294RJq+9cFatwy4uYa78HcYBCXU1kSD1DIp5yoFsQXmybkIOKCjp18OnhAYk553UIfQ==",
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.1.tgz",
|
||||
"integrity": "sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tabler/icons": "3.36.0"
|
||||
"@tabler/icons": ""
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6326,9 +6319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
|
||||
"integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
|
||||
"version": "5.90.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz",
|
||||
"integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6336,12 +6329,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.90.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz",
|
||||
"integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==",
|
||||
"version": "5.90.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz",
|
||||
"integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.12"
|
||||
"@tanstack/query-core": "5.90.16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6352,12 +6345,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.13.tgz",
|
||||
"integrity": "sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==",
|
||||
"version": "3.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.14.tgz",
|
||||
"integrity": "sha512-WG0d7mBD54eA7dgA3+sO5csS0B49QKqM6Gy5Rf31+Oq/LTKROQSao9m2N/vz1IqVragOKU5t5k1LAcqh/DfTxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.13"
|
||||
"@tanstack/virtual-core": "3.13.14"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6369,9 +6362,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz",
|
||||
"integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==",
|
||||
"version": "3.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.14.tgz",
|
||||
"integrity": "sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -6384,7 +6377,6 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -7063,6 +7055,12 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mustache": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.6.tgz",
|
||||
"integrity": "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||
@ -7090,7 +7088,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -7101,7 +7098,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@ -7117,8 +7113,7 @@
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz",
|
||||
"integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
@ -7798,7 +7793,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -8427,7 +8421,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -8621,9 +8614,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001761",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
||||
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
||||
"version": "1.0.30001762",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -8764,7 +8757,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@ -8808,9 +8800,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz",
|
||||
"integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
|
||||
"integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -9244,7 +9236,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -9793,7 +9784,6 @@
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@ -10031,7 +10021,6 @@
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@ -10203,9 +10192,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@ -11681,7 +11670,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@ -13418,7 +13406,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@ -13468,7 +13455,6 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@ -13508,7 +13494,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
|
||||
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
@ -15118,6 +15103,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@ -15253,9 +15247,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz",
|
||||
"integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==",
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
|
||||
"integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -15826,7 +15820,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -15878,6 +15871,48 @@
|
||||
"postcss": "^8.4.21"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-load-config": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lilconfig": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jiti": ">=1.21.0",
|
||||
"postcss": ">=8.0.9",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nested": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
@ -16232,7 +16267,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -16305,7 +16339,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -16336,7 +16369,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz",
|
||||
"integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@ -16934,7 +16966,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -17459,8 +17490,7 @@
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
|
||||
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
@ -17575,12 +17605,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.1.10.tgz",
|
||||
"integrity": "sha512-oK0t0jEogiKKfv5Z1ao4Of99+xWw1TMUGuGRYDQS4kp2yyBsJQEgu7NI7OLYsCDI6gzt5p3RPtl1lqdeVLUi8A==",
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.1.11.tgz",
|
||||
"integrity": "sha512-pKP5jXJYM4OjvNklGuHKO53wOCAwfx79KvZyOWHoi9zXUH5WVMFUe/ZfWyxXG/GTcj0maRgHGUjq/0I43r0dDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.0",
|
||||
@ -17939,9 +17968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.46.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.0.tgz",
|
||||
"integrity": "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw==",
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.1.tgz",
|
||||
"integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
@ -18003,9 +18032,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
|
||||
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
@ -18023,7 +18052,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@ -18087,48 +18115,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-load-config": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lilconfig": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jiti": ">=1.21.0",
|
||||
"postcss": ">=8.0.9",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
@ -18375,13 +18361,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/token-types": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz",
|
||||
"integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
|
||||
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@borewit/text-codec": "^0.1.0",
|
||||
"@borewit/text-codec": "^0.2.1",
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
},
|
||||
@ -18646,7 +18632,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -19157,7 +19142,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -20172,15 +20156,14 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.0",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||
"version": "3.25.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
||||
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/react-query": "^5.49.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@xyflow/react": "^12.3.6",
|
||||
"ace-builds": "^1.41.0",
|
||||
"ag-grid-community": "^32.3.9",
|
||||
@ -56,6 +57,7 @@
|
||||
"lucide-react": "^0.503.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"openseadragon": "^4.1.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
|
||||
@ -0,0 +1,286 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import MustachePromptAreaComponent from "../index";
|
||||
|
||||
// Mock the MustachePromptModal component
|
||||
jest.mock("@/modals/mustachePromptModal", () => {
|
||||
return function MockMustachePromptModal({
|
||||
children,
|
||||
value,
|
||||
setValue,
|
||||
id,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
setValue: (val: string) => void;
|
||||
id: string;
|
||||
}) {
|
||||
return (
|
||||
<div data-testid="mock-mustache-modal" data-value={value} data-id={id}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
// Mock IconComponent
|
||||
jest.mock("@/components/common/genericIconComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ name, className }: { name: string; className?: string }) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock SanitizedHTMLWrapper
|
||||
jest.mock("@/components/common/sanitizedHTMLWrapper", () => {
|
||||
return function MockSanitizedHTMLWrapper({
|
||||
content,
|
||||
className,
|
||||
}: {
|
||||
content: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-testid="sanitized-html"
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
describe("MustachePromptAreaComponent", () => {
|
||||
const defaultProps = {
|
||||
field_name: "template",
|
||||
nodeClass: null,
|
||||
handleOnNewValue: jest.fn(),
|
||||
handleNodeClass: jest.fn(),
|
||||
value: "",
|
||||
disabled: false,
|
||||
editNode: false,
|
||||
id: "test-mustache-prompt",
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
it("should render with empty value and show placeholder", () => {
|
||||
render(<MustachePromptAreaComponent {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Type your prompt here using/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with value containing text", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Hello, this is a test prompt."
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("sanitized-html")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should highlight mustache variables in the content", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Hello {{name}}, welcome to {{place}}!"
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
expect(sanitizedHtml.innerHTML).toContain("chat-message-highlight");
|
||||
expect(sanitizedHtml.innerHTML).toContain("{{name}}");
|
||||
expect(sanitizedHtml.innerHTML).toContain("{{place}}");
|
||||
});
|
||||
|
||||
it("should escape HTML tags in content", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="<script>alert('xss')</script>"
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
expect(sanitizedHtml.innerHTML).toContain("<script>");
|
||||
expect(sanitizedHtml.innerHTML).not.toContain("<script>");
|
||||
});
|
||||
|
||||
it("should preserve newlines as <br /> tags", () => {
|
||||
// The component converts \n to <br /> internally before passing to SanitizedHTMLWrapper
|
||||
// We test the component's transformation by checking the expected pattern
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value={"Line 1\nLine 2\nLine 3"}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
// The value is transformed by the component before being passed to the wrapper
|
||||
// The actual component renders <br /> but our mock receives the raw transformed content
|
||||
expect(sanitizedHtml).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("editNode mode", () => {
|
||||
it("should apply edit node classes when editNode is true", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
editNode={true}
|
||||
value="test value"
|
||||
/>,
|
||||
);
|
||||
|
||||
const promptSpan = screen.getByTestId("test-mustache-prompt");
|
||||
expect(promptSpan).toHaveClass("input-edit-node");
|
||||
});
|
||||
|
||||
it("should apply normal classes when editNode is false", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
editNode={false}
|
||||
value="test value"
|
||||
/>,
|
||||
);
|
||||
|
||||
const promptSpan = screen.getByTestId("test-mustache-prompt");
|
||||
expect(promptSpan).toHaveClass("primary-input");
|
||||
});
|
||||
});
|
||||
|
||||
describe("disabled state", () => {
|
||||
it("should show lock icon when disabled and no value", () => {
|
||||
render(<MustachePromptAreaComponent {...defaultProps} disabled={true} />);
|
||||
|
||||
expect(screen.getByTestId("icon-lock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Braces icon when not disabled and no value", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent {...defaultProps} disabled={false} />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("icon-Braces")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show icon when there is a value", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Some value"
|
||||
disabled={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("icon-lock")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("icon-Braces")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should apply disabled class to prompt span when disabled and not editNode", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
disabled={true}
|
||||
editNode={false}
|
||||
value="test"
|
||||
/>,
|
||||
);
|
||||
|
||||
const promptSpan = screen.getByTestId("test-mustache-prompt");
|
||||
expect(promptSpan).toHaveClass("disabled-state");
|
||||
});
|
||||
|
||||
it("should apply pointer-events-none to wrapper when disabled", () => {
|
||||
const { container } = render(
|
||||
<MustachePromptAreaComponent {...defaultProps} disabled={true} />,
|
||||
);
|
||||
|
||||
const wrapper = container.firstChild;
|
||||
expect(wrapper).toHaveClass("pointer-events-none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("modal integration", () => {
|
||||
it("should pass correct props to MustachePromptModal", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="test value"
|
||||
id="custom-id"
|
||||
/>,
|
||||
);
|
||||
|
||||
const modal = screen.getByTestId("mock-mustache-modal");
|
||||
expect(modal).toHaveAttribute("data-value", "test value");
|
||||
expect(modal).toHaveAttribute("data-id", "custom-id");
|
||||
});
|
||||
|
||||
it("should render button inside modal trigger", () => {
|
||||
render(<MustachePromptAreaComponent {...defaultProps} />);
|
||||
|
||||
const button = screen.getByTestId("button_open_mustache_prompt_modal");
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("variable highlighting", () => {
|
||||
it("should only highlight valid mustache variables", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Valid: {{name}}, Invalid: {{123invalid}} and {{with space}}"
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
// Only {{name}} should be highlighted (valid variable)
|
||||
expect(sanitizedHtml.innerHTML).toContain(
|
||||
'<span class="chat-message-highlight">{{name}}</span>',
|
||||
);
|
||||
// Invalid patterns should NOT be highlighted
|
||||
expect(sanitizedHtml.innerHTML).not.toContain(
|
||||
'<span class="chat-message-highlight">{{123invalid}}</span>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should highlight variables starting with underscore", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Value: {{_private}}"
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
expect(sanitizedHtml.innerHTML).toContain(
|
||||
'<span class="chat-message-highlight">{{_private}}</span>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should highlight variables with numbers", () => {
|
||||
render(
|
||||
<MustachePromptAreaComponent
|
||||
{...defaultProps}
|
||||
value="Value: {{var123}}"
|
||||
/>,
|
||||
);
|
||||
|
||||
const sanitizedHtml = screen.getByTestId("sanitized-html");
|
||||
expect(sanitizedHtml.innerHTML).toContain(
|
||||
'<span class="chat-message-highlight">{{var123}}</span>',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,208 @@
|
||||
// Tests for MustachePromptAreaComponent functionality
|
||||
// Focus on testing the core logic without importing the full component
|
||||
|
||||
describe("MustachePromptAreaComponent", () => {
|
||||
describe("mustache variable highlighting logic", () => {
|
||||
// Test the core mustache highlighting logic that's used in the component
|
||||
const applyMustacheHighlighting = (value: string) => {
|
||||
return (
|
||||
(typeof value === "string" ? value : "")
|
||||
// escape HTML first
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
// highlight only simple mustache variables {{variable_name}} - no complex syntax
|
||||
.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, varName) => {
|
||||
return `<span class="chat-message-highlight">{{${varName}}}</span>`;
|
||||
})
|
||||
// preserve new-lines
|
||||
.replace(/\n/g, "<br />")
|
||||
);
|
||||
};
|
||||
|
||||
it("should highlight single mustache variable", () => {
|
||||
const input = "Hello {{name}}!";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
'Hello <span class="chat-message-highlight">{{name}}</span>!',
|
||||
);
|
||||
});
|
||||
|
||||
it("should highlight multiple mustache variables", () => {
|
||||
const input = "Hello {{name}}, you are {{age}} years old.";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
'Hello <span class="chat-message-highlight">{{name}}</span>, you are <span class="chat-message-highlight">{{age}}</span> years old.',
|
||||
);
|
||||
});
|
||||
|
||||
it("should escape HTML content", () => {
|
||||
const input = "Content: <script>alert('test')</script>";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
"Content: <script>alert('test')</script>",
|
||||
);
|
||||
});
|
||||
|
||||
it("should preserve newlines", () => {
|
||||
const input = "Line 1\nLine 2\nLine 3";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("Line 1<br />Line 2<br />Line 3");
|
||||
});
|
||||
|
||||
it("should NOT highlight complex mustache syntax", () => {
|
||||
const input = "{{#if user}}Hello {{name}}{{/if}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
'{{#if user}}Hello <span class="chat-message-highlight">{{name}}</span>{{/if}}',
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle mixed content", () => {
|
||||
const input =
|
||||
"Hello {{name}}!\n<script>alert('test')</script>\nAge: {{age}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
'Hello <span class="chat-message-highlight">{{name}}</span>!<br /><script>alert(\'test\')</script><br />Age: <span class="chat-message-highlight">{{age}}</span>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const input = "";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle string with no mustache variables", () => {
|
||||
const input = "This is a plain string.";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("This is a plain string.");
|
||||
});
|
||||
|
||||
it("should NOT highlight variables with spaces", () => {
|
||||
const input = "Hello {{ name with spaces }}!";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("Hello {{ name with spaces }}!");
|
||||
});
|
||||
|
||||
it("should NOT highlight variables with invalid characters", () => {
|
||||
const input = "Value: {{price-$100}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("Value: {{price-$100}}");
|
||||
});
|
||||
|
||||
it("should handle variables with underscores", () => {
|
||||
const input = "Value: {{price_100}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe(
|
||||
'Value: <span class="chat-message-highlight">{{price_100}}</span>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT highlight dot notation variables", () => {
|
||||
const input = "Hello {{user.name}} from {{company.address.city}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
// Dot notation is not supported, so should not be highlighted
|
||||
expect(result).toBe("Hello {{user.name}} from {{company.address.city}}");
|
||||
});
|
||||
|
||||
it("should reject variables starting with numbers", () => {
|
||||
const input = "Value: {{123invalid}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("Value: {{123invalid}}");
|
||||
});
|
||||
|
||||
it("should reject variables with hashtags (conditionals)", () => {
|
||||
const input = "{{#each items}}Item{{/each}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{#each items}}Item{{/each}}");
|
||||
});
|
||||
|
||||
it("should reject variables with forward slashes (closers)", () => {
|
||||
const input = "{{/if}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{/if}}");
|
||||
});
|
||||
|
||||
it("should reject variables with carets (inverted sections)", () => {
|
||||
const input = "{{^isEmpty}}Not empty{{/isEmpty}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{^isEmpty}}Not empty{{/isEmpty}}");
|
||||
});
|
||||
|
||||
it("should reject variables with ampersands (unescaped)", () => {
|
||||
const input = "{{&unescaped}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{&unescaped}}");
|
||||
});
|
||||
|
||||
it("should reject variables with dots at the start", () => {
|
||||
const input = "{{.}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{.}}");
|
||||
});
|
||||
|
||||
it("should reject empty variables", () => {
|
||||
const input = "{{}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{}}");
|
||||
});
|
||||
|
||||
it("should reject variables with special operators", () => {
|
||||
const input = "{{>partial}} {{!comment}}";
|
||||
const result = applyMustacheHighlighting(input);
|
||||
expect(result).toBe("{{>partial}} {{!comment}}");
|
||||
});
|
||||
});
|
||||
|
||||
describe("component props validation", () => {
|
||||
it("should accept correct props type", () => {
|
||||
const mockProps = {
|
||||
id: "test-id",
|
||||
value: "Hello {{name}}!",
|
||||
editNode: false,
|
||||
handleOnNewValue: jest.fn(),
|
||||
disabled: false,
|
||||
nodeClass: {
|
||||
description: "Test component",
|
||||
template: {},
|
||||
display_name: "Test Component",
|
||||
documentation: "Test component documentation",
|
||||
},
|
||||
handleNodeClass: jest.fn(),
|
||||
nodeId: "test-node-id",
|
||||
readonly: false,
|
||||
field_name: "template",
|
||||
};
|
||||
|
||||
// If this compiles without TypeScript errors, the types are correct
|
||||
expect(mockProps).toBeDefined();
|
||||
expect(mockProps.nodeClass.description).toBe("Test component");
|
||||
expect(mockProps.nodeClass.display_name).toBe("Test Component");
|
||||
expect(mockProps.nodeClass.documentation).toBe(
|
||||
"Test component documentation",
|
||||
);
|
||||
expect(mockProps.value).toBe("Hello {{name}}!");
|
||||
expect(mockProps.field_name).toBe("template");
|
||||
});
|
||||
|
||||
it("should handle different value types", () => {
|
||||
const propsWithEmptyValue = {
|
||||
value: "",
|
||||
field_name: "template",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
const propsWithComplexValue = {
|
||||
value: "Item: {{name}} - {{price}}",
|
||||
field_name: "template",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
expect(propsWithEmptyValue.value).toBe("");
|
||||
expect(propsWithComplexValue.value).toContain("{{name}}");
|
||||
expect(propsWithComplexValue.value).toContain("{{price}}");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,102 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import SanitizedHTMLWrapper from "@/components/common/sanitizedHTMLWrapper";
|
||||
import MustachePromptModal from "@/modals/mustachePromptModal";
|
||||
import { cn } from "../../../../../utils/utils";
|
||||
import { Button } from "../../../../ui/button";
|
||||
import { getPlaceholder } from "../../helpers/get-placeholder-disabled";
|
||||
import type { InputProps, PromptAreaComponentType } from "../../types";
|
||||
|
||||
const promptContentClasses = {
|
||||
base: "overflow-hidden text-clip whitespace-nowrap bg-background h-fit max-h-28",
|
||||
editNode: "input-edit-node input-dialog py-2",
|
||||
normal: "primary-input text-primary",
|
||||
disabled: "disabled-state",
|
||||
};
|
||||
|
||||
export default function MustachePromptAreaComponent({
|
||||
field_name,
|
||||
nodeClass,
|
||||
handleOnNewValue,
|
||||
handleNodeClass,
|
||||
value,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: InputProps<string, PromptAreaComponentType>): JSX.Element {
|
||||
const coloredContent = (typeof value === "string" ? value : "")
|
||||
// escape HTML first
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
// highlight only simple mustache variables {{variable_name}} - no complex syntax
|
||||
.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, varName) => {
|
||||
return `<span class="chat-message-highlight">{{${varName}}}</span>`;
|
||||
})
|
||||
// preserve new-lines
|
||||
.replace(/\n/g, "<br />");
|
||||
|
||||
const renderPromptText = () => (
|
||||
<span
|
||||
id={id}
|
||||
data-testid={id}
|
||||
className={cn(
|
||||
promptContentClasses.base,
|
||||
editNode ? promptContentClasses.editNode : promptContentClasses.normal,
|
||||
disabled && !editNode && promptContentClasses.disabled,
|
||||
)}
|
||||
>
|
||||
{value !== "" ? (
|
||||
<SanitizedHTMLWrapper
|
||||
className="m-0 whitespace-pre-wrap p-0 text-xs"
|
||||
content={coloredContent}
|
||||
suppressWarning={true}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{getPlaceholder(
|
||||
disabled,
|
||||
"Type your prompt here using {{variable}}...",
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderExternalLinkIcon = () =>
|
||||
!value || value == "" ? (
|
||||
<ForwardedIconComponent
|
||||
name={disabled ? "lock" : "Braces"}
|
||||
className={cn(
|
||||
"icons-parameters-comp pointer-events-none absolute right-3 top-1/2 h-4 w-4 shrink-0 -translate-y-1/2",
|
||||
disabled ? "text-placeholder-foreground" : "text-foreground",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", disabled && "pointer-events-none")}>
|
||||
<MustachePromptModal
|
||||
id={id}
|
||||
field_name={field_name}
|
||||
readonly={readonly}
|
||||
value={value}
|
||||
setValue={(newValue) => handleOnNewValue({ value: newValue })}
|
||||
nodeClass={nodeClass}
|
||||
setNodeClass={handleNodeClass}
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
className="w-full"
|
||||
data-testid="button_open_mustache_prompt_modal"
|
||||
>
|
||||
<div className="relative w-full">
|
||||
{renderPromptText()}
|
||||
{renderExternalLinkIcon()}
|
||||
</div>
|
||||
</Button>
|
||||
</MustachePromptModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
// TypeScript-only test to verify component props interface
|
||||
// This test doesn't run any actual component code to avoid Jest configuration issues
|
||||
|
||||
describe("ParameterRenderComponent Types", () => {
|
||||
it("should accept correct props type", () => {
|
||||
// This test verifies that the component accepts the correct props structure
|
||||
const baseProps = {
|
||||
handleOnNewValue: jest.fn(),
|
||||
name: "test-field",
|
||||
nodeId: "test-node-id",
|
||||
editNode: false,
|
||||
handleNodeClass: jest.fn(),
|
||||
nodeClass: {
|
||||
description: "Test component",
|
||||
template: {},
|
||||
display_name: "Test Component",
|
||||
documentation: "Test component documentation",
|
||||
},
|
||||
disabled: false,
|
||||
templateValue: "Hello {{name}}!",
|
||||
};
|
||||
|
||||
// Test different template data types
|
||||
const mustacheTemplateData = { type: "mustache", name: "template" };
|
||||
const promptTemplateData = { type: "prompt", name: "template" };
|
||||
|
||||
// If this compiles without TypeScript errors, the types are correct
|
||||
expect(baseProps).toBeDefined();
|
||||
expect(baseProps.nodeClass.description).toBe("Test component");
|
||||
expect(baseProps.nodeClass.display_name).toBe("Test Component");
|
||||
expect(baseProps.nodeClass.documentation).toBe(
|
||||
"Test component documentation",
|
||||
);
|
||||
expect(mustacheTemplateData.type).toBe("mustache");
|
||||
expect(promptTemplateData.type).toBe("prompt");
|
||||
});
|
||||
});
|
||||
@ -17,6 +17,7 @@ import IntComponent from "./components/intComponent";
|
||||
import KeypairListComponent from "./components/keypairListComponent";
|
||||
import McpComponent from "./components/mcpComponent";
|
||||
import MultiselectComponent from "./components/multiselectComponent";
|
||||
import MustachePromptAreaComponent from "./components/mustachePromptComponent";
|
||||
import PromptAreaComponent from "./components/promptComponent";
|
||||
import QueryComponent from "./components/queryComponent";
|
||||
import SortableListComponent from "./components/sortableListComponent";
|
||||
@ -190,6 +191,15 @@ export function ParameterRenderComponent({
|
||||
id={`promptarea_${id}`}
|
||||
/>
|
||||
);
|
||||
case "mustache":
|
||||
return (
|
||||
<MustachePromptAreaComponent
|
||||
{...baseInputProps}
|
||||
readonly={!!nodeClass.flow}
|
||||
field_name={name}
|
||||
id={`mustachepromptarea_${id}`}
|
||||
/>
|
||||
);
|
||||
case "code":
|
||||
return <CodeAreaComponent {...baseInputProps} id={`codearea_${id}`} />;
|
||||
case "table":
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
import { customDefaultShortcuts } from "../customization/constants";
|
||||
import type { languageMap } from "../types/components";
|
||||
|
||||
const getEnvVar = (key: string, defaultValue: any = undefined) => {
|
||||
const getEnvVar = (key: string, defaultValue: unknown = undefined) => {
|
||||
if (typeof process !== "undefined" && process.env) {
|
||||
return process.env[key] ?? defaultValue;
|
||||
}
|
||||
@ -172,6 +172,9 @@ export const CODE_DICT_DIALOG_SUBTITLE =
|
||||
export const PROMPT_DIALOG_SUBTITLE =
|
||||
"Create your prompt. Prompts can help guide the behavior of a Language Model. Use curly brackets {} to introduce variables.";
|
||||
|
||||
export const MUSTACHE_PROMPT_DIALOG_SUBTITLE =
|
||||
"Create your prompt. Prompts can help guide the behavior of a Language Model. Use double curly brackets {{}} to introduce variables.";
|
||||
|
||||
export const CHAT_CANNOT_OPEN_TITLE = "Chat Cannot Open";
|
||||
|
||||
export const CHAT_CANNOT_OPEN_DESCRIPTION = "This is not a chat flow.";
|
||||
@ -673,6 +676,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
||||
"float",
|
||||
"code",
|
||||
"prompt",
|
||||
"mustache",
|
||||
"file",
|
||||
"int",
|
||||
"dict",
|
||||
|
||||
@ -13,6 +13,7 @@ interface IPostValidatePrompt {
|
||||
name: string;
|
||||
template: string;
|
||||
frontend_node: APIClassType;
|
||||
mustache?: boolean;
|
||||
}
|
||||
|
||||
export const usePostValidatePrompt: useMutationFunctionType<
|
||||
@ -32,6 +33,7 @@ export const usePostValidatePrompt: useMutationFunctionType<
|
||||
name: payload.name,
|
||||
template: payload.template,
|
||||
frontend_node: payload.frontend_node,
|
||||
mustache: payload.mustache ?? false,
|
||||
},
|
||||
);
|
||||
|
||||
@ -42,7 +44,10 @@ export const usePostValidatePrompt: useMutationFunctionType<
|
||||
PromptTypeAPI,
|
||||
ResponseErrorDetailAPI,
|
||||
IPostValidatePrompt
|
||||
> = mutate(["usePostValidatePrompt"], postValidatePromptFn, options);
|
||||
> = mutate(["usePostValidatePrompt"], postValidatePromptFn, {
|
||||
...options,
|
||||
retry: 0, // Don't retry validation errors - they're not transient
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
@ -0,0 +1,524 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import MustachePromptModal from "../index";
|
||||
|
||||
// Mock the API hook
|
||||
const mockMutate = jest.fn();
|
||||
jest.mock("@/controllers/API/queries/nodes/use-post-validate-prompt", () => ({
|
||||
usePostValidatePrompt: () => ({
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock alert store
|
||||
const mockSetSuccessData = jest.fn();
|
||||
const mockSetErrorData = jest.fn();
|
||||
const mockSetNoticeData = jest.fn();
|
||||
|
||||
interface AlertState {
|
||||
setSuccessData: typeof mockSetSuccessData;
|
||||
setErrorData: typeof mockSetErrorData;
|
||||
setNoticeData: typeof mockSetNoticeData;
|
||||
}
|
||||
|
||||
jest.mock("@/stores/alertStore", () => ({
|
||||
__esModule: true,
|
||||
default: <T,>(selector: (state: AlertState) => T): T => {
|
||||
const state: AlertState = {
|
||||
setSuccessData: mockSetSuccessData,
|
||||
setErrorData: mockSetErrorData,
|
||||
setNoticeData: mockSetNoticeData,
|
||||
};
|
||||
return selector(state);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock IconComponent
|
||||
jest.mock("@/components/common/genericIconComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ name, className }: { name: string; className?: string }) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock SanitizedHTMLWrapper
|
||||
jest.mock("@/components/common/sanitizedHTMLWrapper", () => {
|
||||
const React = require("react");
|
||||
return React.forwardRef(function MockSanitizedHTMLWrapper(
|
||||
{
|
||||
content,
|
||||
className,
|
||||
onClick,
|
||||
}: { content: string; className?: string; onClick?: () => void },
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-testid="sanitized-html"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Mock ShadTooltip
|
||||
jest.mock("@/components/common/shadTooltipComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
content,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
content: string;
|
||||
}) => <div data-tooltip={content}>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock BaseModal
|
||||
jest.mock("@/modals/baseModal", () => {
|
||||
const React = require("react");
|
||||
|
||||
interface ChildrenProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface HeaderProps extends ChildrenProps {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface TriggerProps extends ChildrenProps {
|
||||
disable?: boolean;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
interface BaseModalProps extends ChildrenProps {
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
interface ReactChild {
|
||||
type?: {
|
||||
displayName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const MockContent = ({ children }: ChildrenProps) => (
|
||||
<div data-testid="modal-content">{children}</div>
|
||||
);
|
||||
const MockHeader = ({ children, description }: HeaderProps) => (
|
||||
<div data-testid="modal-header" data-description={description}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const MockTrigger = ({ children, disable }: TriggerProps) => (
|
||||
<div data-testid="modal-trigger" data-disabled={disable}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const MockFooter = ({ children }: ChildrenProps) => (
|
||||
<div data-testid="modal-footer">{children}</div>
|
||||
);
|
||||
|
||||
function MockBaseModal({ children, open, setOpen, size }: BaseModalProps) {
|
||||
// Only render children when open
|
||||
if (!open) {
|
||||
// Still render the trigger so we can click it
|
||||
const trigger = React.Children.toArray(children).find(
|
||||
(child: ReactChild) => child?.type?.displayName === "Trigger",
|
||||
);
|
||||
return (
|
||||
<div data-testid="base-modal-closed">
|
||||
{trigger}
|
||||
<button data-testid="mock-open-modal" onClick={() => setOpen?.(true)}>
|
||||
Open Modal
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="base-modal" data-size={size}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MockContent.displayName = "Content";
|
||||
MockHeader.displayName = "Header";
|
||||
MockTrigger.displayName = "Trigger";
|
||||
MockFooter.displayName = "Footer";
|
||||
|
||||
MockBaseModal.Content = MockContent;
|
||||
MockBaseModal.Header = MockHeader;
|
||||
MockBaseModal.Trigger = MockTrigger;
|
||||
MockBaseModal.Footer = MockFooter;
|
||||
|
||||
return { __esModule: true, default: MockBaseModal };
|
||||
});
|
||||
|
||||
// Mock varHighlightHTML
|
||||
jest.mock("@/modals/promptModal/utils/var-highlight-html", () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
name,
|
||||
addCurlyBraces,
|
||||
}: {
|
||||
name: string;
|
||||
addCurlyBraces: boolean;
|
||||
}) => `<span class="highlighted">${name}</span>`,
|
||||
}));
|
||||
|
||||
// Mock reactflowUtils
|
||||
jest.mock("@/utils/reactflowUtils", () => ({
|
||||
handleKeyDown: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MustachePromptModal", () => {
|
||||
const defaultProps = {
|
||||
field_name: "template",
|
||||
value: "",
|
||||
setValue: jest.fn(),
|
||||
nodeClass: {
|
||||
template: {
|
||||
template: { value: "" },
|
||||
},
|
||||
},
|
||||
setNodeClass: jest.fn(),
|
||||
children: <button data-testid="trigger">Open</button>,
|
||||
disabled: false,
|
||||
id: "test-modal",
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockMutate.mockClear();
|
||||
mockSetSuccessData.mockClear();
|
||||
mockSetErrorData.mockClear();
|
||||
mockSetNoticeData.mockClear();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
it("should render the modal trigger", () => {
|
||||
render(<MustachePromptModal {...defaultProps} />);
|
||||
expect(screen.getByTestId("modal-trigger")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render modal content when open", () => {
|
||||
render(<MustachePromptModal {...defaultProps} />);
|
||||
|
||||
// Click to open the modal
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
expect(screen.getByTestId("modal-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("modal-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("modal-footer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Edit Prompt title", () => {
|
||||
render(<MustachePromptModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
expect(screen.getByTestId("modal-title")).toHaveTextContent(
|
||||
"Edit Prompt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render with initial value", () => {
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
expect(textarea).toHaveValue("Hello {{name}}");
|
||||
});
|
||||
});
|
||||
|
||||
describe("variable extraction", () => {
|
||||
it("should extract mustache variables and display as badges", () => {
|
||||
render(
|
||||
<MustachePromptModal
|
||||
{...defaultProps}
|
||||
value="Hello {{name}}, welcome to {{place}}!"
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Look for badge elements
|
||||
expect(screen.getByText("name")).toBeInTheDocument();
|
||||
expect(screen.getByText("place")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not extract invalid variable patterns", () => {
|
||||
render(
|
||||
<MustachePromptModal
|
||||
{...defaultProps}
|
||||
value="Invalid: {{123abc}} {{with space}}"
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Invalid patterns should not be shown as badges
|
||||
expect(screen.queryByText("123abc")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("with space")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should deduplicate variables", () => {
|
||||
render(
|
||||
<MustachePromptModal
|
||||
{...defaultProps}
|
||||
value="{{name}} and {{name}} and {{name}}"
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Should only show one badge for "name"
|
||||
const badges = screen.getAllByText("name");
|
||||
expect(badges).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("textarea editing", () => {
|
||||
it("should update input value when typing", async () => {
|
||||
render(<MustachePromptModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
// Use fireEvent.change for mustache syntax since userEvent.type treats { as special chars
|
||||
fireEvent.change(textarea, { target: { value: "Hello {{world}}" } });
|
||||
|
||||
expect(textarea).toHaveValue("Hello {{world}}");
|
||||
});
|
||||
|
||||
it("should update variables when input changes", async () => {
|
||||
render(<MustachePromptModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
// Use fireEvent.change for mustache syntax since userEvent.type treats { as special chars
|
||||
fireEvent.change(textarea, { target: { value: "{{new_var}}" } });
|
||||
|
||||
expect(screen.getByText("new_var")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation and saving", () => {
|
||||
it("should call validatePrompt when save button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "template",
|
||||
template: "Hello {{name}}",
|
||||
mustache: true,
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call setValue on successful validation", async () => {
|
||||
const mockSetValue = jest.fn();
|
||||
const mockSetNodeClass = jest.fn();
|
||||
|
||||
mockMutate.mockImplementation((data, options) => {
|
||||
options.onSuccess({
|
||||
frontend_node: {
|
||||
template: { template: { value: data.template } },
|
||||
},
|
||||
input_variables: ["name"],
|
||||
});
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<MustachePromptModal
|
||||
{...defaultProps}
|
||||
value="Hello {{name}}"
|
||||
setValue={mockSetValue}
|
||||
setNodeClass={mockSetNodeClass}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockSetValue).toHaveBeenCalledWith("Hello {{name}}");
|
||||
expect(mockSetSuccessData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show notice when no input variables are found", async () => {
|
||||
mockMutate.mockImplementation((data, options) => {
|
||||
options.onSuccess({
|
||||
frontend_node: {
|
||||
template: { template: { value: data.template } },
|
||||
},
|
||||
input_variables: [],
|
||||
});
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello world" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockSetNoticeData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show error on validation failure", async () => {
|
||||
mockMutate.mockImplementation((data, options) => {
|
||||
options.onError({
|
||||
response: {
|
||||
data: {
|
||||
detail: "Validation failed",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockSetErrorData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
list: expect.arrayContaining(["Validation failed"]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show bug alert when apiReturn is empty", async () => {
|
||||
mockMutate.mockImplementation((data, options) => {
|
||||
options.onSuccess(null);
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockSetErrorData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readonly mode", () => {
|
||||
it("should disable save button when readonly", () => {
|
||||
render(<MustachePromptModal {...defaultProps} readonly={true} />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const saveButton = screen.getByTestId("genericModalBtnSave");
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit/preview mode toggle", () => {
|
||||
it("should start in edit mode", () => {
|
||||
render(<MustachePromptModal {...defaultProps} value="test" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// In edit mode, textarea should be visible
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should switch to preview mode on blur", async () => {
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// After blur, should show preview (SanitizedHTMLWrapper)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sanitized-html")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should switch back to edit mode when clicking preview", async () => {
|
||||
render(<MustachePromptModal {...defaultProps} value="Hello {{name}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Blur to switch to preview
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// Wait for preview mode
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sanitized-html")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on preview to switch back to edit mode
|
||||
fireEvent.click(screen.getByTestId("sanitized-html"));
|
||||
|
||||
// Should be back in edit mode
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("modal-test-modal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS class computation", () => {
|
||||
it("should use code-nohighlight class for short variable names", () => {
|
||||
render(<MustachePromptModal {...defaultProps} value="{{a}} {{b}}" />);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Blur to preview mode
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
fireEvent.blur(textarea);
|
||||
|
||||
// The SanitizedHTMLWrapper should have code-nohighlight class
|
||||
const preview = screen.getByTestId("sanitized-html");
|
||||
expect(preview.className).toContain("code-nohighlight");
|
||||
});
|
||||
});
|
||||
|
||||
describe("badge truncation", () => {
|
||||
it("should truncate long variable names in badges", () => {
|
||||
const longVarName = "a".repeat(70); // Over 59 characters
|
||||
render(
|
||||
<MustachePromptModal {...defaultProps} value={`{{${longVarName}}}`} />,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
// Should show truncated name with ...
|
||||
const badge = screen.getByText(/\.\.\.$/);
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("value synchronization", () => {
|
||||
it("should sync inputValue when value prop changes", () => {
|
||||
const { rerender } = render(
|
||||
<MustachePromptModal {...defaultProps} value="initial" />,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("mock-open-modal"));
|
||||
|
||||
const textarea = screen.getByTestId("modal-test-modal");
|
||||
expect(textarea).toHaveValue("initial");
|
||||
|
||||
// Update the value prop
|
||||
rerender(<MustachePromptModal {...defaultProps} value="updated" />);
|
||||
|
||||
expect(textarea).toHaveValue("updated");
|
||||
});
|
||||
});
|
||||
});
|
||||
319
src/frontend/src/modals/mustachePromptModal/index.tsx
Normal file
319
src/frontend/src/modals/mustachePromptModal/index.tsx
Normal file
@ -0,0 +1,319 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { usePostValidatePrompt } from "@/controllers/API/queries/nodes/use-post-validate-prompt";
|
||||
import IconComponent from "../../components/common/genericIconComponent";
|
||||
import SanitizedHTMLWrapper from "../../components/common/sanitizedHTMLWrapper";
|
||||
import ShadTooltip from "../../components/common/shadTooltipComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import {
|
||||
BUG_ALERT,
|
||||
PROMPT_ERROR_ALERT,
|
||||
PROMPT_SUCCESS_ALERT,
|
||||
TEMP_NOTICE_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
EDIT_TEXT_PLACEHOLDER,
|
||||
MAX_WORDS_HIGHLIGHT,
|
||||
MUSTACHE_PROMPT_DIALOG_SUBTITLE,
|
||||
} from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { PromptModalType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
import varHighlightHTML from "../promptModal/utils/var-highlight-html";
|
||||
|
||||
// Simple regex to extract mustache variables - only matches valid {{variable_name}} patterns
|
||||
const SIMPLE_VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
||||
|
||||
// Type for non-standard caretPositionFromPoint API (not supported in Safari)
|
||||
interface CaretPosition {
|
||||
offset: number;
|
||||
}
|
||||
interface DocumentWithCaretPosition extends Document {
|
||||
caretPositionFromPoint(x: number, y: number): CaretPosition | null;
|
||||
}
|
||||
|
||||
export default function MustachePromptModal({
|
||||
field_name = "",
|
||||
value,
|
||||
setValue,
|
||||
nodeClass,
|
||||
setNodeClass,
|
||||
children,
|
||||
disabled,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: PromptModalType): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEdit, setIsEdit] = useState(true);
|
||||
const [wordsHighlight, setWordsHighlight] = useState<Set<string>>(new Set());
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
const divRef = useRef(null);
|
||||
const _divRefPrompt = useRef(null);
|
||||
const { mutate: postValidatePrompt } = usePostValidatePrompt();
|
||||
const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 });
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
function checkVariables(valueToCheck: string): void {
|
||||
// Extract only valid mustache variables {{variable_name}}
|
||||
const matches: string[] = [];
|
||||
const regex = new RegExp(SIMPLE_VARIABLE_PATTERN.source, "g");
|
||||
let match: RegExpExecArray | null = regex.exec(valueToCheck);
|
||||
|
||||
while (match !== null) {
|
||||
const varName = match[1];
|
||||
if (!matches.includes(varName)) {
|
||||
matches.push(varName);
|
||||
}
|
||||
match = regex.exec(valueToCheck);
|
||||
}
|
||||
|
||||
setWordsHighlight(new Set(matches.map((v) => `{{${v}}}`)));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValue && inputValue !== "") {
|
||||
checkVariables(inputValue);
|
||||
}
|
||||
}, [inputValue]);
|
||||
|
||||
const coloredContent = (typeof inputValue === "string" ? inputValue : "")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match) => {
|
||||
return varHighlightHTML({ name: match, addCurlyBraces: false });
|
||||
})
|
||||
.replace(/\n/g, "<br />");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof value === "string") setInputValue(value);
|
||||
}, [value, modalOpen]);
|
||||
|
||||
function getClassByNumberLength(): string {
|
||||
let sumOfCaracteres: number = 0;
|
||||
wordsHighlight.forEach((element) => {
|
||||
sumOfCaracteres = sumOfCaracteres + element.replace(/[{}]/g, "").length;
|
||||
});
|
||||
return sumOfCaracteres > MAX_WORDS_HIGHLIGHT
|
||||
? "code-highlight"
|
||||
: "code-nohighlight";
|
||||
}
|
||||
|
||||
// Function need some review, working for now
|
||||
function validatePrompt(closeModal: boolean): void {
|
||||
//nodeClass is always null on tweaks
|
||||
postValidatePrompt(
|
||||
{
|
||||
name: field_name,
|
||||
template: inputValue,
|
||||
frontend_node: nodeClass!,
|
||||
mustache: true,
|
||||
},
|
||||
{
|
||||
onSuccess: (apiReturn) => {
|
||||
if (field_name === "") {
|
||||
field_name = Array.isArray(
|
||||
apiReturn?.frontend_node?.custom_fields?.[""],
|
||||
)
|
||||
? (apiReturn?.frontend_node?.custom_fields?.[""][0] ?? "")
|
||||
: (apiReturn?.frontend_node?.custom_fields?.[""] ?? "");
|
||||
}
|
||||
if (apiReturn) {
|
||||
const inputVariables = apiReturn.input_variables ?? [];
|
||||
if (
|
||||
JSON.stringify(apiReturn?.frontend_node) !== JSON.stringify({})
|
||||
) {
|
||||
setValue(inputValue);
|
||||
apiReturn.frontend_node.template.template.value = inputValue;
|
||||
if (setNodeClass) setNodeClass(apiReturn?.frontend_node);
|
||||
setModalOpen(closeModal);
|
||||
setIsEdit(false);
|
||||
}
|
||||
if (!inputVariables || inputVariables.length === 0) {
|
||||
setNoticeData({
|
||||
title: TEMP_NOTICE_ALERT,
|
||||
});
|
||||
} else {
|
||||
setSuccessData({
|
||||
title: PROMPT_SUCCESS_ALERT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setIsEdit(true);
|
||||
setErrorData({
|
||||
title: BUG_ALERT,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsEdit(true);
|
||||
return setErrorData({
|
||||
title: PROMPT_ERROR_ALERT,
|
||||
list: [error.response.data.detail ?? ""],
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const handlePreviewClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isEdit && !readonly) {
|
||||
const clickX = e.clientX;
|
||||
const clickY = e.clientY;
|
||||
setClickPosition({ x: clickX, y: clickY });
|
||||
setScrollPosition(e.currentTarget.scrollTop);
|
||||
setIsEdit(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
textareaRef.current.scrollTop = scrollPosition;
|
||||
|
||||
const textArea = textareaRef.current;
|
||||
const { x, y } = clickPosition;
|
||||
|
||||
// Use caretPositionFromPoint to get the closest text position. Does not work on Safari.
|
||||
if ("caretPositionFromPoint" in document) {
|
||||
const docWithCaret = document as DocumentWithCaretPosition;
|
||||
const range = docWithCaret.caretPositionFromPoint(x, y)?.offset ?? 0;
|
||||
if (range) {
|
||||
const position = range;
|
||||
textArea.setSelectionRange(position, position);
|
||||
}
|
||||
}
|
||||
} else if (!isEdit && previewRef.current) {
|
||||
previewRef.current.scrollTop = scrollPosition;
|
||||
}
|
||||
}, [isEdit, clickPosition, scrollPosition]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
onChangeOpenModal={(open) => {}}
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
size="x-large"
|
||||
>
|
||||
<BaseModal.Trigger disable={disabled} asChild>
|
||||
{children}
|
||||
</BaseModal.Trigger>
|
||||
<BaseModal.Header description={MUSTACHE_PROMPT_DIALOG_SUBTITLE}>
|
||||
<div className="flex w-full items-start gap-3">
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="TerminalSquare"
|
||||
className="h-6 w-6 pr-1 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="pl-2" data-testid="modal-title">
|
||||
Edit Prompt
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content overflowHidden>
|
||||
<div className={classNames("flex h-full w-full rounded-lg border")}>
|
||||
{isEdit && !readonly ? (
|
||||
<Textarea
|
||||
id={"modal-" + id}
|
||||
data-testid={"modal-" + id}
|
||||
ref={textareaRef}
|
||||
className="form-input h-full w-full resize-none rounded-lg border-0 custom-scroll focus-visible:ring-1"
|
||||
value={inputValue}
|
||||
onBlur={() => {
|
||||
setScrollPosition(textareaRef.current?.scrollTop || 0);
|
||||
setIsEdit(false);
|
||||
}}
|
||||
autoFocus
|
||||
onChange={(event) => {
|
||||
setInputValue(event.target.value);
|
||||
checkVariables(event.target.value);
|
||||
}}
|
||||
placeholder={EDIT_TEXT_PLACEHOLDER}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, inputValue, "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SanitizedHTMLWrapper
|
||||
ref={previewRef}
|
||||
className={getClassByNumberLength() + " bg-muted"}
|
||||
onClick={handlePreviewClick}
|
||||
content={coloredContent}
|
||||
suppressWarning={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<div className="flex w-full shrink-0 items-end justify-between">
|
||||
<div className="mb-auto flex-1">
|
||||
<div className="mr-2">
|
||||
<div
|
||||
ref={divRef}
|
||||
className="max-h-20 overflow-y-auto custom-scroll"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<IconComponent
|
||||
name="Braces"
|
||||
className="flex h-4 w-4 text-primary"
|
||||
/>
|
||||
<span className="text-md font-semibold text-primary">
|
||||
Prompt Variables:
|
||||
</span>
|
||||
|
||||
{Array.from(wordsHighlight).map((word, index) => (
|
||||
<ShadTooltip
|
||||
key={index}
|
||||
content={word.replace(/[{}]/g, "")}
|
||||
asChild={false}
|
||||
>
|
||||
<Badge
|
||||
key={index}
|
||||
variant="gray"
|
||||
size="md"
|
||||
className="max-w-[40vw] cursor-default truncate p-1 text-sm"
|
||||
>
|
||||
<div className="relative bottom-[1px]">
|
||||
<span id={"badge" + index.toString()}>
|
||||
{word.replace(/[{}]/g, "").length > 59
|
||||
? word.replace(/[{}]/g, "").slice(0, 56) + "..."
|
||||
: word.replace(/[{}]/g, "")}
|
||||
</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="mt-2 text-xs text-muted-foreground">
|
||||
Prompt variables can be created with any chosen name inside
|
||||
double curly brackets, e.g. {"{{variable_name}}"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="genericModalBtnSave"
|
||||
id="genericModalBtnSave"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
validatePrompt(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Check & Save
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,11 @@
|
||||
import type { IVarHighlightType } from "../../../types/components";
|
||||
|
||||
export default function varHighlightHTML({ name }: IVarHighlightType): string {
|
||||
const html = `<span class="chat-message-highlight">{${name}}</span>`;
|
||||
return html;
|
||||
export default function varHighlightHTML({
|
||||
name,
|
||||
addCurlyBraces,
|
||||
}: IVarHighlightType): string {
|
||||
if (addCurlyBraces) {
|
||||
return `<span class="font-semibold chat-message-highlight">{${name}}</span>`;
|
||||
}
|
||||
return `<span class="font-semibold chat-message-highlight">${name}</span>`;
|
||||
}
|
||||
|
||||
@ -296,6 +296,7 @@ export type TextHighlightType = {
|
||||
|
||||
export interface IVarHighlightType {
|
||||
name: string;
|
||||
addCurlyBraces?: boolean;
|
||||
}
|
||||
|
||||
export type IconComponentProps = {
|
||||
|
||||
214
src/frontend/src/utils/__tests__/mustacheUtils.test.ts
Normal file
214
src/frontend/src/utils/__tests__/mustacheUtils.test.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import {
|
||||
extractMustacheVariables,
|
||||
type MustacheValidationResult,
|
||||
validateMustacheTemplate,
|
||||
} from "../mustacheUtils";
|
||||
|
||||
describe("mustacheUtils", () => {
|
||||
describe("validateMustacheTemplate", () => {
|
||||
describe("valid templates", () => {
|
||||
it("should return valid for empty string", () => {
|
||||
const result = validateMustacheTemplate("");
|
||||
expect(result).toEqual({ isValid: true, variables: [] });
|
||||
});
|
||||
|
||||
it("should return valid for plain text without variables", () => {
|
||||
const result = validateMustacheTemplate("Hello, this is plain text.");
|
||||
expect(result).toEqual({ isValid: true, variables: [] });
|
||||
});
|
||||
|
||||
it("should return valid for simple variable", () => {
|
||||
const result = validateMustacheTemplate("Hello {{name}}!");
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["name"]);
|
||||
});
|
||||
|
||||
it("should return valid for multiple different variables", () => {
|
||||
const result = validateMustacheTemplate(
|
||||
"Hello {{first_name}} {{last_name}}!",
|
||||
);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["first_name", "last_name"]);
|
||||
});
|
||||
|
||||
it("should deduplicate repeated variables", () => {
|
||||
const result = validateMustacheTemplate(
|
||||
"{{name}} is {{name}} and {{name}}",
|
||||
);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["name"]);
|
||||
});
|
||||
|
||||
it("should accept variables starting with underscore", () => {
|
||||
const result = validateMustacheTemplate("Value: {{_private_var}}");
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["_private_var"]);
|
||||
});
|
||||
|
||||
it("should accept variables with numbers", () => {
|
||||
const result = validateMustacheTemplate("Value: {{var123}}");
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["var123"]);
|
||||
});
|
||||
|
||||
it("should handle multiline templates", () => {
|
||||
const template = `
|
||||
Hello {{name}},
|
||||
Your order {{order_id}} is ready.
|
||||
Thanks!
|
||||
`;
|
||||
const result = validateMustacheTemplate(template);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.variables).toEqual(["name", "order_id"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dangerous patterns", () => {
|
||||
it("should reject triple braces (unescaped HTML)", () => {
|
||||
const result = validateMustacheTemplate("{{{unsafe}}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
expect(result.variables).toEqual([]);
|
||||
});
|
||||
|
||||
it("should reject conditional sections start", () => {
|
||||
const result = validateMustacheTemplate(
|
||||
"{{#section}}content{{/section}}",
|
||||
);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject conditional sections end", () => {
|
||||
const result = validateMustacheTemplate("{{/section}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject inverted sections", () => {
|
||||
const result = validateMustacheTemplate(
|
||||
"{{^inverted}}no value{{/inverted}}",
|
||||
);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject unescaped variables with &", () => {
|
||||
const result = validateMustacheTemplate("{{&unescaped}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject partials", () => {
|
||||
const result = validateMustacheTemplate("{{>partial}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject comments", () => {
|
||||
const result = validateMustacheTemplate("{{! this is a comment }}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject current context", () => {
|
||||
const result = validateMustacheTemplate("{{.}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Complex mustache syntax is not allowed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unclosed tags", () => {
|
||||
it("should reject unclosed opening braces", () => {
|
||||
const result = validateMustacheTemplate("Hello {{name");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("matching closing braces");
|
||||
});
|
||||
|
||||
it("should reject multiple unclosed braces", () => {
|
||||
const result = validateMustacheTemplate("{{foo}} and {{bar");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("matching closing braces");
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid variable names", () => {
|
||||
it("should reject variable names starting with number", () => {
|
||||
const result = validateMustacheTemplate("{{123abc}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("Invalid mustache variable");
|
||||
});
|
||||
|
||||
it("should reject variables with spaces", () => {
|
||||
const result = validateMustacheTemplate("{{my variable}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("Invalid mustache variable");
|
||||
});
|
||||
|
||||
it("should reject variables with special characters", () => {
|
||||
const result = validateMustacheTemplate("{{my-variable}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("Invalid mustache variable");
|
||||
});
|
||||
|
||||
it("should reject empty braces", () => {
|
||||
const result = validateMustacheTemplate("{{}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("Invalid mustache variable");
|
||||
});
|
||||
|
||||
it("should reject variables with dots (nested access)", () => {
|
||||
const result = validateMustacheTemplate("{{user.name}}");
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain("Invalid mustache variable");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMustacheVariables", () => {
|
||||
it("should extract variables from valid template", () => {
|
||||
const variables = extractMustacheVariables(
|
||||
"Hello {{name}}, your {{item}} is ready.",
|
||||
);
|
||||
expect(variables).toEqual(["name", "item"]);
|
||||
});
|
||||
|
||||
it("should return empty array for invalid template", () => {
|
||||
const variables = extractMustacheVariables(
|
||||
"{{#section}}content{{/section}}",
|
||||
);
|
||||
expect(variables).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for empty template", () => {
|
||||
const variables = extractMustacheVariables("");
|
||||
expect(variables).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for template without variables", () => {
|
||||
const variables = extractMustacheVariables("Just plain text");
|
||||
expect(variables).toEqual([]);
|
||||
});
|
||||
|
||||
it("should deduplicate variables", () => {
|
||||
const variables = extractMustacheVariables("{{x}} + {{x}} = {{result}}");
|
||||
expect(variables).toEqual(["x", "result"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
108
src/frontend/src/utils/mustacheUtils.ts
Normal file
108
src/frontend/src/utils/mustacheUtils.ts
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Security utilities for mustache template processing.
|
||||
* Mirrors the backend validation in lfx/utils/mustache_security.py
|
||||
*/
|
||||
|
||||
// Regex pattern for simple variables only - same as backend
|
||||
const SIMPLE_VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
||||
|
||||
// Patterns for complex mustache syntax that we want to block
|
||||
const DANGEROUS_PATTERNS = [
|
||||
/\{\{\{/, // Triple braces (unescaped HTML in Mustache)
|
||||
/\{\{#/, // Conditionals/sections start
|
||||
/\{\{\//, // Conditionals/sections end
|
||||
/\{\{\^/, // Inverted sections
|
||||
/\{\{&/, // Unescaped variables
|
||||
/\{\{>/, // Partials
|
||||
/\{\{!/, // Comments
|
||||
/\{\{\./, // Current context
|
||||
];
|
||||
|
||||
export interface MustacheValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
variables: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a mustache template only contains simple variable substitutions.
|
||||
* Returns validation result with extracted variables if valid.
|
||||
*/
|
||||
export function validateMustacheTemplate(
|
||||
template: string,
|
||||
): MustacheValidationResult {
|
||||
if (!template) {
|
||||
return { isValid: true, variables: [] };
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(template)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
"Complex mustache syntax is not allowed. Only simple variable substitution like {{variable}} is permitted.",
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unclosed tags - {{ without a matching }}
|
||||
// Find all {{ and check each one has a closing }}
|
||||
let searchPos = 0;
|
||||
while (true) {
|
||||
const openPos = template.indexOf("{{", searchPos);
|
||||
if (openPos === -1) break;
|
||||
|
||||
const afterOpen = template.slice(openPos + 2);
|
||||
const closePos = afterOpen.indexOf("}}");
|
||||
|
||||
if (closePos === -1) {
|
||||
// No }} found after this {{
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Invalid template syntax. Check that all {{variables}} have matching closing braces.`,
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Move past this {{ to find the next one
|
||||
searchPos = openPos + 2 + closePos + 2;
|
||||
}
|
||||
|
||||
// Find all {{ }} patterns and validate them
|
||||
const allMustachePatterns = template.match(/\{\{[^}]*\}\}/g) || [];
|
||||
const simpleVarPattern = /^\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}$/;
|
||||
|
||||
for (const pattern of allMustachePatterns) {
|
||||
if (!simpleVarPattern.test(pattern)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Invalid mustache variable: ${pattern}. Only simple variable names like {{variable}} are allowed.`,
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Extract valid variables
|
||||
const variables: string[] = [];
|
||||
const regex = new RegExp(SIMPLE_VARIABLE_PATTERN.source, "g");
|
||||
let match: RegExpExecArray | null = regex.exec(template);
|
||||
while (match !== null) {
|
||||
if (!variables.includes(match[1])) {
|
||||
variables.push(match[1]);
|
||||
}
|
||||
match = regex.exec(template);
|
||||
}
|
||||
|
||||
return { isValid: true, variables };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract simple variable names from a mustache template.
|
||||
* Only extracts valid {{variable}} patterns, ignoring complex syntax.
|
||||
*/
|
||||
export function extractMustacheVariables(template: string): string[] {
|
||||
const result = validateMustacheTemplate(template);
|
||||
return result.variables;
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../fixtures";
|
||||
import { adjustScreenView } from "../../utils/adjust-screen-view";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
|
||||
// Helper function to verify prompt variables
|
||||
async function verifyPromptVariables(
|
||||
page: any,
|
||||
page: Page,
|
||||
template: string,
|
||||
expectedVars: string[],
|
||||
isFirstTime = true,
|
||||
@ -356,3 +357,239 @@ test(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"PromptTemplateComponent - Double Brackets Variable Extraction",
|
||||
{ tag: ["@release", "@workspace"], timeout: 60000 },
|
||||
async ({ page }) => {
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("prompt");
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="models_and_agentsPrompt Template"]',
|
||||
{
|
||||
timeout: 3000,
|
||||
},
|
||||
);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="models_and_agentsPrompt Template"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
await adjustScreenView(page);
|
||||
|
||||
// Wait for the node to appear
|
||||
await page.waitForSelector('[data-testid="div-generic-node"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Click on the node to select it
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
// Wait for and click the edit button
|
||||
await page.waitForSelector('[data-testid="edit-button-modal"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.getByTestId("edit-button-modal").last().click();
|
||||
|
||||
// Wait for the modal to open and the toggle to be visible
|
||||
await page.waitForSelector(
|
||||
'[data-testid="toggle_bool_edit_use_double_brackets"]',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
// Enable the "Use Double Brackets" toggle in the modal
|
||||
await page.getByTestId("toggle_bool_edit_use_double_brackets").click();
|
||||
|
||||
// Verify the toggle is now checked
|
||||
expect(
|
||||
await page
|
||||
.getByTestId("toggle_bool_edit_use_double_brackets")
|
||||
.isChecked(),
|
||||
).toBeTruthy();
|
||||
|
||||
// Close the modal
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
// Now test double bracket variable extraction - click the mustache prompt button
|
||||
await page.waitForSelector(
|
||||
'[data-testid="button_open_mustache_prompt_modal"]',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
await page.getByTestId("button_open_mustache_prompt_modal").click();
|
||||
|
||||
// Wait for the prompt modal textarea (mustache modal uses different testid pattern)
|
||||
const mustacheTextareaId = "modal-mustachepromptarea_mustache_template";
|
||||
await page.waitForSelector(`[data-testid="${mustacheTextareaId}"]`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.getByTestId(mustacheTextareaId).fill("Hello {{name}}!");
|
||||
|
||||
let value = await page.getByTestId(mustacheTextareaId).inputValue();
|
||||
|
||||
expect(value).toBe("Hello {{name}}!");
|
||||
|
||||
// Verify the variable badge shows "name"
|
||||
await page.waitForSelector('//*[@id="badge0"]', { timeout: 5000 });
|
||||
const valueBadgeOne = await page.locator('//*[@id="badge0"]').innerText();
|
||||
expect(valueBadgeOne).toBe("name");
|
||||
|
||||
await page.getByTestId("genericModalBtnSave").click();
|
||||
|
||||
// Wait for modal to close and variable input to appear
|
||||
await page.waitForSelector('[data-testid="textarea_str_name"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Verify the input field for the variable was created
|
||||
await page.getByTestId("textarea_str_name").click();
|
||||
await page.getByTestId("textarea_str_name").fill("World");
|
||||
|
||||
value = await page.getByTestId("textarea_str_name").inputValue();
|
||||
expect(value).toBe("World");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"PromptTemplateComponent - Double Brackets Multiple Variables",
|
||||
{ tag: ["@release", "@workspace"], timeout: 60000 },
|
||||
async ({ page }) => {
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("prompt");
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="models_and_agentsPrompt Template"]',
|
||||
{
|
||||
timeout: 3000,
|
||||
},
|
||||
);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="models_and_agentsPrompt Template"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
await adjustScreenView(page);
|
||||
|
||||
// Wait for the node to appear
|
||||
await page.waitForSelector('[data-testid="div-generic-node"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Click on the node to select it
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
|
||||
// Wait for and click the edit button
|
||||
await page.waitForSelector('[data-testid="edit-button-modal"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.getByTestId("edit-button-modal").last().click();
|
||||
|
||||
// Wait for the modal to open and the toggle to be visible
|
||||
await page.waitForSelector(
|
||||
'[data-testid="toggle_bool_edit_use_double_brackets"]',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
// Enable the "Use Double Brackets" toggle in the modal
|
||||
await page.getByTestId("toggle_bool_edit_use_double_brackets").click();
|
||||
|
||||
// Verify the toggle is now checked
|
||||
expect(
|
||||
await page
|
||||
.getByTestId("toggle_bool_edit_use_double_brackets")
|
||||
.isChecked(),
|
||||
).toBeTruthy();
|
||||
|
||||
// Close the modal
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
// Test multiple double bracket variables - click the mustache prompt button
|
||||
await page.waitForSelector(
|
||||
'[data-testid="button_open_mustache_prompt_modal"]',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
await page.getByTestId("button_open_mustache_prompt_modal").click();
|
||||
|
||||
// Wait for the prompt modal textarea (mustache modal uses different testid pattern)
|
||||
const mustacheTextareaId = "modal-mustachepromptarea_mustache_template";
|
||||
await page.waitForSelector(`[data-testid="${mustacheTextareaId}"]`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId(mustacheTextareaId)
|
||||
.fill("{{greeting}} {{name}}! You are {{age}} years old.");
|
||||
|
||||
const value = await page.getByTestId(mustacheTextareaId).inputValue();
|
||||
|
||||
expect(value).toBe("{{greeting}} {{name}}! You are {{age}} years old.");
|
||||
|
||||
// Verify all variable badges
|
||||
await page.waitForSelector('//*[@id="badge0"]', { timeout: 5000 });
|
||||
const badgeGreeting = await page.locator('//*[@id="badge0"]').innerText();
|
||||
expect(badgeGreeting).toBe("greeting");
|
||||
|
||||
const badgeName = await page.locator('//*[@id="badge1"]').innerText();
|
||||
expect(badgeName).toBe("name");
|
||||
|
||||
const badgeAge = await page.locator('//*[@id="badge2"]').innerText();
|
||||
expect(badgeAge).toBe("age");
|
||||
|
||||
// Verify no extra badges
|
||||
const extraBadge = await page
|
||||
.locator('//*[@id="badge3"]')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(extraBadge).toBeFalsy();
|
||||
|
||||
await page.getByTestId("genericModalBtnSave").click();
|
||||
|
||||
// Wait for modal to close and verify input fields for all variables were created
|
||||
await page.waitForSelector('[data-testid="textarea_str_greeting"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="textarea_str_name"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="textarea_str_age"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Fill in the variable inputs and verify they work
|
||||
await page.getByTestId("textarea_str_greeting").fill("Hello");
|
||||
expect(await page.getByTestId("textarea_str_greeting").inputValue()).toBe(
|
||||
"Hello",
|
||||
);
|
||||
|
||||
await page.getByTestId("textarea_str_name").fill("Alice");
|
||||
expect(await page.getByTestId("textarea_str_name").inputValue()).toBe(
|
||||
"Alice",
|
||||
);
|
||||
|
||||
await page.getByTestId("textarea_str_age").fill("25");
|
||||
expect(await page.getByTestId("textarea_str_age").inputValue()).toBe("25");
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
export const addLegacyComponents = async (page: Page) => {
|
||||
await page.getByTestId("sidebar-options-trigger").click();
|
||||
await page.getByTestId("sidebar-legacy-switch").isVisible({ timeout: 5000 });
|
||||
await page.getByTestId("sidebar-legacy-switch").click();
|
||||
await expect(page.getByTestId("sidebar-legacy-switch")).toBeChecked();
|
||||
await page.waitForSelector(
|
||||
'[data-testid="sidebar-legacy-switch"][data-state="checked"]',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
await page.getByTestId("sidebar-options-trigger").click();
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
export const navigateSettingsPages = async (
|
||||
page: Page,
|
||||
@ -17,8 +17,5 @@ export const navigateSettingsPages = async (
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByTestId("settings_menu_header").last()).toContainText(
|
||||
settingsMenuName,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,18 +1,22 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export async function lockFlow(page: Page) {
|
||||
await page.getByTestId("flow_name").click();
|
||||
await page.getByTestId("lock-flow-switch").click();
|
||||
await page.getByTestId("icon-Lock").isVisible({ timeout: 5000 });
|
||||
const lockSwitch = page.getByTestId("lock-flow-switch");
|
||||
await expect(lockSwitch).toBeVisible({ timeout: 1000 });
|
||||
await expect(lockSwitch).toHaveAttribute("data-state", "checked");
|
||||
await page.waitForSelector(
|
||||
'[data-testid="lock-flow-switch"][data-state="checked"]',
|
||||
{
|
||||
timeout: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByTestId("save-flow-settings").isEnabled({ timeout: 3000 });
|
||||
await page.getByTestId("save-flow-settings").click();
|
||||
|
||||
await expect(page.getByTestId("save-flow-settings")).toBeHidden({
|
||||
await page.waitForSelector('[data-testid="save-flow-settings"]', {
|
||||
state: "hidden",
|
||||
timeout: 5000,
|
||||
});
|
||||
//ensure the UI is updated
|
||||
@ -23,15 +27,19 @@ export async function unlockFlow(page: Page) {
|
||||
await page.getByTestId("flow_name").click();
|
||||
await page.getByTestId("lock-flow-switch").click();
|
||||
await page.getByTestId("icon-Lock").isVisible({ timeout: 5000 });
|
||||
const lockSwitch = page.getByTestId("lock-flow-switch");
|
||||
await expect(lockSwitch).toBeVisible({ timeout: 1000 });
|
||||
await expect(lockSwitch).toHaveAttribute("data-state", "unchecked");
|
||||
await page.waitForSelector(
|
||||
'[data-testid="lock-flow-switch"][data-state="unchecked"]',
|
||||
{
|
||||
timeout: 1000,
|
||||
},
|
||||
);
|
||||
//ensure the UI is updated
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTestId("save-flow-settings").isEnabled({ timeout: 3000 });
|
||||
await page.getByTestId("save-flow-settings").click();
|
||||
await expect(page.getByTestId("save-flow-settings")).toBeHidden({
|
||||
await page.waitForSelector('[data-testid="save-flow-settings"]', {
|
||||
state: "hidden",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.prompts.string import mustache_template_vars
|
||||
|
||||
from lfx.inputs.inputs import DefaultPromptField
|
||||
from lfx.interface.utils import extract_input_variables_from_prompt
|
||||
@ -90,7 +91,7 @@ def _check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||
def _check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables) -> None:
|
||||
if any(var for var in input_variables if var not in fixed_variables):
|
||||
error_message = (
|
||||
f"Error: Input variables contain invalid characters or formats. \n"
|
||||
f"Input variables contain invalid characters or formats. \n"
|
||||
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||
@ -122,8 +123,37 @@ def _check_input_variables(input_variables):
|
||||
return fixed_variables
|
||||
|
||||
|
||||
def validate_prompt(prompt_template: str, *, silent_errors: bool = False) -> list[str]:
|
||||
input_variables = extract_input_variables_from_prompt(prompt_template)
|
||||
def validate_prompt(prompt_template: str, *, silent_errors: bool = False, is_mustache: bool = False) -> list[str]:
|
||||
if is_mustache:
|
||||
# Extract only mustache variables
|
||||
try:
|
||||
input_variables = mustache_template_vars(prompt_template)
|
||||
except Exception as exc:
|
||||
# Mustache parser errors are often cryptic (e.g., "unclosed tag at line 1")
|
||||
# Provide a more helpful error message
|
||||
error_str = str(exc).lower()
|
||||
if "unclosed" in error_str or "tag" in error_str:
|
||||
msg = "Invalid template syntax. Check that all {{variables}} have matching opening and closing braces."
|
||||
else:
|
||||
msg = f"Invalid mustache template: {exc}"
|
||||
raise ValueError(msg) from exc
|
||||
|
||||
# Also get f-string variables to filter them out
|
||||
fstring_vars = extract_input_variables_from_prompt(prompt_template)
|
||||
|
||||
# Only keep variables that are actually in mustache syntax (not in f-string syntax)
|
||||
# This handles cases where template has both {var} and {{var}}
|
||||
input_variables = [v for v in input_variables if v not in fstring_vars or f"{{{{{v}}}}}" in prompt_template]
|
||||
else:
|
||||
# Extract f-string variables
|
||||
input_variables = extract_input_variables_from_prompt(prompt_template)
|
||||
|
||||
# Also get mustache variables to filter them out
|
||||
mustache_vars = mustache_template_vars(prompt_template)
|
||||
|
||||
# Only keep variables that are NOT in mustache syntax
|
||||
# This handles cases where template has both {var} and {{var}}
|
||||
input_variables = [v for v in input_variables if v not in mustache_vars]
|
||||
|
||||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = _check_input_variables(input_variables)
|
||||
@ -199,11 +229,16 @@ def update_input_variables_field(input_variables, template) -> None:
|
||||
|
||||
|
||||
def process_prompt_template(
|
||||
template: str, name: str, custom_fields: dict[str, list[str]] | None, frontend_node_template: dict[str, Any]
|
||||
template: str,
|
||||
name: str,
|
||||
custom_fields: dict[str, list[str]] | None,
|
||||
frontend_node_template: dict[str, Any],
|
||||
*,
|
||||
is_mustache: bool = False,
|
||||
):
|
||||
"""Process and validate prompt template, update template and custom fields."""
|
||||
# Validate the prompt template and extract input variables
|
||||
input_variables = validate_prompt(template)
|
||||
input_variables = validate_prompt(template, is_mustache=is_mustache)
|
||||
|
||||
# Initialize custom_fields if None
|
||||
if custom_fields is None:
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
from typing import Any
|
||||
|
||||
from lfx.base.prompts.api_utils import process_prompt_template
|
||||
from lfx.custom.custom_component.component import Component
|
||||
from lfx.inputs.input_mixin import FieldTypes
|
||||
from lfx.inputs.inputs import DefaultPromptField
|
||||
from lfx.io import MessageTextInput, Output, PromptInput
|
||||
from lfx.io import BoolInput, MessageTextInput, Output, PromptInput
|
||||
from lfx.log.logger import logger
|
||||
from lfx.schema.dotdict import dotdict
|
||||
from lfx.schema.message import Message
|
||||
from lfx.template.utils import update_template_values
|
||||
from lfx.utils.mustache_security import validate_mustache_template
|
||||
|
||||
|
||||
class PromptComponent(Component):
|
||||
display_name: str = "Prompt Template"
|
||||
description: str = "Create a prompt template with dynamic variables."
|
||||
documentation: str = "https://docs.langflow.org/components-prompts"
|
||||
icon = "braces"
|
||||
icon = "prompts"
|
||||
trace_type = "prompt"
|
||||
name = "Prompt Template"
|
||||
priority = 0 # Set priority to 0 to make it appear first
|
||||
|
||||
inputs = [
|
||||
PromptInput(name="template", display_name="Template"),
|
||||
BoolInput(
|
||||
name="use_double_brackets",
|
||||
display_name="Use Double Brackets",
|
||||
value=False,
|
||||
advanced=True,
|
||||
info="Use {{variable}} syntax instead of {variable}.",
|
||||
real_time_refresh=True,
|
||||
),
|
||||
MessageTextInput(
|
||||
name="tool_placeholder",
|
||||
display_name="Tool Placeholder",
|
||||
@ -30,34 +44,107 @@ class PromptComponent(Component):
|
||||
Output(display_name="Prompt", name="prompt", method="build_prompt"),
|
||||
]
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:
|
||||
"""Update the template field type based on the selected mode."""
|
||||
if field_name == "use_double_brackets":
|
||||
# Change the template field type based on mode
|
||||
is_mustache = field_value is True
|
||||
if is_mustache:
|
||||
build_config["template"]["type"] = FieldTypes.MUSTACHE_PROMPT.value
|
||||
else:
|
||||
build_config["template"]["type"] = FieldTypes.PROMPT.value
|
||||
|
||||
# Re-process the template to update variables when mode changes
|
||||
template_value = build_config.get("template", {}).get("value", "")
|
||||
if template_value:
|
||||
# Ensure custom_fields is properly initialized
|
||||
if "custom_fields" not in build_config:
|
||||
build_config["custom_fields"] = {}
|
||||
|
||||
# Clean up fields from the OLD mode before processing with NEW mode
|
||||
# This ensures we don't keep fields with wrong syntax even if validation fails
|
||||
old_custom_fields = build_config["custom_fields"].get("template", [])
|
||||
for old_field in list(old_custom_fields):
|
||||
# Remove the field from custom_fields and template
|
||||
if old_field in old_custom_fields:
|
||||
old_custom_fields.remove(old_field)
|
||||
build_config.pop(old_field, None)
|
||||
|
||||
# Try to process template with new mode to add new variables
|
||||
# If validation fails, at least we cleaned up old fields
|
||||
try:
|
||||
# Validate mustache templates for security
|
||||
if is_mustache:
|
||||
validate_mustache_template(template_value)
|
||||
|
||||
# Re-process template with new mode to add new variables
|
||||
_ = process_prompt_template(
|
||||
template=template_value,
|
||||
name="template",
|
||||
custom_fields=build_config["custom_fields"],
|
||||
frontend_node_template=build_config,
|
||||
is_mustache=is_mustache,
|
||||
)
|
||||
except ValueError as e:
|
||||
# If validation fails, we still updated the mode and cleaned old fields
|
||||
# User will see error when they try to save
|
||||
logger.debug(f"Template validation failed during mode switch: {e}")
|
||||
return build_config
|
||||
|
||||
async def build_prompt(self) -> Message:
|
||||
prompt = Message.from_template(**self._attributes)
|
||||
use_double_brackets = self.use_double_brackets if hasattr(self, "use_double_brackets") else False
|
||||
template_format = "mustache" if use_double_brackets else "f-string"
|
||||
prompt = await Message.from_template_and_variables(template_format=template_format, **self._attributes)
|
||||
self.status = prompt.text
|
||||
return prompt
|
||||
|
||||
def _update_template(self, frontend_node: dict):
|
||||
prompt_template = frontend_node["template"]["template"]["value"]
|
||||
custom_fields = frontend_node["custom_fields"]
|
||||
frontend_node_template = frontend_node["template"]
|
||||
_ = process_prompt_template(
|
||||
template=prompt_template,
|
||||
name="template",
|
||||
custom_fields=custom_fields,
|
||||
frontend_node_template=frontend_node_template,
|
||||
)
|
||||
use_double_brackets = frontend_node["template"].get("use_double_brackets", {}).get("value", False)
|
||||
is_mustache = use_double_brackets is True
|
||||
|
||||
try:
|
||||
# Validate mustache templates for security
|
||||
if is_mustache:
|
||||
validate_mustache_template(prompt_template)
|
||||
|
||||
custom_fields = frontend_node["custom_fields"]
|
||||
frontend_node_template = frontend_node["template"]
|
||||
_ = process_prompt_template(
|
||||
template=prompt_template,
|
||||
name="template",
|
||||
custom_fields=custom_fields,
|
||||
frontend_node_template=frontend_node_template,
|
||||
is_mustache=is_mustache,
|
||||
)
|
||||
except ValueError as e:
|
||||
# If validation fails, don't add variables but allow component to be created
|
||||
logger.debug(f"Template validation failed in _update_template: {e}")
|
||||
return frontend_node
|
||||
|
||||
async def update_frontend_node(self, new_frontend_node: dict, current_frontend_node: dict):
|
||||
"""This function is called after the code validation is done."""
|
||||
frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)
|
||||
template = frontend_node["template"]["template"]["value"]
|
||||
# Kept it duplicated for backwards compatibility
|
||||
_ = process_prompt_template(
|
||||
template=template,
|
||||
name="template",
|
||||
custom_fields=frontend_node["custom_fields"],
|
||||
frontend_node_template=frontend_node["template"],
|
||||
)
|
||||
use_double_brackets = frontend_node["template"].get("use_double_brackets", {}).get("value", False)
|
||||
is_mustache = use_double_brackets is True
|
||||
|
||||
try:
|
||||
# Validate mustache templates for security
|
||||
if is_mustache:
|
||||
validate_mustache_template(template)
|
||||
|
||||
# Kept it duplicated for backwards compatibility
|
||||
_ = process_prompt_template(
|
||||
template=template,
|
||||
name="template",
|
||||
custom_fields=frontend_node["custom_fields"],
|
||||
frontend_node_template=frontend_node["template"],
|
||||
is_mustache=is_mustache,
|
||||
)
|
||||
except ValueError as e:
|
||||
# If validation fails, don't add variables but allow component to be updated
|
||||
logger.debug(f"Template validation failed in update_frontend_node: {e}")
|
||||
# Now that template is updated, we need to grab any values that were set in the current_frontend_node
|
||||
# and update the frontend_node with those values
|
||||
update_template_values(new_template=frontend_node, previous_template=current_frontend_node["template"])
|
||||
|
||||
@ -28,6 +28,7 @@ class FieldTypes(str, Enum):
|
||||
AUTH = "auth"
|
||||
FILE = "file"
|
||||
PROMPT = "prompt"
|
||||
MUSTACHE_PROMPT = "mustache"
|
||||
CODE = "code"
|
||||
OTHER = "other"
|
||||
TABLE = "table"
|
||||
|
||||
@ -120,6 +120,10 @@ class PromptInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeM
|
||||
field_type: SerializableFieldTypes = FieldTypes.PROMPT
|
||||
|
||||
|
||||
class MustachePromptInput(PromptInput):
|
||||
field_type: SerializableFieldTypes = FieldTypes.MUSTACHE_PROMPT
|
||||
|
||||
|
||||
class CodeInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeMixin):
|
||||
field_type: SerializableFieldTypes = FieldTypes.CODE
|
||||
|
||||
@ -807,6 +811,7 @@ InputTypes: TypeAlias = (
|
||||
| NestedDictInput
|
||||
| ToolsInput
|
||||
| PromptInput
|
||||
| MustachePromptInput
|
||||
| CodeInput
|
||||
| SecretStrInput
|
||||
| StrInput
|
||||
|
||||
@ -27,6 +27,7 @@ from lfx.schema.properties import Properties, Source
|
||||
from lfx.schema.validators import timestamp_to_str, timestamp_to_str_validator
|
||||
from lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_NAME_USER, MESSAGE_SENDER_USER
|
||||
from lfx.utils.image import create_image_content_dict
|
||||
from lfx.utils.mustache_security import safe_mustache_render
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lfx.schema.dataframe import DataFrame
|
||||
@ -287,28 +288,35 @@ class Message(Data):
|
||||
prompt_json = prompt.to_json()
|
||||
return cls(prompt=prompt_json)
|
||||
|
||||
def format_text(self):
|
||||
def format_text(self, template_format="f-string"):
|
||||
if template_format == "mustache":
|
||||
# Use our secure mustache renderer
|
||||
variables_with_str_values = dict_values_to_string(self.variables)
|
||||
formatted_prompt = safe_mustache_render(self.template, variables_with_str_values)
|
||||
self.text = formatted_prompt
|
||||
return formatted_prompt
|
||||
# Use langchain's template for other formats
|
||||
from langchain_core.prompts.prompt import PromptTemplate
|
||||
|
||||
prompt_template = PromptTemplate.from_template(self.template)
|
||||
prompt_template = PromptTemplate.from_template(self.template, template_format=template_format)
|
||||
variables_with_str_values = dict_values_to_string(self.variables)
|
||||
formatted_prompt = prompt_template.format(**variables_with_str_values)
|
||||
self.text = formatted_prompt
|
||||
return formatted_prompt
|
||||
|
||||
@classmethod
|
||||
async def from_template_and_variables(cls, template: str, **variables):
|
||||
async def from_template_and_variables(cls, template: str, template_format: str = "f-string", **variables):
|
||||
# This method has to be async for backwards compatibility with versions
|
||||
# >1.0.15, <1.1
|
||||
return cls.from_template(template, **variables)
|
||||
return cls.from_template(template, template_format=template_format, **variables)
|
||||
|
||||
# Define a sync version for backwards compatibility with versions >1.0.15, <1.1
|
||||
@classmethod
|
||||
def from_template(cls, template: str, **variables):
|
||||
def from_template(cls, template: str, template_format: str = "f-string", **variables):
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
instance = cls(template=template, variables=variables)
|
||||
text = instance.format_text()
|
||||
text = instance.format_text(template_format=template_format)
|
||||
message = HumanMessage(content=text)
|
||||
contents = []
|
||||
for value in variables.values():
|
||||
|
||||
0
src/lfx/src/lfx/tests/unit/components/__init__.py
Normal file
0
src/lfx/src/lfx/tests/unit/components/__init__.py
Normal file
@ -71,6 +71,7 @@ DIRECT_TYPES = [
|
||||
"float",
|
||||
"Any",
|
||||
"prompt",
|
||||
"mustache",
|
||||
"code",
|
||||
"NestedDict",
|
||||
"table",
|
||||
|
||||
79
src/lfx/src/lfx/utils/mustache_security.py
Normal file
79
src/lfx/src/lfx/utils/mustache_security.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Security utilities for mustache template processing."""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
# Regex pattern for simple variables only - same as frontend
|
||||
SIMPLE_VARIABLE_PATTERN = re.compile(r"\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}")
|
||||
|
||||
# Patterns for complex mustache syntax that we want to block
|
||||
DANGEROUS_PATTERNS = [
|
||||
re.compile(r"\{\{\{"), # Triple braces (unescaped HTML in Mustache)
|
||||
re.compile(r"\{\{#"), # Conditionals/sections start
|
||||
re.compile(r"\{\{/"), # Conditionals/sections end
|
||||
re.compile(r"\{\{\^"), # Inverted sections
|
||||
re.compile(r"\{\{&"), # Unescaped variables
|
||||
re.compile(r"\{\{>"), # Partials
|
||||
re.compile(r"\{\{!"), # Comments
|
||||
re.compile(r"\{\{\."), # Current context
|
||||
]
|
||||
|
||||
|
||||
def validate_mustache_template(template: str) -> None:
|
||||
"""Validate that a mustache template only contains simple variable substitutions.
|
||||
|
||||
Raises ValueError if complex mustache syntax is detected.
|
||||
"""
|
||||
if not template:
|
||||
return
|
||||
|
||||
# Check for dangerous patterns
|
||||
for pattern in DANGEROUS_PATTERNS:
|
||||
if pattern.search(template):
|
||||
msg = (
|
||||
"Complex mustache syntax is not allowed. Only simple variable substitution "
|
||||
"like {{variable}} is permitted."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check that all {{ }} patterns are simple variables
|
||||
all_mustache_patterns = re.findall(r"\{\{[^}]*\}\}", template)
|
||||
for pattern in all_mustache_patterns:
|
||||
if not SIMPLE_VARIABLE_PATTERN.match(pattern):
|
||||
msg = f"Invalid mustache variable: {pattern}. Only simple variable names like {{{{variable}}}} are allowed."
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def safe_mustache_render(template: str, variables: dict[str, Any]) -> str:
|
||||
"""Safely render a mustache template with only simple variable substitution.
|
||||
|
||||
This function performs a single-pass replacement of all {{variable}} patterns.
|
||||
Variable values that themselves contain mustache-like patterns (e.g., "{{other}}")
|
||||
will NOT be processed - they are treated as literal strings. This prevents
|
||||
injection attacks where user-controlled values could introduce new template variables.
|
||||
|
||||
Args:
|
||||
template: The mustache template string
|
||||
variables: Dictionary of variables to substitute
|
||||
|
||||
Returns:
|
||||
The rendered template
|
||||
|
||||
Raises:
|
||||
ValueError: If template contains complex mustache syntax
|
||||
"""
|
||||
# Validate template first
|
||||
validate_mustache_template(template)
|
||||
|
||||
# Simple replacement - find all simple variables and replace them
|
||||
def replace_variable(match):
|
||||
var_name = match.group(1)
|
||||
|
||||
# Get the variable value directly (no dot notation support)
|
||||
value = variables.get(var_name, "")
|
||||
|
||||
# Convert to string
|
||||
return str(value) if value is not None else ""
|
||||
|
||||
# Replace all simple variables
|
||||
return SIMPLE_VARIABLE_PATTERN.sub(replace_variable, template)
|
||||
0
src/lfx/tests/unit/components/__init__.py
Normal file
0
src/lfx/tests/unit/components/__init__.py
Normal file
319
src/lfx/tests/unit/components/test_prompt_component.py
Normal file
319
src/lfx/tests/unit/components/test_prompt_component.py
Normal file
@ -0,0 +1,319 @@
|
||||
"""Tests for PromptComponent with f-string and double brackets syntax."""
|
||||
|
||||
from lfx.components.models_and_agents.prompt import PromptComponent
|
||||
|
||||
|
||||
class TestPromptComponent:
|
||||
"""Test the PromptComponent."""
|
||||
|
||||
def test_update_template_single_variable(self):
|
||||
"""Test template update with a single variable."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {name}!"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
assert "name" in result["template"]
|
||||
|
||||
def test_update_template_multiple_variables(self):
|
||||
"""Test template with multiple variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "{greeting} {name}!"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert "greeting" in result["custom_fields"]["template"]
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
|
||||
def test_update_template_duplicate_variables(self):
|
||||
"""Test template with duplicate variables only creates one field."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {name}! How are you {name}?"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert result["custom_fields"]["template"].count("name") == 1
|
||||
assert "name" in result["template"]
|
||||
|
||||
def test_update_template_no_variables(self):
|
||||
"""Test template with no variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello World!"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert len(result["custom_fields"].get("template", [])) == 0
|
||||
|
||||
def test_update_template_escaped_braces(self):
|
||||
"""Test template with escaped braces doesn't create variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Result: {{not_a_var}} but {real_var} works"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
# Only {real_var} should be extracted
|
||||
assert "real_var" in result["custom_fields"]["template"]
|
||||
# {{not_a_var}} should NOT be extracted in f-string mode
|
||||
assert "not_a_var" not in result["custom_fields"]["template"]
|
||||
|
||||
async def test_build_prompt_basic(self):
|
||||
"""Test building a basic prompt."""
|
||||
component = PromptComponent()
|
||||
component._attributes = {
|
||||
"template": "Hello {name}!",
|
||||
"name": "World",
|
||||
}
|
||||
|
||||
result = await component.build_prompt()
|
||||
|
||||
assert result.text == "Hello World!"
|
||||
assert result.template == "Hello {name}!"
|
||||
|
||||
async def test_build_prompt_multiple_variables(self):
|
||||
"""Test building prompt with multiple variables."""
|
||||
component = PromptComponent()
|
||||
component._attributes = {
|
||||
"template": "{greeting} {name}! You are {age} years old.",
|
||||
"greeting": "Hello",
|
||||
"name": "Alice",
|
||||
"age": "25",
|
||||
}
|
||||
|
||||
result = await component.build_prompt()
|
||||
|
||||
assert result.text == "Hello Alice! You are 25 years old."
|
||||
|
||||
async def test_update_frontend_node(self):
|
||||
"""Test update_frontend_node processes template correctly."""
|
||||
component = PromptComponent()
|
||||
|
||||
new_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {name}!"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
current_node = {
|
||||
"template": {"template": {"value": ""}},
|
||||
}
|
||||
|
||||
result = await component.update_frontend_node(new_node, current_node)
|
||||
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
assert "name" in result["template"]
|
||||
|
||||
async def test_update_frontend_node_creates_variable_fields(self):
|
||||
"""Test that update_frontend_node creates fields for template variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
new_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {name} and {greeting}!"},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
current_node = {
|
||||
"template": {"template": {"value": ""}},
|
||||
}
|
||||
|
||||
result = await component.update_frontend_node(new_node, current_node)
|
||||
|
||||
# Both variables should be in custom_fields
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
assert "greeting" in result["custom_fields"]["template"]
|
||||
|
||||
|
||||
class TestPromptComponentDoubleBrackets:
|
||||
"""Test the PromptComponent with double brackets (mustache) syntax."""
|
||||
|
||||
def test_update_template_double_brackets_single_variable(self):
|
||||
"""Test template update with a single double-bracket variable."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {{name}}!"},
|
||||
"use_double_brackets": {"value": True},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
assert "name" in result["template"]
|
||||
|
||||
def test_update_template_double_brackets_multiple_variables(self):
|
||||
"""Test template with multiple double-bracket variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "{{greeting}} {{name}}!"},
|
||||
"use_double_brackets": {"value": True},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert "greeting" in result["custom_fields"]["template"]
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
|
||||
def test_update_template_double_brackets_no_variables(self):
|
||||
"""Test double-bracket template with no variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello World!"},
|
||||
"use_double_brackets": {"value": True},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
assert len(result["custom_fields"].get("template", [])) == 0
|
||||
|
||||
def test_update_template_double_brackets_ignores_single_braces(self):
|
||||
"""Test that double-bracket mode ignores single-brace variables."""
|
||||
component = PromptComponent()
|
||||
|
||||
frontend_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {single} and {{double}}!"},
|
||||
"use_double_brackets": {"value": True},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
result = component._update_template(frontend_node)
|
||||
|
||||
# Only {{double}} should be extracted in double-bracket mode
|
||||
assert "double" in result["custom_fields"]["template"]
|
||||
# {single} should NOT be extracted
|
||||
assert "single" not in result["custom_fields"].get("template", [])
|
||||
|
||||
async def test_build_prompt_double_brackets_basic(self):
|
||||
"""Test building a basic prompt with double brackets."""
|
||||
component = PromptComponent()
|
||||
component.use_double_brackets = True
|
||||
component._attributes = {
|
||||
"template": "Hello {{name}}!",
|
||||
"name": "World",
|
||||
"use_double_brackets": True,
|
||||
}
|
||||
|
||||
result = await component.build_prompt()
|
||||
|
||||
assert result.text == "Hello World!"
|
||||
|
||||
async def test_build_prompt_double_brackets_multiple_variables(self):
|
||||
"""Test building prompt with multiple double-bracket variables."""
|
||||
component = PromptComponent()
|
||||
component.use_double_brackets = True
|
||||
component._attributes = {
|
||||
"template": "{{greeting}} {{name}}! You are {{age}} years old.",
|
||||
"greeting": "Hello",
|
||||
"name": "Alice",
|
||||
"age": "25",
|
||||
"use_double_brackets": True,
|
||||
}
|
||||
|
||||
result = await component.build_prompt()
|
||||
|
||||
assert result.text == "Hello Alice! You are 25 years old."
|
||||
|
||||
async def test_build_prompt_default_is_single_brackets(self):
|
||||
"""Test that default mode uses single brackets (f-string)."""
|
||||
component = PromptComponent()
|
||||
# Don't set use_double_brackets - should default to False
|
||||
component._attributes = {
|
||||
"template": "Hello {name}!",
|
||||
"name": "World",
|
||||
}
|
||||
|
||||
result = await component.build_prompt()
|
||||
|
||||
assert result.text == "Hello World!"
|
||||
|
||||
async def test_update_frontend_node_double_brackets(self):
|
||||
"""Test update_frontend_node processes double-bracket template correctly."""
|
||||
component = PromptComponent()
|
||||
|
||||
new_node = {
|
||||
"template": {
|
||||
"template": {"value": "Hello {{name}}!"},
|
||||
"use_double_brackets": {"value": True},
|
||||
},
|
||||
"custom_fields": {},
|
||||
}
|
||||
|
||||
current_node = {
|
||||
"template": {"template": {"value": ""}},
|
||||
}
|
||||
|
||||
result = await component.update_frontend_node(new_node, current_node)
|
||||
|
||||
assert "name" in result["custom_fields"]["template"]
|
||||
assert "name" in result["template"]
|
||||
|
||||
def test_update_build_config_switches_to_mustache(self):
|
||||
"""Test that update_build_config switches field type when enabling double brackets."""
|
||||
component = PromptComponent()
|
||||
|
||||
build_config = {
|
||||
"template": {"type": "prompt", "value": "Hello {{name}}!"},
|
||||
"custom_fields": {"template": []},
|
||||
}
|
||||
|
||||
result = component.update_build_config(build_config, field_value=True, field_name="use_double_brackets")
|
||||
|
||||
assert result["template"]["type"] == "mustache"
|
||||
|
||||
def test_update_build_config_switches_to_fstring(self):
|
||||
"""Test that update_build_config switches field type when disabling double brackets."""
|
||||
component = PromptComponent()
|
||||
|
||||
build_config = {
|
||||
"template": {"type": "MustachePrompt", "value": "Hello {name}!"},
|
||||
"custom_fields": {"template": []},
|
||||
}
|
||||
|
||||
result = component.update_build_config(build_config, field_value=False, field_name="use_double_brackets")
|
||||
|
||||
assert result["template"]["type"] == "prompt"
|
||||
172
src/lfx/tests/unit/schema/test_mustache_template_processing.py
Normal file
172
src/lfx/tests/unit/schema/test_mustache_template_processing.py
Normal file
@ -0,0 +1,172 @@
|
||||
"""Tests for mustache template processing in the Message class.
|
||||
|
||||
Note: Our mustache implementation only supports simple variable substitution
|
||||
for security reasons. Complex features like conditionals, loops, and sections
|
||||
are not supported.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from lfx.schema.message import Message
|
||||
from lfx.utils.mustache_security import validate_mustache_template
|
||||
|
||||
|
||||
class TestMustacheTemplateProcessing:
|
||||
"""Test mustache template processing in the Message class."""
|
||||
|
||||
def test_format_text_mustache_basic(self):
|
||||
"""Test basic mustache template formatting."""
|
||||
message = Message(template="Hello {{name}}!", variables={"name": "World"})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Hello World!"
|
||||
assert message.text == "Hello World!"
|
||||
|
||||
def test_format_text_mustache_multiple_variables(self):
|
||||
"""Test mustache template with multiple variables."""
|
||||
message = Message(
|
||||
template="Hello {{name}}! You are {{age}} years old.", variables={"name": "Alice", "age": "25"}
|
||||
)
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Hello Alice! You are 25 years old."
|
||||
|
||||
def test_format_text_mustache_missing_variable(self):
|
||||
"""Test mustache template with missing variable."""
|
||||
message = Message(template="Hello {{name}}! You are {{age}} years old.", variables={"name": "Bob"})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
# Missing variables should render as empty strings
|
||||
assert result == "Hello Bob! You are years old."
|
||||
|
||||
def test_format_text_mustache_no_variables(self):
|
||||
"""Test mustache template with no variables."""
|
||||
message = Message(template="Hello World!", variables={})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Hello World!"
|
||||
|
||||
def test_format_text_mustache_empty_template(self):
|
||||
"""Test mustache template with empty template."""
|
||||
message = Message(template="", variables={"name": "Test"})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == ""
|
||||
|
||||
def test_format_text_mustache_with_numeric_values(self):
|
||||
"""Test mustache template with numeric values."""
|
||||
message = Message(
|
||||
template="Price: ${{price}}, Quantity: {{quantity}}", variables={"price": 19.99, "quantity": 3}
|
||||
)
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Price: $19.99, Quantity: 3"
|
||||
|
||||
def test_format_text_mustache_with_newlines(self):
|
||||
"""Test mustache template with newlines."""
|
||||
message = Message(
|
||||
template="Line 1: {{line1}}\nLine 2: {{line2}}", variables={"line1": "First", "line2": "Second"}
|
||||
)
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Line 1: First\nLine 2: Second"
|
||||
|
||||
def test_format_text_mustache_with_empty_string_variable(self):
|
||||
"""Test mustache template with empty string variable."""
|
||||
message = Message(template="Hello {{name}}!", variables={"name": ""})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Hello !"
|
||||
|
||||
def test_format_text_mustache_with_none_variable(self):
|
||||
"""Test mustache template with None variable."""
|
||||
message = Message(template="Hello {{name}}!", variables={"name": None})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
# None should render as empty string
|
||||
assert result == "Hello !"
|
||||
|
||||
async def test_from_template_and_variables_mustache(self):
|
||||
"""Test from_template_and_variables with mustache format."""
|
||||
message = await Message.from_template_and_variables(
|
||||
template="Hello {{name}}!", template_format="mustache", name="World"
|
||||
)
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert message.text == "Hello World!"
|
||||
assert message.template == "Hello {{name}}!"
|
||||
assert message.variables == {"name": "World"}
|
||||
|
||||
async def test_from_template_and_variables_mustache_no_variables(self):
|
||||
"""Test from_template_and_variables with no variables."""
|
||||
message = await Message.from_template_and_variables(template="Static message", template_format="mustache")
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert message.text == "Static message"
|
||||
assert message.variables == {}
|
||||
|
||||
def test_format_text_mustache_preserves_original_variables(self):
|
||||
"""Test that format_text doesn't modify the original variables."""
|
||||
original_variables = {"name": "Test", "age": 25}
|
||||
message = Message(template="Hello {{name}}, age {{age}}!", variables=original_variables.copy())
|
||||
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Hello Test, age 25!"
|
||||
assert message.variables == original_variables
|
||||
|
||||
def test_format_text_mustache_with_zero_values(self):
|
||||
"""Test mustache template with zero values."""
|
||||
message = Message(template="Count: {{count}}, Price: {{price}}", variables={"count": 0, "price": 0.0})
|
||||
result = message.format_text(template_format="mustache")
|
||||
|
||||
assert result == "Count: 0, Price: 0.0"
|
||||
|
||||
def test_mustache_security_rejects_conditionals(self):
|
||||
"""Test that conditional syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{#show}}Hello{{/show}}")
|
||||
|
||||
def test_mustache_security_rejects_inverted_sections(self):
|
||||
"""Test that inverted section syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{^items}}No items{{/items}}")
|
||||
|
||||
def test_mustache_security_rejects_loops(self):
|
||||
"""Test that loop syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{#items}}{{.}}{{/items}}")
|
||||
|
||||
def test_mustache_security_rejects_unescaped_variables(self):
|
||||
"""Test that unescaped variable syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{&variable}}")
|
||||
|
||||
def test_mustache_security_rejects_partials(self):
|
||||
"""Test that partial syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{>partial}}")
|
||||
|
||||
def test_mustache_security_rejects_comments(self):
|
||||
"""Test that comment syntax is rejected for security."""
|
||||
with pytest.raises(ValueError, match="Complex mustache syntax is not allowed"):
|
||||
validate_mustache_template("{{!comment}}")
|
||||
|
||||
def test_mustache_security_allows_simple_variables(self):
|
||||
"""Test that simple variables are allowed."""
|
||||
# Should not raise
|
||||
validate_mustache_template("Hello {{name}}!")
|
||||
validate_mustache_template("{{var1}} and {{var2}}")
|
||||
|
||||
def test_mustache_security_rejects_dot_notation(self):
|
||||
"""Test that dot notation is NOT allowed."""
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{user.name}}")
|
||||
with pytest.raises(ValueError, match="Invalid mustache variable"):
|
||||
validate_mustache_template("{{company.ceo.name}}")
|
||||
|
||||
def test_format_text_defaults_to_f_string(self):
|
||||
"""Test that format_text defaults to f-string format."""
|
||||
message = Message(template="Hello {name}!", variables={"name": "World"})
|
||||
result = message.format_text() # No template_format specified
|
||||
|
||||
assert result == "Hello World!"
|
||||
Reference in New Issue
Block a user