diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/TemplateMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/TemplateMode.tsx index 2213b4f3bbe..42681e0ab15 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/TemplateMode.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/TemplateMode.tsx @@ -26,6 +26,7 @@ import { MarkdownPreview } from "../controls/MarkdownPreview"; import { transformExpressionToMarkdown } from "../utils/transformToMarkdown"; import { useFormContext } from "../../../../context/form"; import { ErrorBanner } from "@wso2/ui-toolkit"; +import { RawTemplateEditorConfig } from "../../MultiModeExpressionEditor/Configurations"; const ExpressionContainer = styled.div` width: 100%; @@ -150,6 +151,7 @@ export const TemplateMode: React.FC = ({ onEditorViewReady={setEditorView} toolbarRef={toolbarRef} enableListContinuation={true} + configuration={new RawTemplateEditorConfig()} /> )} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx index 866a72b2e7e..d0eb01de2b2 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx @@ -373,9 +373,9 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { const inputModeRef = useRef(inputMode); const [isExpressionEditorHovered, setIsExpressionEditorHovered] = useState(false); const [showModeSwitchWarning, setShowModeSwitchWarning] = useState(false); - const [targetInputMode, setTargetInputMode] = useState(null); const [formDiagnostics, setFormDiagnostics] = useState(field.diagnostics); const [isExpandedModalOpen, setIsExpandedModalOpen] = useState(false); + const targetInputModeRef = useRef(null); // Update formDiagnostics when field.diagnostics changes useEffect(() => { @@ -441,34 +441,22 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { } let newInputMode = getInputModeFromTypes(field.valueTypeConstraint) - if (isModeSwitcherRestricted()) { + if (!newInputMode) { setInputMode(InputMode.EXP); return; } - if (!newInputMode) { + if (isModeSwitcherRestricted()) { setInputMode(InputMode.EXP); return; } - if (newInputMode === InputMode.TEXT - && typeof initialFieldValue.current === 'string' - && initialFieldValue.current.trim() !== '' - && !(initialFieldValue.current.trim().startsWith("\"") - && initialFieldValue.current.trim().endsWith("\"") - ) - ) { - setInputMode(InputMode.EXP) - } else if (newInputMode === InputMode.TEMPLATE) { - if (sanitizedExpression && rawExpression) { - const sanitized = sanitizedExpression(initialFieldValue.current as string); - if (sanitized !== initialFieldValue.current || !initialFieldValue.current || initialFieldValue.current.trim() === '') { - setInputMode(InputMode.TEMPLATE); - } else { + switch (newInputMode) { + case (InputMode.BOOLEAN): + if (!isExpToBooleanSafe(field?.value as string)) { setInputMode(InputMode.EXP); + return; } - } - } else { - setInputMode(newInputMode); } + setInputMode(newInputMode) }, [field?.valueTypeConstraint, recordTypeField]); const handleFocus = async (controllerOnChange?: (value: string) => void) => { @@ -548,63 +536,37 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { return await extractArgsFromFunction(value, getPropertyFromFormField(field), cursorPosition); }; + const isExpToBooleanSafe = (expValue: string) => { + return ["true", "false"].includes(expValue.trim().toLowerCase()) + } + const handleModeChange = (value: InputMode) => { const raw = watch(key); const currentValue = typeof raw === "string" ? raw.trim() : ""; - - // Warn when switching from EXP to TEXT if value doesn't have quotes - if ( - inputMode === InputMode.EXP - && value === InputMode.TEXT - && (!currentValue.trim().startsWith("\"") || !currentValue.trim().endsWith("\"")) - && currentValue.trim() !== '' - ) { - setTargetInputMode(value); - setShowModeSwitchWarning(true); - return; - } - - // Warn when switching from EXP to TEMPLATE if sanitization would hide parts of the expression - if ( - inputMode === InputMode.EXP - && value === InputMode.TEMPLATE - && sanitizedExpression - && currentValue - && currentValue.trim() !== '' - ) { - setTargetInputMode(value); - if (currentValue === sanitizedExpression(currentValue)) { - setShowModeSwitchWarning(true); - } else { - setInputMode(value); - } + if (inputMode !== InputMode.EXP) { + setInputMode(value); return; } - - // Auto-add quotes when switching from TEXT to EXP if not present - if (inputMode === InputMode.TEXT && value === InputMode.EXP) { - if (currentValue && typeof currentValue === 'string' && - !currentValue.startsWith('"') && !currentValue.endsWith('"')) { - setValue(key, `"${currentValue}"`); - } + const primaryInputMode = getInputModeFromTypes(field.valueTypeConstraint); + switch (primaryInputMode) { + case (InputMode.BOOLEAN): + if (!isExpToBooleanSafe(currentValue)) { + targetInputModeRef.current = value; + setShowModeSwitchWarning(true) + return; + } + break; } - setInputMode(value); }; const handleModeSwitchWarningContinue = () => { - if (targetInputMode !== null) { - setInputMode(targetInputMode); - setTargetInputMode(null); - if (targetInputMode === InputMode.TEMPLATE && inputMode === InputMode.EXP && rawExpression) { - setValue(key, rawExpression("")); - } - } + setInputMode(targetInputModeRef.current); setShowModeSwitchWarning(false); }; const handleModeSwitchWarningCancel = () => { - setTargetInputMode(null); + targetInputModeRef.current = null; setShowModeSwitchWarning(false); }; @@ -697,15 +659,15 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { render={({ field: { name, value, onChange }, fieldState: { error } }) => (
{ @@ -714,13 +676,12 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { setFormDiagnostics([]); // Use ref to get current mode (not stale closure value) const currentMode = inputModeRef.current; - const rawValue = currentMode === InputMode.TEMPLATE && rawExpression ? rawExpression(updatedValue) : updatedValue; - onChange(rawValue); + onChange(updatedValue); if (getExpressionEditorDiagnostics && (currentMode === InputMode.EXP || currentMode === InputMode.TEMPLATE)) { getExpressionEditorDiagnostics( - (required ?? !field.optional) || rawValue !== '', - rawValue, + (required ?? !field.optional) || updatedValue !== '', + updatedValue, key, getPropertyFromFormField(field) ); @@ -729,18 +690,18 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { // Check if the current character is a trigger character const triggerCharacter = updatedCursorPosition > 0 - ? triggerCharacters.find((char) => rawValue[updatedCursorPosition - 1] === char) + ? triggerCharacters.find((char) => updatedValue[updatedCursorPosition - 1] === char) : undefined; if (triggerCharacter) { await retrieveCompletions( - rawValue, + updatedValue, getPropertyFromFormField(field), updatedCursorPosition, triggerCharacter ); } else { await retrieveCompletions( - rawValue, + updatedValue, getPropertyFromFormField(field), updatedCursorPosition ); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx index 53d79a469e6..40901dcc659 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx @@ -27,13 +27,19 @@ import { } from '@wso2/ui-toolkit'; import { S } from './ExpressionEditor'; import TextModeEditor from './MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor'; -import { InputMode } from './MultiModeExpressionEditor/ChipExpressionEditor/types'; +import { InputMode, TokenType } from './MultiModeExpressionEditor/ChipExpressionEditor/types'; import { LineRange } from '@wso2/ballerina-core/lib/interfaces/common'; -import { HelperpaneOnChangeOptions } from '../Form/types'; +import { FormField, HelperpaneOnChangeOptions } from '../Form/types'; import { ChipExpressionEditorComponent } from './MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor'; +import RecordConfigPreviewEditor from './MultiModeExpressionEditor/RecordConfigPreviewEditor/RecordConfigPreviewEditor'; +import { RawTemplateEditorConfig, StringTemplateEditorConfig, PrimaryModeChipExpressionEditorConfig } from './MultiModeExpressionEditor/Configurations'; +import NumberExpressionEditor from './MultiModeExpressionEditor/NumberExpressionEditor/NumberEditor'; +import BooleanEditor from './MultiModeExpressionEditor/BooleanEditor/BooleanEditor'; export interface ExpressionField { + field: FormField; inputMode: InputMode; + primaryMode: InputMode; name: string; value: string; fileName?: string; @@ -96,6 +102,8 @@ const EditorRibbon = ({ onClick }: { onClick: () => void }) => { export const ExpressionField: React.FC = ({ inputMode, + field, + primaryMode, name, value, completions, @@ -127,9 +135,18 @@ export const ExpressionField: React.FC = ({ onOpenExpandedMode, isInExpandedMode }) => { - if (inputMode === InputMode.TEXT || inputMode === InputMode.RECORD) { + if (inputMode === InputMode.BOOLEAN) { return ( - + ); + } + if (inputMode === InputMode.RECORD) { + return ( + = ({ onOpenExpandedMode={onOpenExpandedMode} isInExpandedMode={isInExpandedMode} /> + ); + } + if (inputMode === InputMode.TEXT) { + return ( + + + ); + } + if (inputMode === InputMode.TEMPLATE) { + return ( + + + ); + } + if (inputMode === InputMode.NUMBER) { + return ( + ); } @@ -166,6 +244,7 @@ export const ExpressionField: React.FC = ({ onOpenExpandedMode={onOpenExpandedMode} onRemove={onRemove} isInExpandedMode={isInExpandedMode} + configuration={new PrimaryModeChipExpressionEditorConfig(primaryMode)} /> ); }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/BooleanEditor/BooleanEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/BooleanEditor/BooleanEditor.tsx new file mode 100644 index 00000000000..a7ca838881f --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/BooleanEditor/BooleanEditor.tsx @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ChangeEvent } from "react"; +import { Dropdown } from "@wso2/ui-toolkit"; +import { FormField } from "../../../Form/types"; +import { OptionProps } from "@wso2/ballerina-core"; + +interface BooleanEditorProps { + value: string; + field: FormField; + onChange: (value: string, cursorPosition: number) => void; +} + +const dropdownItems: OptionProps[] = [ + { + id: "default-option", + content: "None Selected", + value: "" + }, + { + id: "1", + content: "True", + value: "true" + }, + { + id: "2", + content: "False", + value: "false" + } +] + +export const BooleanEditor: React.FC = ({ value, onChange, field }) => { + const handleChange = (e: ChangeEvent) => { + onChange(e.target.value, e.target.value.length) + } + + return ( + + ); +}; + +export default BooleanEditor; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionDefaultConfig.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionDefaultConfig.ts new file mode 100644 index 00000000000..327024a32ec --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionDefaultConfig.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import FXButton from "./components/FxButton"; +import { ParsedToken } from "./utils"; + +export abstract class ChipExpressionEditorDefaultConfiguration { + getHelperValue(value: string, token?: ParsedToken) { + return value; + } + getSerializationPrefix() { + return ""; + } + getSerializationSuffix() { + return ""; + } + serializeValue(value: string) { + return value + } + deserializeValue(value: string) { + return value; + } + showHelperPane() { + return true; + } + getPlugins() { + return []; + } + getAdornment() { + return (FXButton) + } +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts index eb454b683da..89924c84d97 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts @@ -38,8 +38,6 @@ export type TokenStream = number[]; export type TokensChangePayload = { tokens: TokenStream; - rawValue?: string; // Raw expression (e.g., `${var}`) - sanitizedValue?: string; // Sanitized expression (e.g., ${var}) }; export type CursorInfo = { @@ -226,23 +224,12 @@ export const tokenField = StateField.define({ for (let effect of tr.effects) { if (effect.is(tokensChangeEffect)) { const payload = effect.value; - const sanitizedDoc = tr.newDoc.toString(); - - // Parse tokens using the raw value if provided, otherwise use sanitized - const valueForParsing = payload.rawValue || sanitizedDoc; - tokens = getParsedExpressionTokens(payload.tokens, valueForParsing); - - // If we have both raw and sanitized values, map positions - if (payload.rawValue && payload.sanitizedValue) { - tokens = tokens.map(token => ({ - ...token, - start: mapRawToSanitized(token.start, payload.rawValue!, payload.sanitizedValue!), - end: mapRawToSanitized(token.end, payload.rawValue!, payload.sanitizedValue!) - })); - } + const currentValue = tr.newDoc.toString(); + + tokens = getParsedExpressionTokens(payload.tokens, currentValue); // Detect compounds once when tokens change - compounds = detectTokenPatterns(tokens, sanitizedDoc); + compounds = detectTokenPatterns(tokens, currentValue); return { tokens, compounds }; } @@ -616,3 +603,18 @@ export const buildHelperPaneKeymap = (getIsHelperPaneOpen: () => boolean, onClos } ]; }; + +export const isSelectionOnToken = (from: number, to: number, view: EditorView): ParsedToken => { + if (!view) return undefined; + const { tokens, compounds } = view.state.field(tokenField); + + const matchingCompound = compounds.find( + compound => compound.start === from && compound.end === to + ); + if (matchingCompound) return undefined; + + const matchingToken = tokens.find( + token => token.start === from && token.end === to + ); + return matchingToken; +}; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx index 504defb4e6e..400f5fbe248 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx @@ -35,21 +35,22 @@ import { CursorInfo, buildOnFocusOutListner, buildOnSelectionChange, - SyncDocValueWithPropValue + SyncDocValueWithPropValue, + isSelectionOnToken } from "../CodeUtils"; -import { mapSanitizedToRaw } from "../utils"; +import { TOKEN_START_CHAR_OFFSET_INDEX } from "../utils"; import { history } from "@codemirror/commands"; import { autocompletion } from "@codemirror/autocomplete"; import { FloatingButtonContainer, FloatingToggleButton, ChipEditorContainer } from "../styles"; import { HelperpaneOnChangeOptions } from "../../../../Form/types"; -import { CompletionItem, FnSignatureDocumentation, HelperPaneHeight } from "@wso2/ui-toolkit"; +import { CompletionItem, FnSignatureDocumentation, HELPER_PANE_WIDTH, HelperPaneHeight } from "@wso2/ui-toolkit"; import { CloseHelperIcon, ExpandIcon, MinimizeIcon, OpenHelperIcon } from "./FloatingButtonIcons"; import { LineRange } from "@wso2/ballerina-core"; -import FXButton from "./FxButton"; import { HelperPaneToggleButton } from "./HelperPaneToggleButton"; import { HelperPane } from "./HelperPane"; import { listContinuationKeymap } from "../../../ExpandedEditor/utils/templateUtils"; -import { HELPER_PANE_WIDTH } from "../constants"; +import { ChipExpressionEditorDefaultConfiguration } from "../ChipExpressionDefaultConfig"; +import { ChipExpressionEditorConfig } from "../../Configurations"; type HelperPaneState = { isOpen: boolean; @@ -91,11 +92,12 @@ export type ChipExpressionEditorComponentProps = { onEditorViewReady?: (view: EditorView) => void; toolbarRef?: React.RefObject; enableListContinuation?: boolean; + configuration?: ChipExpressionEditorDefaultConfiguration; } export const ChipExpressionEditorComponent = (props: ChipExpressionEditorComponentProps) => { + const { configuration = new ChipExpressionEditorConfig() } = props; const [helperPaneState, setHelperPaneState] = useState({ isOpen: false, top: 0, left: 0 }); - const editorRef = useRef(null); const helperPaneRef = useRef(null); const fieldContainerRef = useRef(null); @@ -115,7 +117,7 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone const handleChangeListner = buildOnChangeListner((newValue, cursor) => { completionsFetchScheduledRef.current = true; - props.onChange(newValue, cursor.position.to); + props.onChange(configuration.deserializeValue(newValue), cursor.position.to); const textBeforeCursor = newValue.slice(0, cursor.position.to); const lastNonSpaceChar = textBeforeCursor.trimEnd().slice(-1); const isTrigger = lastNonSpaceChar === '+' || lastNonSpaceChar === ':'; @@ -165,13 +167,15 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone }); const onHelperItemSelect = async (value: string, options: HelperpaneOnChangeOptions) => { - const newValue = value if (!viewRef.current) return; const view = viewRef.current; // Use saved selection if available, otherwise fall back to current selection const currentSelection = savedSelectionRef.current || view.state.selection.main; const { from, to } = options?.replaceFullText ? { from: 0, to: view.state.doc.length } : currentSelection; + + const selectionIsOnToken = isSelectionOnToken(currentSelection.from, currentSelection.to, view); + const newValue = selectionIsOnToken ? value : configuration.getHelperValue(value); let finalValue = newValue; let cursorPosition = from + newValue.length; @@ -188,7 +192,7 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone let functionDef = newValue; let prefix = ''; let suffix = ''; - + // Check if it's within a string template const stringTemplateMatch = newValue.match(/^(.*\$\{)([^}]+)(\}.*)$/); if (stringTemplateMatch) { @@ -196,12 +200,12 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone functionDef = stringTemplateMatch[2]; suffix = stringTemplateMatch[3]; } - + let cursorPositionForExtraction = from + prefix.length + functionDef.length - 1; if (functionDef.endsWith(')}')) { cursorPositionForExtraction -= 1; } - + const fnSignature = await props.extractArgsFromFunction(functionDef, cursorPositionForExtraction); if (fnSignature && fnSignature.args && fnSignature.args.length > 0) { @@ -279,6 +283,7 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone const startState = EditorState.create({ doc: props.value ?? "", extensions: [ + ...(configuration.getPlugins()), history(), keymap.of([ ...helperPaneKeymap, @@ -335,8 +340,14 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone useEffect(() => { if (props.value == null || !viewRef.current) return; + const serializedValue = configuration.serializeValue(props.value); + const deserializeValue = configuration.deserializeValue(props.value); + if (deserializeValue.trim() !== props.value.trim()) { + props.onChange(deserializeValue, deserializeValue.length); + return + } const updateEditorState = async () => { - const sanitizedValue = props.sanitizedExpression ? props.sanitizedExpression(props.value) : props.value; + const sanitizedValue = props.sanitizedExpression ? props.sanitizedExpression(serializedValue) : serializedValue; const currentDoc = viewRef.current!.state.doc.toString(); const isExternalUpdate = sanitizedValue !== currentDoc; @@ -344,15 +355,18 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone const startLine = props.targetLineRange?.startLine; const tokenStream = await expressionEditorRpcManager?.getExpressionTokens( - props.value, + deserializeValue, props.fileName, startLine !== undefined ? startLine : undefined ); + let prefixCorrectedTokenStream = tokenStream; + if (tokenStream && tokenStream.length >= 5) { + prefixCorrectedTokenStream = [...tokenStream]; + prefixCorrectedTokenStream[TOKEN_START_CHAR_OFFSET_INDEX] -= configuration.getSerializationPrefix().length; + } setIsTokenUpdateScheduled(false); - const effects = tokenStream ? [tokensChangeEffect.of({ - tokens: tokenStream, - rawValue: props.value, - sanitizedValue: sanitizedValue + const effects = prefixCorrectedTokenStream ? [tokensChangeEffect.of({ + tokens: prefixCorrectedTokenStream })] : []; const changes = isExternalUpdate ? { from: 0, to: viewRef.current!.state.doc.length, insert: sanitizedValue } @@ -433,10 +447,10 @@ export const ChipExpressionEditorComponent = (props: ChipExpressionEditorCompone ...props.sx, ...(props.isInExpandedMode ? { height: '100%' } : props.sx && 'height' in props.sx ? {} : { height: 'auto' }) }}> - {!props.isInExpandedMode && } -
{}})} +
- {helperPaneState.isOpen && + {helperPaneState.isOpen && configuration.showHelperPane() && void; }) => JSX.Element { + return () => null; + } + serializeValue(value: string): string { + const suffix = this.getSerializationSuffix(); + const prefix = this.getSerializationPrefix(); + if (value.trim().startsWith(prefix) && value.trim().endsWith(suffix)) { + return value.trim().slice(prefix.length, value.trim().length - suffix.length); + } + return value; + } + deserializeValue(value: string): string { + const suffix = this.getSerializationSuffix(); + const prefix = this.getSerializationPrefix(); + if (value.trim().startsWith(prefix) && value.trim().endsWith(suffix)) { + return value; + } + return `${prefix}${value}${suffix}`; + } +} + +export class RawTemplateEditorConfig extends ChipExpressionEditorDefaultConfiguration { + getHelperValue(value: string, token?: ParsedToken): string { + if (token?.type === TokenType.FUNCTION) return value; + return `\$\{${value}\}`; + } + getSerializationPrefix() { + return "`"; + } + getSerializationSuffix() { + return "`"; + } + getAdornment(): ({ onClick }: { onClick?: () => void; }) => JSX.Element { + return () => null; + } + serializeValue(value: string): string { + const suffix = this.getSerializationSuffix(); + const prefix = this.getSerializationPrefix(); + if (value.trim().startsWith(prefix) && value.trim().endsWith(suffix)) { + return value.trim().slice(prefix.length, value.trim().length - suffix.length); + } + return value; + } + deserializeValue(value: string): string { + const suffix = this.getSerializationSuffix(); + const prefix = this.getSerializationPrefix(); + if (value.trim().startsWith(prefix) && value.trim().endsWith(suffix)) { + return value; + } + return `${prefix}${value}${suffix}`; + } +} + +export class ChipExpressionEditorConfig extends ChipExpressionEditorDefaultConfiguration { + getHelperValue(value: string, token?: ParsedToken): string { + if (token?.type === TokenType.FUNCTION) return value; + return `\$\{${value}\}`; + } +} + +export class PrimaryModeChipExpressionEditorConfig extends ChipExpressionEditorDefaultConfiguration { + private readonly primaryMode: InputMode; + + constructor(primaryMode: InputMode) { + super(); + this.primaryMode = primaryMode; + } + + getHelperValue(value: string, token?: ParsedToken): string { + const isTextOrTemplateMode = this.primaryMode === InputMode.TEXT || this.primaryMode === InputMode.TEMPLATE; + if (isTextOrTemplateMode && (!token || token.type !== TokenType.FUNCTION)) { + return `\$\{${value}\}`; + } + return value; + } +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/NumberExpressionEditor/NumberEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/NumberExpressionEditor/NumberEditor.tsx new file mode 100644 index 00000000000..c22c8637d93 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/NumberExpressionEditor/NumberEditor.tsx @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from "react"; +import { getValueForTextModeEditor } from "../../utils"; +import { ChipExpressionEditorComponent, ChipExpressionEditorComponentProps } from "../ChipExpressionEditor/components/ChipExpressionEditor"; +import { ChipExpressionEditorDefaultConfiguration } from "../ChipExpressionEditor/ChipExpressionDefaultConfig"; +import { EditorState } from "@codemirror/state"; + +class NumberExpressionEditorConfig extends ChipExpressionEditorDefaultConfiguration { + showHelperPane() { + return false; + } + getAdornment() { + return () => null; + } + getPlugins() { + const numericOnly = EditorState.changeFilter.of(tr => { + let allow = true; + tr.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => { + const text = inserted.toString(); + + if (!/^[0-9.]*$/.test(text)) { + allow = false; + return; + } + + if ((text.match(/\./g) || []).length > 1) { + allow = false; + return; + } + }); + + return allow; + }); + + return [numericOnly]; + + } +} + +export const NumberExpressionEditor: React.FC = (props) => { + + return ( + + ); +}; + +export default NumberExpressionEditor diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/RecordConfigPreviewEditor/RecordConfigPreviewEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/RecordConfigPreviewEditor/RecordConfigPreviewEditor.tsx new file mode 100644 index 00000000000..a09e1b27173 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/RecordConfigPreviewEditor/RecordConfigPreviewEditor.tsx @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FormExpressionEditor } from "@wso2/ui-toolkit"; +import { ExpressionField } from "../../ExpressionField"; +import React from "react"; +import { getValueForTextModeEditor } from "../../utils"; +import styled from "@emotion/styled"; +import { FloatingToggleButton } from "../ChipExpressionEditor/components/FloatingToggleButton"; +import { ExpandIcon } from "../ChipExpressionEditor/components/FloatingButtonIcons"; + +const EditorContainer = styled.div` + width: 100%; + position: relative; + + #text-mode-editor-expand { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + + &:hover #text-mode-editor-expand { + opacity: 1; + } +`; + +type RecordConfigPreviewEditorProps = Pick; + +export const RecordConfigPreviewEditor: React.FC = ({ + name, + value, + autoFocus, + ariaLabel, + placeholder, + onChange, + onFocus, + onBlur, + onSave, + onCancel, + onRemove, + growRange, + exprRef, + anchorRef, + onOpenExpandedMode, + isInExpandedMode, +}) => { + + const handleOnChange = async (value: string, updatedCursorPosition: number) => { + const newValue = "\"" + value + "\""; + onChange(newValue, updatedCursorPosition); + } + + return ( + + } + ariaLabel={ariaLabel} + onChange={handleOnChange} + onFocus={onFocus} + onBlur={onBlur} + onSave={onSave} + onCancel={onCancel} + onRemove={onRemove} + enableExIcon={false} + growRange={growRange} + sx={{ paddingInline: '0' }} + placeholder={placeholder} + /> + {onOpenExpandedMode && !isInExpandedMode && ( +
+ + + +
+ )} +
+ ); +}; + +export default RecordConfigPreviewEditor diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor.tsx index 8fde4736c91..d91d91d9e15 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/TextExpressionEditor/TextModeEditor.tsx @@ -16,13 +16,12 @@ * under the License. */ -import { FormExpressionEditor } from "@wso2/ui-toolkit"; -import { ExpressionField } from "../../ExpressionField"; import React from "react"; import { getValueForTextModeEditor } from "../../utils"; import styled from "@emotion/styled"; import { FloatingToggleButton } from "../ChipExpressionEditor/components/FloatingToggleButton"; import { ExpandIcon } from "../ChipExpressionEditor/components/FloatingButtonIcons"; +import { ChipExpressionEditorComponent, ChipExpressionEditorComponentProps } from "../ChipExpressionEditor/components/ChipExpressionEditor"; const EditorContainer = styled.div` width: 100%; @@ -38,57 +37,29 @@ const EditorContainer = styled.div` } `; -type TextModeEditorProps = Pick; - -export const TextModeEditor: React.FC = ({ - name, - value, - autoFocus, - ariaLabel, - placeholder, - onChange, - onFocus, - onBlur, - onSave, - onCancel, - onRemove, - growRange, - exprRef, - anchorRef, - onOpenExpandedMode, - isInExpandedMode, -}) => { - - const handleOnChange = async (value: string, updatedCursorPosition: number) => { - const newValue = "\"" + value + "\""; - onChange(newValue, updatedCursorPosition); - } +export const TextModeEditor: React.FC = (props) => { return ( - } - ariaLabel={ariaLabel} - onChange={handleOnChange} - onFocus={onFocus} - onBlur={onBlur} - onSave={onSave} - onCancel={onCancel} - onRemove={onRemove} - enableExIcon={false} - growRange={growRange} - sx={{ paddingInline: '0' }} - placeholder={placeholder} + - {onOpenExpandedMode && !isInExpandedMode && ( + {props.onOpenExpandedMode && !props.isInExpandedMode && (
- +
diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DocumentConfig.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DocumentConfig.tsx index fbef883dad6..54a40f33396 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DocumentConfig.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/DocumentConfig.tsx @@ -44,9 +44,8 @@ type DocumentConfigProps = { const AI_DOCUMENT_TYPES = Object.values(AIDocumentType); // Helper function to wrap content in document structure -const wrapInDocumentType = (documentType: AIDocumentType, content: string, addInterpolation: boolean = true): string => { - const docStructure = `<${documentType}>{content: ${content}}`; - return addInterpolation ? `\${${docStructure}}` : docStructure; +const wrapInDocumentType = (documentType: AIDocumentType, content: string): string => { + return`<${documentType}>{content: ${content}}`; }; export const DocumentConfig = ({ onChange, onClose, targetLineRange, filteredCompletions, currentValue, handleRetrieveCompletions, isInModal, inputMode }: DocumentConfigProps) => { @@ -164,13 +163,13 @@ export const DocumentConfig = ({ onChange, onClose, targetLineRange, filteredCom if (isAIDocumentType) { // For AI document types, wrap in string interpolation only in template mode if (isTemplateMode) { - onChange(`\${${fullPath}}`, false); + onChange(`${fullPath}`, false); } else { onChange(fullPath, false); } } else if (needsTypeCasting) { // Wrap the variable in the document structure with or without interpolation based on mode - const wrappedValue = wrapInDocumentType(documentType, fullPath, isTemplateMode); + const wrappedValue = wrapInDocumentType(documentType, fullPath); onChange(wrappedValue, false); } else { // For other types (records, etc.), insert directly @@ -212,7 +211,7 @@ export const DocumentConfig = ({ onChange, onClose, targetLineRange, filteredCom return; } const isTemplateMode = inputMode === InputMode.TEMPLATE; - const wrappedValue = wrapInDocumentType(documentType, `"${url.trim()}"`, isTemplateMode); + const wrappedValue = wrapInDocumentType(documentType, `"${url.trim()}"`); onChange(wrappedValue, false, false); closeModal(POPUP_IDS.DOCUMENT_URL); }; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/utils.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/utils.ts index e65fd742a3b..0cbe367348d 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/utils.ts +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/utils/utils.ts @@ -19,6 +19,6 @@ import { InputMode } from "@wso2/ballerina-side-panel"; // Wraps a value in template interpolation syntax ${} if in template mode -export const wrapInTemplateInterpolation = (value: string, inputMode?: InputMode): string => { - return inputMode === InputMode.TEMPLATE ? `\${${value}}` : value; +export const wrapInTemplateInterpolation = (value: string, _inputMode?: InputMode): string => { + return value; };