diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 5874b1febad..f6c8e6f37f3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -987,6 +987,12 @@ importers: '@codemirror/language': specifier: ~6.11.3 version: 6.11.3 + '@codemirror/language-data': + specifier: ~6.5.2 + version: 6.5.2 + '@codemirror/lint': + specifier: ~6.8.1 + version: 6.8.5 '@codemirror/state': specifier: ~6.5.2 version: 6.5.2 @@ -1017,6 +1023,42 @@ importers: lodash: specifier: ~4.17.21 version: 4.17.21 + markdown-it: + specifier: ~14.1.0 + version: 14.1.0 + prosemirror-commands: + specifier: ~1.7.1 + version: 1.7.1 + prosemirror-gapcursor: + specifier: ~1.4.0 + version: 1.4.0 + prosemirror-history: + specifier: ~1.5.0 + version: 1.5.0 + prosemirror-inputrules: + specifier: ~1.5.1 + version: 1.5.1 + prosemirror-keymap: + specifier: ~1.2.2 + version: 1.2.3 + prosemirror-markdown: + specifier: ~1.13.2 + version: 1.13.2 + prosemirror-model: + specifier: ~1.25.4 + version: 1.25.4 + prosemirror-schema-basic: + specifier: ~1.2.2 + version: 1.2.4 + prosemirror-schema-list: + specifier: ~1.5.1 + version: 1.5.1 + prosemirror-state: + specifier: ~1.4.3 + version: 1.4.4 + prosemirror-view: + specifier: ~1.41.3 + version: 1.41.4 react: specifier: 18.2.0 version: 18.2.0 @@ -1283,6 +1325,9 @@ importers: '@wso2/ballerina-core': specifier: workspace:* version: link:../ballerina-core + '@wso2/ballerina-side-panel': + specifier: workspace:* + version: link:../ballerina-side-panel '@wso2/ui-toolkit': specifier: workspace:* version: link:../../common-libs/ui-toolkit @@ -1298,6 +1343,9 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: 7.56.4 + version: 7.56.4(react@18.2.0) devDependencies: '@babel/core': specifier: ~7.27.1 @@ -9888,6 +9936,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash.camelcase@4.3.9': resolution: {integrity: sha512-ys9/hGBfsKxzmFI8hckII40V0ASQ83UM2pxfQRghHAwekhH4/jWtjz/3/9YDy7ZpUd/H0k2STSqmPR28dnj7Zg==} @@ -9906,6 +9957,9 @@ packages: '@types/lodash@4.17.17': resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -9915,6 +9969,9 @@ packages: '@types/mdurl@1.0.5': resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -18296,6 +18353,9 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + original@1.0.2: resolution: {integrity: sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==} @@ -19366,6 +19426,42 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-transform@1.10.5: + resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} + + prosemirror-view@1.41.4: + resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==} + protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -20351,6 +20447,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -35658,6 +35757,8 @@ snapshots: dependencies: '@types/node': 22.15.35 + '@types/linkify-it@5.0.0': {} + '@types/lodash.camelcase@4.3.9': dependencies: '@types/lodash': 4.17.17 @@ -35676,6 +35777,11 @@ snapshots: '@types/lodash@4.17.17': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -35686,6 +35792,8 @@ snapshots: '@types/mdurl@1.0.5': {} + '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} '@types/mime-types@2.1.4': {} @@ -47396,6 +47504,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + orderedmap@2.1.1: {} + original@1.0.2: dependencies: url-parse: 1.5.10 @@ -48531,6 +48641,72 @@ snapshots: property-information@7.1.0: {} + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.4 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.2: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 + + prosemirror-transform@1.10.5: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -50042,6 +50218,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + router@2.2.0: dependencies: debug: 4.4.3(supports-color@8.1.1) diff --git a/workspaces/ballerina/ballerina-side-panel/package.json b/workspaces/ballerina/ballerina-side-panel/package.json index a0d36fc7c3e..9c1eaeb002c 100644 --- a/workspaces/ballerina/ballerina-side-panel/package.json +++ b/workspaces/ballerina/ballerina-side-panel/package.json @@ -18,14 +18,32 @@ "author": "", "license": "ISC", "dependencies": { - "react": "18.2.0", - "react-dom": "18.2.0", + "@codemirror/autocomplete": "~6.19.1", + "@codemirror/commands": "~6.10.0", + "@codemirror/language-data": "~6.5.2", + "@codemirror/lint": "~6.8.1", + "@codemirror/state": "~6.5.2", + "@codemirror/view": "~6.38.8", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@wso2/ui-toolkit": "workspace:*", "@wso2/ballerina-core": "workspace:*", "@wso2/ballerina-rpc-client": "workspace:*", + "@wso2/ui-toolkit": "workspace:*", "lodash": "~4.17.21", + "markdown-it": "~14.1.0", + "prosemirror-commands": "~1.7.1", + "prosemirror-gapcursor": "~1.4.0", + "prosemirror-history": "~1.5.0", + "prosemirror-inputrules": "~1.5.1", + "prosemirror-keymap": "~1.2.2", + "prosemirror-markdown": "~1.13.2", + "prosemirror-model": "~1.25.4", + "prosemirror-schema-basic": "~1.2.2", + "prosemirror-schema-list": "~1.5.1", + "prosemirror-state": "~1.4.3", + "prosemirror-view": "~1.41.3", + "react": "18.2.0", + "react-dom": "18.2.0", "react-hook-form": "7.56.4", "react-markdown": "~10.1.0", "rehype-raw": "^7.0.0", @@ -41,10 +59,10 @@ }, "devDependencies": { "@storybook/react": "^6.5.16", + "@types/lodash": "~4.17.16", "@types/react": "18.2.0", "@types/react-dom": "18.2.0", - "typescript": "5.8.3", - "@types/lodash": "~4.17.16", - "copyfiles": "^2.4.1" + "copyfiles": "^2.4.1", + "typescript": "5.8.3" } } diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx index 4cd26249f0d..422df6601f6 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/ModeSwitcher/index.tsx @@ -26,14 +26,15 @@ interface ModeSwitcherProps { isRecordTypeField: boolean; onChange: (value: InputMode) => void; valueTypeConstraint: string | string[]; + fieldKey?: string; } -const ModeSwitcher: React.FC = ({ value, isRecordTypeField, onChange, valueTypeConstraint }) => { +const ModeSwitcher: React.FC = ({ value, isRecordTypeField, onChange, valueTypeConstraint, fieldKey }) => { const isChecked = value === InputMode.EXP; const defaultMode = useMemo( - () => isRecordTypeField ? InputMode.RECORD : getDefaultExpressionMode(valueTypeConstraint), - [valueTypeConstraint, isRecordTypeField] + () => isRecordTypeField ? InputMode.RECORD : getDefaultExpressionMode(valueTypeConstraint, fieldKey), + [valueTypeConstraint, isRecordTypeField, fieldKey] ); const handlePrimaryModeClick = () => { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx index 4ea059de9f7..b63b7c6101e 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/EditorFactory.tsx @@ -155,7 +155,7 @@ export const EditorFactory = (props: FormFieldEditorProps) => { /> ); - } else if (!field.items && (field.type === "RAW_TEMPLATE" || field.valueTypeConstraint === "ai:Prompt") && field.editable) { + } else if (!field.items && (field.type === "RAW_TEMPLATE") && field.editable) { return ( > = { - text: TextMode, - prompt: PromptMode, - expression: ExpressionMode, - template: TemplateMode + [InputMode.TEXT]: TextMode, + [InputMode.PROMPT]: PromptMode, + [InputMode.EXP]: ExpressionMode, + [InputMode.TEMPLATE]: TemplateMode }; export const ExpandedEditor: React.FC = ({ @@ -168,29 +169,27 @@ export const ExpandedEditor: React.FC = ({ extractArgsFromFunction, getHelperPane, error, - formDiagnostics + formDiagnostics, + inputMode }) => { - const promptFields = ["query", "instructions", "role"]; + const promptFields = ["instructions", "role"]; // Determine mode - use prop if provided, otherwise auto-detect - const defaultMode: EditorMode = propMode ?? ( - promptFields.includes(field.key) ? "prompt" : "text" + let defaultMode: EditorMode = propMode ?? ( + promptFields.includes(field.key) ? InputMode.PROMPT : InputMode.TEXT ); + if (field.key === "query" && propMode === InputMode.TEXT) { + defaultMode = InputMode.PROMPT; + } + const [mode, setMode] = useState(defaultMode); - const [showPreview, setShowPreview] = useState(false); const [mouseDownTarget, setMouseDownTarget] = useState(null); useEffect(() => { setMode(defaultMode); }, [defaultMode]); - useEffect(() => { - if (mode === "text") { - setShowPreview(false); - } - }, [mode]); - const handleMinimize = () => { onClose(); }; @@ -217,13 +216,21 @@ export const ExpandedEditor: React.FC = ({ value: value, onChange: onChange, field, - // Props for modes with preview support - ...(mode === "prompt" && { - isPreviewMode: showPreview, - onTogglePreview: (enabled: boolean) => setShowPreview(enabled) + // Props for prompt mode + ...(mode === InputMode.PROMPT && { + completions, + fileName, + targetLineRange, + sanitizedExpression, + rawExpression, + extractArgsFromFunction, + getHelperPane, + error, + formDiagnostics, + inputMode }), // Props for expression mode - ...(mode === "expression" && { + ...(mode === InputMode.EXP && { completions, fileName, targetLineRange, @@ -235,7 +242,7 @@ export const ExpandedEditor: React.FC = ({ formDiagnostics }), // Props for template mode - ...(mode === "template" && { + ...(mode === InputMode.TEMPLATE && { completions, fileName, targetLineRange, @@ -243,10 +250,9 @@ export const ExpandedEditor: React.FC = ({ rawExpression, extractArgsFromFunction, getHelperPane, - isPreviewMode: showPreview, - onTogglePreview: (enabled: boolean) => setShowPreview(enabled), error, - formDiagnostics + formDiagnostics, + inputMode }) }; // HACK: Must find a proper central way to manager popups diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/ChipComponent.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/ChipComponent.tsx deleted file mode 100644 index 7121af0b8f2..00000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/ChipComponent.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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 { DocumentType, TokenType } from "../../MultiModeExpressionEditor/ChipExpressionEditor/types"; -import { - getChipDisplayContent, - StandardChip, - ChipText, - getTokenIconClass, - StandardIcon -} from "../../MultiModeExpressionEditor/ChipExpressionEditor/chipStyles"; - -export interface ChipComponentProps { - type: TokenType; - content: string; - documentType?: DocumentType; -} - -export const ChipComponent: React.FC = ({ type, content, documentType }) => { - let iconClass = getTokenIconClass(type, documentType); - return ( - - - {getChipDisplayContent(type, content)} - - ); -}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/LinkDialog.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/LinkDialog.tsx new file mode 100644 index 00000000000..ca85542738d --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/LinkDialog.tsx @@ -0,0 +1,175 @@ +/** + * 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, { useState, useEffect } from "react"; +import styled from "@emotion/styled"; +import { TextField, Button, Codicon, Divider, Typography, ThemeColors } from "@wso2/ui-toolkit"; + +const PopupContainer = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 30000; + display: flex; + justify-content: center; + align-items: center; +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + z-index: 29999; +`; + +const PopupBox = styled.div` + width: 400px; + max-width: 90vw; + max-height: 90vh; + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 16px; + border-radius: 3px; + background-color: ${ThemeColors.SURFACE_DIM}; + box-shadow: 0 3px 8px rgb(0 0 0 / 0.2); + z-index: 30001; +`; + +const PopupHeader = styled.header` + display: flex; + align-items: center; + justify-content: space-between; + padding-inline: 16px; + margin-bottom: 8px; +`; + +const PopupContent = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; +`; + +const DialogActions = styled.div` + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 8px; + padding: 0 16px 8px; +`; + +interface LinkDialogProps { + isOpen: boolean; + onClose: () => void; + onInsert: (href: string, title?: string) => void; + initialTitle?: string; +} + +export const LinkDialog: React.FC = ({ + isOpen, + onClose, + onInsert, + initialTitle = "" +}) => { + const [url, setUrl] = useState(""); + const [title, setTitle] = useState(initialTitle); + + useEffect(() => { + if (isOpen) { + setUrl(""); + setTitle(initialTitle); + } + }, [isOpen, initialTitle]); + + const handleInsert = () => { + if (!url.trim()) return; + onInsert(url.trim(), title.trim() || undefined); + handleClose(); + }; + + const handleClose = () => { + setUrl(""); + setTitle(""); + onClose(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && url.trim()) { + e.preventDefault(); + handleInsert(); + } else if (e.key === "Escape") { + e.preventDefault(); + handleClose(); + } + }; + + if (!isOpen) return null; + + return ( + <> + + + + + + Create a link + + + + + + setUrl((e.target as HTMLInputElement).value)} + autoFocus={true} + required={true} + /> + + setTitle((e.target as HTMLInputElement).value)} + /> + + + + + + + + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownPreview.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownPreview.tsx deleted file mode 100644 index 7429841c27b..00000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownPreview.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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 styled from "@emotion/styled"; -import { ThemeColors } from "@wso2/ui-toolkit"; -import ReactMarkdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; -import { ChipComponent } from "./ChipComponent"; -import { DocumentType, TokenType } from "../../MultiModeExpressionEditor/ChipExpressionEditor/types"; -import "../styles/markdown-preview.css"; - -const PreviewContainer = styled.div` - width: 100%; - height: 100%; - padding: 16px; - background: var(--input-background); - border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; - border-top: none; - border-radius: 0 0 4px 4px; - overflow-y: auto; - overflow-x: auto; - box-sizing: border-box; - - .markdown-body { - background: transparent; - font-size: 14px; - color: ${ThemeColors.ON_SURFACE}; - } -`; - -interface MarkdownPreviewProps { - /** Markdown content to render */ - content: string; -} - -/** - * Markdown preview component that renders markdown content - */ -export const MarkdownPreview: React.FC = ({ content }) => { - return ( - -
- null, - iframe: () => null, - // Custom chip component for rendering tokens - chip: ({ node }: any) => { - const props = node?.properties || {}; - return ( - - ); - } - }} - disallowedElements={['script', 'iframe', 'object', 'embed']} - unwrapDisallowed={true} - > - {content} - -
-
- ); -}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownToolbar.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownToolbar.tsx deleted file mode 100644 index 09bd74955aa..00000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/MarkdownToolbar.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/** - * 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 styled from "@emotion/styled"; -import { ThemeColors, Icon, Switch } from "@wso2/ui-toolkit"; -import "@github/markdown-toolbar-element"; - -// Type declarations for GitHub markdown toolbar custom elements -declare global { - namespace JSX { - interface IntrinsicElements { - 'markdown-toolbar': React.DetailedHTMLProps & { for?: string }, HTMLElement>; - 'md-bold': React.DetailedHTMLProps, HTMLElement>; - 'md-italic': React.DetailedHTMLProps, HTMLElement>; - 'md-code': React.DetailedHTMLProps, HTMLElement>; - 'md-link': React.DetailedHTMLProps, HTMLElement>; - 'md-header': React.DetailedHTMLProps & { 'data-md-header'?: string }, HTMLElement>; - 'md-quote': React.DetailedHTMLProps, HTMLElement>; - 'md-unordered-list': React.DetailedHTMLProps, HTMLElement>; - 'md-ordered-list': React.DetailedHTMLProps, HTMLElement>; - 'md-task-list': React.DetailedHTMLProps, HTMLElement>; - 'md-code-block': React.DetailedHTMLProps, HTMLElement>; - } - } -} - -const ToolbarContainer = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - padding: 8px 12px; - border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; - border-radius: 4px 4px 0 0; - flex-wrap: wrap; - font-family: GilmerMedium; - - markdown-toolbar { - display: flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; - } -`; - -const ToolbarButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - background-color: transparent; - color: ${ThemeColors.ON_SURFACE}; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - - &:hover:not(:disabled) { - background-color: ${ThemeColors.SECONDARY_CONTAINER}; - border-color: ${ThemeColors.OUTLINE}; - } - - &:active:not(:disabled) { - background-color: ${ThemeColors.SECONDARY_CONTAINER}; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &:focus-visible { - outline: 2px solid ${ThemeColors.PRIMARY}; - outline-offset: 2px; - } -`; - -const ToolbarDivider = styled.div` - width: 1px; - height: 24px; - background-color: ${ThemeColors.OUTLINE_VARIANT}; - margin: 0 4px; -`; - -interface MarkdownToolbarProps { - /** ID of the textarea this toolbar controls */ - textareaId: string; - /** Whether preview mode is active */ - isPreviewMode?: boolean; - /** Callback to toggle preview mode */ - onTogglePreview?: () => void; -} - -/** - * Markdown formatting toolbar using GitHub's markdown-toolbar-element - * Provides buttons for common markdown formatting operations - */ -export const MarkdownToolbar: React.FC = ({ - textareaId, - isPreviewMode = false, - onTogglePreview -}) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {onTogglePreview && ( - - )} - - ); -}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RawTemplateMarkdownToolbar.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RawTemplateMarkdownToolbar.tsx new file mode 100644 index 00000000000..5ffd0d4dc27 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RawTemplateMarkdownToolbar.tsx @@ -0,0 +1,407 @@ +/** + * 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, { useState, useRef, useEffect } from "react"; +import styled from "@emotion/styled"; +import { ThemeColors, Icon, Switch } from "@wso2/ui-toolkit"; +import { EditorView } from "@codemirror/view"; +import { + insertMarkdownFormatting, + insertMarkdownHeader, + insertMarkdownLink, + insertMarkdownBlockquote, + insertMarkdownUnorderedList, + insertMarkdownOrderedList, + undoCommand, + redoCommand, + canUndo, + canRedo +} from "../utils/templateUtils"; +import { HelperPaneToggleButton } from "../../MultiModeExpressionEditor/ChipExpressionEditor/components/HelperPaneToggleButton"; + +const ToolbarContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + padding: 8px 12px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 4px 4px 0 0; + flex-wrap: wrap; + font-family: GilmerMedium; +`; + +const ToolbarButtonGroup = styled.div` + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +`; + +const ToolbarButton = styled.button<{ isActive?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background-color: ${(props: { isActive?: boolean }) => props.isActive ? ThemeColors.SECONDARY_CONTAINER : 'transparent'}; + color: ${ThemeColors.ON_SURFACE}; + border: 1px solid ${(props: { isActive?: boolean }) => props.isActive ? ThemeColors.OUTLINE : 'transparent'}; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + border-color: ${ThemeColors.OUTLINE}; + } + + &:active:not(:disabled) { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:focus-visible { + outline: 2px solid ${ThemeColors.PRIMARY}; + outline-offset: 2px; + } +`; + +const ToolbarDivider = styled.div` + width: 1px; + height: 24px; + background-color: ${ThemeColors.OUTLINE_VARIANT}; + margin: 0 4px; +`; + +const SplitButtonContainer = styled.div` + position: relative; + display: flex; + align-items: center; +`; + +const SplitButtonMain = styled(ToolbarButton)` + border-radius: 4px 0 0 4px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-right: none; + min-width: 40px; + font-size: 12px; + font-weight: 600; +`; + +const SplitButtonDropdown = styled(ToolbarButton)` + width: 24px; + border-radius: 0 4px 4px 0; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; +`; + +const DropdownMenu = styled.div<{ isOpen: boolean }>` + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 120px; + background-color: var(--vscode-dropdown-background); + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 4px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + z-index: 1000; + display: ${(props: { isOpen: boolean }) => props.isOpen ? 'block' : 'none'}; + overflow: hidden; +`; + +const DropdownItem = styled.button<{ size: number }>` + width: 100%; + padding: 8px 12px; + background-color: transparent; + color: ${ThemeColors.ON_SURFACE}; + border: none; + text-align: left; + cursor: pointer; + font-size: ${(props: { size: number }) => { + const sizes: { [key: number]: string } = { + 1: '16px', + 2: '15px', + 3: '14px', + 4: '13px', + 5: '12px', + 6: '11px' + }; + return sizes[props.size] || '14px'; + }}; + font-weight: ${(props: { size: number }) => props.size <= 3 ? '600' : '500'}; + transition: background-color 0.2s ease; + + &:hover { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:active { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:focus-visible { + outline: 2px solid ${ThemeColors.PRIMARY}; + outline-offset: -2px; + } +`; + +interface RawTemplateMarkdownToolbarProps { + editorView: EditorView | null; + isSourceView?: boolean; + onToggleView?: () => void; + hideHelperPaneToggle?: boolean; + helperPaneToggle?: { + ref: React.RefObject; + isOpen: boolean; + onClick: () => void; + }; +} + +export const RawTemplateMarkdownToolbar = React.forwardRef(({ + editorView, + isSourceView = false, + onToggleView, + hideHelperPaneToggle = false, + helperPaneToggle +}, ref) => { + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + const [currentHeadingLevel, setCurrentHeadingLevel] = useState(1); + const [isHeadingDropdownOpen, setIsHeadingDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + // Update toolbar state when editor state changes + React.useEffect(() => { + if (!editorView) return; + + const updateListener = () => { + forceUpdate(); + }; + + editorView.dom.addEventListener('input', updateListener); + editorView.dom.addEventListener('click', updateListener); + editorView.dom.addEventListener('keyup', updateListener); + + return () => { + editorView.dom.removeEventListener('input', updateListener); + editorView.dom.removeEventListener('click', updateListener); + editorView.dom.removeEventListener('keyup', updateListener); + }; + }, [editorView]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsHeadingDropdownOpen(false); + } + }; + + if (isHeadingDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [isHeadingDropdownOpen]); + + // Prevent buttons from taking focus away from the editor + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + }; + + const handleBold = () => insertMarkdownFormatting(editorView, '**'); + const handleItalic = () => insertMarkdownFormatting(editorView, '_'); + // const handleCode = () => insertMarkdownFormatting(editorView, '`'); // Inline code formatting disabled for now + const handleLink = () => insertMarkdownLink(editorView); + const handleQuote = () => insertMarkdownBlockquote(editorView); + const handleUnorderedList = () => insertMarkdownUnorderedList(editorView); + const handleOrderedList = () => insertMarkdownOrderedList(editorView); + + const handleHeader = (level?: number) => { + const headingLevel = level ?? currentHeadingLevel; + insertMarkdownHeader(editorView, headingLevel); + if (level !== undefined) { + setCurrentHeadingLevel(level); + setIsHeadingDropdownOpen(false); + } + }; + + const toggleHeadingDropdown = () => setIsHeadingDropdownOpen(!isHeadingDropdownOpen); + + const isUndoAvailable = editorView ? canUndo(editorView) : false; + const isRedoAvailable = editorView ? canRedo(editorView) : false; + + return ( + + + {!hideHelperPaneToggle && helperPaneToggle && ( + <> + + + + )} + + undoCommand(editorView)} + onMouseDown={handleMouseDown} + > + + + + redoCommand(editorView)} + onMouseDown={handleMouseDown} + > + + + + + + + + + + + + + + {/* + + */} + + + + + + + + + handleHeader()} + onMouseDown={handleMouseDown} + > + H{currentHeadingLevel} + + + + + + {[1, 2, 3, 4, 5, 6].map((level) => ( + handleHeader(level)} + onMouseDown={handleMouseDown} + > + Heading {level} + + ))} + + + + + + + + + + + + + + + + + + + {onToggleView && ( + + )} + + ); +}); + +RawTemplateMarkdownToolbar.displayName = 'RawTemplateMarkdownToolbar'; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RichTemplateMarkdownToolbar.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RichTemplateMarkdownToolbar.tsx new file mode 100644 index 00000000000..54b819d7c97 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/RichTemplateMarkdownToolbar.tsx @@ -0,0 +1,469 @@ +/** + * 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, { useState, useRef, useEffect } from "react"; +import styled from "@emotion/styled"; +import { ThemeColors, Icon, Switch } from "@wso2/ui-toolkit"; +import { EditorView } from "prosemirror-view"; +import { + toggleBold, + toggleItalic, + toggleLink, + toggleHeading, + toggleBlockquote, + toggleBulletList, + toggleOrderedList, + isMarkActive, + isNodeActive, + isListActive, + undoCommand, + redoCommand, + canUndo, + canRedo +} from "../../MultiModeExpressionEditor/RichTextTemplateEditor/plugins/markdownCommands"; +import { HelperPaneToggleButton } from "../../MultiModeExpressionEditor/ChipExpressionEditor/components/HelperPaneToggleButton"; +import { LinkDialog } from "./LinkDialog"; + +const ToolbarContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + padding: 8px 12px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 4px 4px 0 0; + flex-wrap: wrap; + font-family: GilmerMedium; +`; + +const ToolbarButtonGroup = styled.div` + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +`; + +const ToolbarButton = styled.button<{ isActive?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background-color: ${(props: { isActive?: boolean }) => props.isActive ? ThemeColors.SECONDARY_CONTAINER : 'transparent'}; + color: ${ThemeColors.ON_SURFACE}; + border: 1px solid ${(props: { isActive?: boolean }) => props.isActive ? ThemeColors.OUTLINE : 'transparent'}; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + border-color: ${ThemeColors.OUTLINE}; + } + + &:active:not(:disabled) { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:focus-visible { + outline: 2px solid ${ThemeColors.PRIMARY}; + outline-offset: 2px; + } +`; + +const ToolbarDivider = styled.div` + width: 1px; + height: 24px; + background-color: ${ThemeColors.OUTLINE_VARIANT}; + margin: 0 4px; +`; + +const SplitButtonContainer = styled.div` + position: relative; + display: flex; + align-items: center; +`; + +const SplitButtonMain = styled(ToolbarButton)` + border-radius: 4px 0 0 4px; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-right: none; + min-width: 40px; + font-size: 12px; + font-weight: 600; +`; + +const SplitButtonDropdown = styled(ToolbarButton)` + width: 24px; + border-radius: 0 4px 4px 0; + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; +`; + +const DropdownMenu = styled.div<{ isOpen: boolean }>` + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 120px; + background-color: var(--vscode-dropdown-background); + border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; + border-radius: 4px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + z-index: 1000; + display: ${(props: { isOpen: boolean }) => props.isOpen ? 'block' : 'none'}; + overflow: hidden; +`; + +const DropdownItem = styled.button<{ size: number }>` + width: 100%; + padding: 8px 12px; + background-color: transparent; + color: ${ThemeColors.ON_SURFACE}; + border: none; + text-align: left; + cursor: pointer; + font-size: ${(props: { size: number }) => { + const sizes: { [key: number]: string } = { + 1: '16px', + 2: '15px', + 3: '14px', + 4: '13px', + 5: '12px', + 6: '11px' + }; + return sizes[props.size] || '14px'; + }}; + font-weight: ${(props: { size: number }) => props.size <= 3 ? '600' : '500'}; + transition: background-color 0.2s ease; + + &:hover { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:active { + background-color: ${ThemeColors.SECONDARY_CONTAINER}; + } + + &:focus-visible { + outline: 2px solid ${ThemeColors.PRIMARY}; + outline-offset: -2px; + } +`; + +interface RichTemplateMarkdownToolbarProps { + editorView: EditorView | null; + isSourceView?: boolean; + onToggleView?: () => void; + hideHelperPaneToggle?: boolean; + helperPaneToggle?: { + ref: React.RefObject; + isOpen: boolean; + onClick: () => void; + }; +} + +export const RichTemplateMarkdownToolbar = React.forwardRef(({ + editorView, + isSourceView = false, + onToggleView, + hideHelperPaneToggle = false, + helperPaneToggle +}, ref) => { + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + const [currentHeadingLevel, setCurrentHeadingLevel] = useState(1); + const [isHeadingDropdownOpen, setIsHeadingDropdownOpen] = useState(false); + const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false); + const [selectedTextForLink, setSelectedTextForLink] = useState(""); + const dropdownRef = useRef(null); + + // Update toolbar state when editor state changes + React.useEffect(() => { + if (!editorView) return; + + const updateListener = () => { + forceUpdate(); + }; + + editorView.dom.addEventListener('input', updateListener); + editorView.dom.addEventListener('click', updateListener); + editorView.dom.addEventListener('keyup', updateListener); + + return () => { + editorView.dom.removeEventListener('input', updateListener); + editorView.dom.removeEventListener('click', updateListener); + editorView.dom.removeEventListener('keyup', updateListener); + }; + }, [editorView]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsHeadingDropdownOpen(false); + } + }; + + if (isHeadingDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [isHeadingDropdownOpen]); + + // Prevent buttons from taking focus away from the editor + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + }; + + const executeCommand = (command: (state: any, dispatch?: any, view?: any) => boolean) => { + if (!editorView) return; + const didChange = command(editorView.state, editorView.dispatch, editorView); + if (didChange) { + forceUpdate(); + } + editorView.focus(); + }; + + const handleHeader = (level?: number) => { + const headingLevel = level ?? currentHeadingLevel; + executeCommand(toggleHeading(headingLevel)); + if (level !== undefined) { + setCurrentHeadingLevel(level); + setIsHeadingDropdownOpen(false); + } + }; + + const toggleHeadingDropdown = () => setIsHeadingDropdownOpen(!isHeadingDropdownOpen); + + const handleLinkButtonClick = () => { + if (!editorView) return; + + const schema = editorView.state.schema; + const { from, to } = editorView.state.selection; + + // If already has link, remove it immediately + if (from !== to && editorView.state.doc.rangeHasMark(from, to, schema.marks.link)) { + executeCommand(toggleLink()); + return; + } + + // Get selected text if any to pre-fill title field + const selectedText = editorView.state.doc.textBetween(from, to, ' '); + setSelectedTextForLink(selectedText); + setIsLinkDialogOpen(true); + }; + + const handleInsertLink = (href: string, title?: string) => { + executeCommand(toggleLink(href, title)); + setIsLinkDialogOpen(false); + }; + + const schema = editorView?.state.schema; + + const isBoldActive = editorView && schema ? isMarkActive(editorView.state, schema.marks.strong) : false; + const isItalicActive = editorView && schema ? isMarkActive(editorView.state, schema.marks.em) : false; + // const isCodeActive = editorView && schema ? isMarkActive(editorView.state, schema.marks.code) : false; // Inline code formatting disabled for now + const isLinkActive = editorView && schema ? isMarkActive(editorView.state, schema.marks.link) : false; + + const isCurrentHeadingActive = editorView && schema + ? isNodeActive(editorView.state, schema.nodes.heading, { level: currentHeadingLevel }) + : false; + + const isBlockquoteActive = editorView && schema ? isNodeActive(editorView.state, schema.nodes.blockquote) : false; + const isBulletListActive = editorView && schema ? isListActive(editorView.state, schema.nodes.bullet_list) : false; + const isOrderedListActive = editorView && schema ? isListActive(editorView.state, schema.nodes.ordered_list) : false; + + const isUndoAvailable = editorView ? canUndo(editorView.state) : false; + const isRedoAvailable = editorView ? canRedo(editorView.state) : false; + + return ( + + + {!hideHelperPaneToggle && helperPaneToggle && ( + <> + + + + + )} + + executeCommand(undoCommand)} + onMouseDown={handleMouseDown} + > + + + + executeCommand(redoCommand)} + onMouseDown={handleMouseDown} + > + + + + + + executeCommand(toggleBold)} + onMouseDown={handleMouseDown} + > + + + + executeCommand(toggleItalic)} + onMouseDown={handleMouseDown} + > + + + + {/* executeCommand(toggleCode)} + onMouseDown={handleMouseDown} + > + + */} + + + + + + + + + handleHeader()} + onMouseDown={handleMouseDown} + > + H{currentHeadingLevel} + + + + + + {[1, 2, 3, 4, 5, 6].map((level) => ( + handleHeader(level)} + onMouseDown={handleMouseDown} + > + Heading {level} + + ))} + + + + executeCommand(toggleBlockquote)} + onMouseDown={handleMouseDown} + > + + + + + + executeCommand(toggleBulletList)} + onMouseDown={handleMouseDown} + > + + + + executeCommand(toggleOrderedList)} + onMouseDown={handleMouseDown} + > + + + + + {onToggleView && ( + + )} + + setIsLinkDialogOpen(false)} + onInsert={handleInsertLink} + initialTitle={selectedTextForLink} + /> + + ); +}); + +RichTemplateMarkdownToolbar.displayName = 'RichTemplateMarkdownToolbar'; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/TemplateMarkdownToolbar.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/TemplateMarkdownToolbar.tsx deleted file mode 100644 index a26e550497e..00000000000 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/controls/TemplateMarkdownToolbar.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * 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 styled from "@emotion/styled"; -import { ThemeColors, Icon } from "@wso2/ui-toolkit"; -import { EditorView } from "@codemirror/view"; -import { - insertMarkdownFormatting, - insertMarkdownHeader, - insertMarkdownLink, - insertMarkdownBlockquote, - insertMarkdownUnorderedList, - insertMarkdownOrderedList, - insertMarkdownTaskList -} from "../utils/templateUtils"; -import { HelperPaneToggleButton } from "../../MultiModeExpressionEditor/ChipExpressionEditor/components/HelperPaneToggleButton"; - -const ToolbarContainer = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - padding: 8px 12px; - border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; - border-radius: 4px 4px 0 0; - flex-wrap: wrap; - font-family: GilmerMedium; -`; - -const ToolbarButtonGroup = styled.div` - display: flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; -`; - -const ToolbarButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - background-color: transparent; - color: ${ThemeColors.ON_SURFACE}; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - - &:hover:not(:disabled) { - background-color: ${ThemeColors.SECONDARY_CONTAINER}; - border-color: ${ThemeColors.OUTLINE}; - } - - &:active:not(:disabled) { - background-color: ${ThemeColors.SECONDARY_CONTAINER}; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &:focus-visible { - outline: 2px solid ${ThemeColors.PRIMARY}; - outline-offset: 2px; - } -`; - -const ToolbarDivider = styled.div` - width: 1px; - height: 24px; - background-color: ${ThemeColors.OUTLINE_VARIANT}; - margin: 0 4px; -`; - -interface TemplateMarkdownToolbarProps { - editorView: EditorView | null; - isPreviewMode?: boolean; - onTogglePreview?: () => void; - helperPaneToggle?: { - ref: React.RefObject; - isOpen: boolean; - onClick: () => void; - }; -} - -export const TemplateMarkdownToolbar = React.forwardRef(({ - editorView, - isPreviewMode = false, - onTogglePreview, - helperPaneToggle -}, ref) => { - // Prevent buttons from taking focus away from the editor - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault(); - }; - - const handleBold = () => insertMarkdownFormatting(editorView, '**'); - const handleItalic = () => insertMarkdownFormatting(editorView, '_'); - const handleCode = () => insertMarkdownFormatting(editorView, '`'); - const handleLink = () => insertMarkdownLink(editorView); - const handleHeader = () => insertMarkdownHeader(editorView, 3); - const handleQuote = () => insertMarkdownBlockquote(editorView); - const handleUnorderedList = () => insertMarkdownUnorderedList(editorView); - const handleOrderedList = () => insertMarkdownOrderedList(editorView); - const handleTaskList = () => insertMarkdownTaskList(editorView); - - return ( - - - {helperPaneToggle && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}); - -TemplateMarkdownToolbar.displayName = 'TemplateMarkdownToolbar'; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx index 42fad8c2b02..388bf5122b7 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx @@ -69,9 +69,9 @@ export const ExpressionMode: React.FC = ({ /> {error ? - : + : formDiagnostics && formDiagnostics.length > 0 && - d.message).join(', ')} /> + d.message).join(', ')} /> } ); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx index 2bc061a6dda..0a34527402d 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx @@ -16,170 +16,145 @@ * under the License. */ -import React from "react"; +import React, { useState, useRef } from "react"; import styled from "@emotion/styled"; -import { ThemeColors } from "@wso2/ui-toolkit"; -import { EditorModeWithPreviewProps } from "./types"; -import { MarkdownToolbar } from "../controls/MarkdownToolbar"; -import { MarkdownPreview } from "../controls/MarkdownPreview"; - -const TextArea = styled.textarea` +import { EditorView as CodeMirrorView } from "@codemirror/view"; +import { EditorView as ProseMirrorView } from "prosemirror-view"; +import { EditorModeExpressionProps } from "./types"; +import { ChipExpressionEditorComponent } from "../../MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor"; +import { RichTextTemplateEditor } from "../../MultiModeExpressionEditor/RichTextTemplateEditor/RichTextTemplateEditor"; +import { RichTemplateMarkdownToolbar } from "../controls/RichTemplateMarkdownToolbar"; +import { RawTemplateMarkdownToolbar } from "../controls/RawTemplateMarkdownToolbar"; +import { ErrorBanner } from "@wso2/ui-toolkit"; +import { RawTemplateEditorConfig, StringTemplateEditorConfig } from "../../MultiModeExpressionEditor/Configurations"; + +const ExpressionContainer = styled.div` width: 100%; - height: 100%; - padding: 12px !important; - fontSize: 13px; - font-family: var(--vscode-editor-font-family); - background: var(--input-background); - color: ${ThemeColors.ON_SURFACE}; - border: 1px solid ${ThemeColors.OUTLINE_VARIANT}; - border-radius: 0 0 4px 4px; - border-top: none; - resize: none; - outline: none; + flex: 1; + display: flex; + flex-direction: column; box-sizing: border-box; + overflow: hidden; `; -const TEXTAREA_ID = "prompt-textarea"; +const SIMPLE_PROMPT_FIELDS = ["query", "instructions", "role"]; -/** - * Prompt mode editor - textarea with markdown toolbar and preview support - */ -export const PromptMode: React.FC = ({ +export const PromptMode: React.FC = ({ value, onChange, - isPreviewMode, - onTogglePreview, - field + field, + completions = [], + fileName, + targetLineRange, + sanitizedExpression, + extractArgsFromFunction, + getHelperPane, + rawExpression, + error, + formDiagnostics, + inputMode }) => { - /** - * Handles Enter key to automatically continue lists (similar to GitHub comments) - */ - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) { - return; - } - - const textarea = e.currentTarget; - const cursorPosition = textarea.selectionStart; - const textBeforeCursor = value.substring(0, cursorPosition); - const textAfterCursor = value.substring(cursorPosition); - - // Find the start of the current line - const lastNewlineIndex = textBeforeCursor.lastIndexOf('\n'); - const currentLine = textBeforeCursor.substring(lastNewlineIndex + 1); - - // Check for unordered list (- or *) - const unorderedMatch = currentLine.match(/^(\s*)([-*])\s+(.*)$/); - if (unorderedMatch) { - const [, indent, marker, content] = unorderedMatch; - - // If the list item is empty (just the marker), remove it and exit list mode - if (!content.trim()) { - e.preventDefault(); - const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after both newlines - queueMicrotask(() => { - textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; - }); - return; - } - - // Continue the list - e.preventDefault(); - const newValue = textBeforeCursor + '\n' + indent + marker + ' ' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after the list marker - queueMicrotask(() => { - const newCursorPos = cursorPosition + indent.length + marker.length + 2; - textarea.selectionStart = textarea.selectionEnd = newCursorPos; - }); - return; - } - - // Check for ordered list (1., 2., etc.) - const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s+(.*)$/); - if (orderedMatch) { - const [, indent, number, content] = orderedMatch; - - // If the list item is empty (just the number), remove it and exit list mode - if (!content.trim()) { - e.preventDefault(); - const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after both newlines - queueMicrotask(() => { - textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; - }); - return; - } - - // Continue the list with incremented number - e.preventDefault(); - const nextNumber = parseInt(number, 10) + 1; - const newValue = textBeforeCursor + '\n' + indent + nextNumber + '. ' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after the list marker - queueMicrotask(() => { - const newCursorPos = cursorPosition + indent.length + nextNumber.toString().length + 3; - textarea.selectionStart = textarea.selectionEnd = newCursorPos; - }); - return; - } - - // Check for task list (- [ ] or - [x]) - const taskMatch = currentLine.match(/^(\s*)([-*])\s+\[([ x])\]\s+(.*)$/); - if (taskMatch) { - const [, indent, marker, , content] = taskMatch; - - // If the task item is empty, remove it and exit list mode - if (!content.trim()) { - e.preventDefault(); - const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after both newlines - queueMicrotask(() => { - textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; - }); - return; - } + // Detect if this is a simple prompt field (text-only, no advanced features) + const isSimpleMode = SIMPLE_PROMPT_FIELDS.includes(field.key) && !getHelperPane; + + const [isSourceView, setIsSourceView] = useState(false); + const [codeMirrorView, setCodeMirrorView] = useState(null); + const [proseMirrorView, setProseMirrorView] = useState(null); + const [helperPaneToggle, setHelperPaneToggle] = useState<{ + ref: React.RefObject; + isOpen: boolean; + onClick: () => void; + } | null>(null); + const richToolbarRef = useRef(null); + const rawToolbarRef = useRef(null); + + // Convert onChange signature from (value: string) => void to (value: string, cursorPosition: number) => void + const handleChange = (updatedValue: string, updatedCursorPosition: number) => { + onChange(updatedValue, updatedCursorPosition); + }; - // Continue the task list with unchecked box - e.preventDefault(); - const newValue = textBeforeCursor + '\n' + indent + marker + ' [ ] ' + textAfterCursor; - onChange(newValue, cursorPosition); - // Set cursor position after the task marker - queueMicrotask(() => { - const newCursorPos = cursorPosition + indent.length + marker.length + 6; - textarea.selectionStart = textarea.selectionEnd = newCursorPos; - }); - return; - } + const handleHelperPaneStateChange = (state: { + isOpen: boolean; + ref: React.RefObject; + toggle: () => void; + }) => { + setHelperPaneToggle({ + ref: state.ref, + isOpen: state.isOpen, + onClick: state.toggle + }); }; - const placeholder = field.placeholder && field.placeholder.trim() !== "" && field.placeholder.trim() !== "\"\"" - ? field.placeholder - : "Enter your text here..."; + const handleToggleView = () => { + setIsSourceView(!isSourceView); + }; return ( <> - onTogglePreview(!isPreviewMode)} - /> - {isPreviewMode ? ( - + {isSourceView ? ( + ) : ( -