From 53fe0c93e3da73d2c751c9360ca05bfc1e6f00db Mon Sep 17 00:00:00 2001 From: samithkavishke Date: Wed, 16 Jul 2025 14:32:40 +0530 Subject: [PATCH 001/602] Add readonly checkbox to typediagram --- .../src/TypeEditor/AdvancedOptions.tsx | 21 ++++++++++++++++++- .../type-editor/src/TypeEditor/TypeEditor.tsx | 6 +++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx index e1de7bba339..b6b4f1904a1 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx @@ -28,7 +28,7 @@ export function AdvancedOptions({ type, onChange }: AdvancedOptionsProps) { Advanced Options {isExpanded && ( - + { + // Match the same pattern used in the working checkbox + onChange({ + ...type, + properties: { + ...type.properties, + isReadOnly: { + ...type.properties?.isReadOnly, + value: checked ? "true" : "false" + } + } + }); + }} + /> + )} ); diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx index 25a654aa5fa..cac5ba8d7fc 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx @@ -80,7 +80,11 @@ export function TypeEditor(props: TypeEditorProps) { readonly: false, label: "" }, - properties: {}, + properties: { + isReadOnly: { + value: "false", + }, + }, codedata: { node: "RECORD" as TypeNodeKind }, From 95003b0503a96917fa7b4c1b4b8f32b6c34d7174 Mon Sep 17 00:00:00 2001 From: samithkavishke Date: Fri, 18 Jul 2025 12:42:45 +0530 Subject: [PATCH 002/602] Add tests and reaonly support for types --- .../src/interfaces/extended-lang-client.ts | 1 + .../src/TypeEditor/AdvancedOptions.tsx | 13 ++- .../src/TypeEditor/FieldEditor.tsx | 15 ++- .../src/TypeEditor/Tabs/TypeCreatorTab.tsx | 6 ++ .../type-editor/src/TypeEditor/TypeEditor.tsx | 6 +- .../test/e2e-playwright-tests/test.list.ts | 2 - .../type/TypeEditorUtils.ts | 91 +++++++++++++++++++ .../e2e-playwright-tests/type/testOutput.bal | 3 +- .../e2e-playwright-tests/type/type.spec.ts | 52 ++++++++++- 9 files changed, 177 insertions(+), 12 deletions(-) diff --git a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts index b256f75286f..f0a58041930 100644 --- a/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts +++ b/workspaces/ballerina/ballerina-core/src/interfaces/extended-lang-client.ts @@ -1210,6 +1210,7 @@ export interface Member { defaultValue?: string; optional?: boolean; imports?: Imports; + readonly?: boolean; } export interface GetGraphqlTypeRequest { diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx index b6b4f1904a1..d7869842e81 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/AdvancedOptions.tsx @@ -7,6 +7,14 @@ interface AdvancedOptionsProps { onChange: (type: Type) => void; } +enum TypeNodeKind { + RECORD = "RECORD", + ENUM = "ENUM", + CLASS = "CLASS", + UNION = "UNION", + ARRAY = "ARRAY" +} + export function AdvancedOptions({ type, onChange }: AdvancedOptionsProps) { const [isExpanded, setIsExpanded] = useState(false); @@ -28,14 +36,15 @@ export function AdvancedOptions({ type, onChange }: AdvancedOptionsProps) { Advanced Options {isExpanded && ( - <> + {type.codedata.node===TypeNodeKind.RECORD && { onChange({ ...type, allowAdditionalFields: checked }); }} - /> + />} = (props) => { + { + // Match the same pattern used in the working checkbox + onChange({ + ...member, + readonly: checked + } + ); + }} + /> - )} + )} {isRecord(member.type) && typeof member.type !== 'string' && (
+ + ); case TypeKind.CLASS: return ( @@ -447,10 +450,13 @@ export function TypeCreatorTab(props: TypeCreatorTabProps) { ); case TypeKind.ARRAY: return ( + <> + + ); default: return
Editor for {selectedTypeKind} type is not implemented yet
; diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx index cac5ba8d7fc..25a654aa5fa 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/TypeEditor.tsx @@ -80,11 +80,7 @@ export function TypeEditor(props: TypeEditorProps) { readonly: false, label: "" }, - properties: { - isReadOnly: { - value: "false", - }, - }, + properties: {}, codedata: { node: "RECORD" as TypeNodeKind }, diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts index d4d64a2b899..70c0186476c 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/test.list.ts @@ -89,8 +89,6 @@ test.describe(dataMapperArtifact); // TODO: Fix this test test.describe(typeDiagramArtifact); // TODO: Fix this test test.describe(connectionArtifact); test.describe(configuration); // TODO: Fix this test - -test.describe(configuration); test.describe(typeTest); test.afterAll(async () => { diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts index fef11ddb937..44db6ed9443 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts @@ -256,4 +256,95 @@ export class TypeEditorUtils { return form; } + + /** + * Toggle field options by clicking the chevron icon + * @param fieldIndex Index of the field to toggle (default is 0 for the first field) + */ + async toggleFieldOptionsByChevron(fieldIndex: number = 0): Promise { + // Find all field rows + const fieldRows = this.webView.locator('div[style*="display: flex"][style*="gap: 8px"][style*="align-items: start"]'); + const targetRow = fieldRows.nth(fieldIndex); + + // Find the element with the chevron icon + const chevronIcon = targetRow.locator('i.codicon.codicon-chevron-right, i.codicon.codicon-chevron-down'); + + try { + await chevronIcon.waitFor({ state: 'visible', timeout: 3000 }); + + // Scroll and force click + await chevronIcon.scrollIntoViewIfNeeded(); + await chevronIcon.click({ force: true }); + console.log('Clicked chevron for field', fieldIndex); + + + await this.page.waitForTimeout(300); + } catch (error) { + throw new Error(`Could not click chevron icon at field index ${fieldIndex}: ${error}`); + } + } + + + /** + * Toggle any dropdown/collapsible section by text + */ + async toggleDropdown(dropdownText: string, waitTime: number = 500): Promise { + const dropdownToggle = this.webView.locator(`text=${dropdownText}`); + await this.waitForElement(dropdownToggle); + await dropdownToggle.click(); + + // Wait for animation to complete + await this.page.waitForTimeout(waitTime); + } + + /** + * Set any checkbox by its aria-label or name + */ + async setCheckbox(checkboxName: string, checked: boolean): Promise { + const checkbox = this.webView.getByRole('checkbox', { name: checkboxName }); + console.log(`Setting checkbox "${checkboxName}" to ${checked}`); + await this.waitForElement(checkbox); + + const ariaChecked = await checkbox.getAttribute('aria-checked'); + const isCurrentlyChecked = ariaChecked === 'true'; + + if (isCurrentlyChecked !== checked) { + await checkbox.click(); + } + } + + /** + * Get checkbox state by its name + */ + async getCheckboxState(checkboxName: string): Promise { + const checkbox = this.webView.getByRole('checkbox', { name: checkboxName }); + await this.waitForElement(checkbox); + + const ariaChecked = await checkbox.getAttribute('aria-checked'); + return ariaChecked === 'true'; + } + + /** + * Verify multiple checkbox states at once + */ + async verifyCheckboxStates( expectedStates: Record): Promise { + const errors: string[] = []; + + for (const [checkboxName, expectedState] of Object.entries(expectedStates)) { + try { + const actualState = await this.getCheckboxState(checkboxName); + + if (actualState !== expectedState) { + errors.push(`${checkboxName}: expected ${expectedState}, got ${actualState}`); + } + } catch (error) { + errors.push(`${checkboxName}: checkbox not found or not accessible`); + } + } + + if (errors.length > 0) { + throw new Error(`Checkbox verification failed:\n${errors.join('\n')}`); + } + } + } diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal index d31bc73a0a1..431dc20f900 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal @@ -7,9 +7,10 @@ enum Role1 { type Id1 int|string; -type Employee1 record {| +type Employee1 readonly & record {| Id1 id; Role1 role; + readonly string name; |}; service class Project1 { diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts index 0efcc63a7f2..45acc9e053d 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts @@ -80,6 +80,56 @@ export default function createTests() { await typeUtils.saveAndWait(editForm); await typeUtils.verifyTypeLink(recordName, 'role', enumName); + // Add name field to Employee record + await typeUtils.editType(recordName); + await typeUtils.addRecordField('name', 'string' ); + + // Toggle drop down + await typeUtils.toggleFieldOptionsByChevron(2); + await typeUtils.setCheckbox('Readonly', true); + await typeUtils.verifyCheckboxStates( + { + 'Readonly': true, + } + ); + + + // Test Advanced Options functionality + console.log('Expanding Advanced Options...'); + await typeUtils.toggleDropdown('Advanced Options'); + + console.log('Testing Allow Additional Fields checkbox...'); + await typeUtils.setCheckbox('Allow Additional Fields', true); + await typeUtils.verifyCheckboxStates( + { + 'Allow Additional Fields': true, + 'Is Readonly Type': false + } + + ); + + + console.log('Testing Is Readonly Type checkbox...'); + await typeUtils.setCheckbox('Is Readonly Type', true); + await typeUtils.verifyCheckboxStates( + { + 'Allow Additional Fields': true, + 'Is Readonly Type': true + } + ); + + console.log('Testing unchecking Allow Additional Fields...'); + await typeUtils.setCheckbox('Allow Additional Fields', false); + await typeUtils.verifyCheckboxStates( + { + 'Allow Additional Fields': false, + 'Is Readonly Type': true + } + ); + + await typeUtils.saveAndWait(recordForm); + await typeUtils.verifyTypeNodeExists(recordName); + // Create Service Class: Project await typeUtils.clickAddType(); const serviceClassName = `Project${testAttempt}`; @@ -92,7 +142,7 @@ export default function createTests() { // Verify the generated types.bal matches testOutput.bal const expectedFilePath = path.join(__dirname, 'testOutput.bal'); - await verifyGeneratedSource('types.bal', expectedFilePath); + await verifyGeneratedSource('types.bal', expectedFilePath); }); }); From bffe2864d00a247f41d78b26ec2aaf1090ad494f Mon Sep 17 00:00:00 2001 From: samithkavishke Date: Sun, 20 Jul 2025 13:34:10 +0530 Subject: [PATCH 003/602] Modify e2e test case --- .../src/TypeEditor/FieldEditor.tsx | 3 +- .../type/TypeEditorUtils.ts | 9 ++-- .../e2e-playwright-tests/type/testOutput.bal | 6 ++- .../e2e-playwright-tests/type/type.spec.ts | 54 ++++++++----------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/workspaces/ballerina/type-editor/src/TypeEditor/FieldEditor.tsx b/workspaces/ballerina/type-editor/src/TypeEditor/FieldEditor.tsx index 0dc3fdf6d63..d159cc4226e 100644 --- a/workspaces/ballerina/type-editor/src/TypeEditor/FieldEditor.tsx +++ b/workspaces/ballerina/type-editor/src/TypeEditor/FieldEditor.tsx @@ -142,6 +142,7 @@ export const FieldEditor: React.FC = (props) => { <>
setPanelOpened(!panelOpened)} > @@ -197,7 +198,7 @@ export const FieldEditor: React.FC = (props) => { }} /> - )} + )} {isRecord(member.type) && typeof member.type !== 'string' && (
{ // Find all field rows - const fieldRows = this.webView.locator('div[style*="display: flex"][style*="gap: 8px"][style*="align-items: start"]'); - const targetRow = fieldRows.nth(fieldIndex); + + // const fieldRows = this.webView.locator('div[style*="display: flex"][style*="gap: 8px"][style*="align-items: start"]'); + const chevronIcons = this.webView.locator('[data-testid="field-expand-btn"]'); + // await this.page.pause(); + const chevronIcon = chevronIcons.nth(fieldIndex); // Find the element with the chevron icon - const chevronIcon = targetRow.locator('i.codicon.codicon-chevron-right, i.codicon.codicon-chevron-down'); + // const chevronIcon = targetRow.locator('i.codicon.codicon-chevron-right, i.codicon.codicon-chevron-down'); try { await chevronIcon.waitFor({ state: 'visible', timeout: 3000 }); diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal index 431dc20f900..a553a4e5bdc 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/testOutput.bal @@ -6,7 +6,11 @@ enum Role1 { } type Id1 int|string; - +type Organization1 record { + Id1 id; + string name; + string location; +}; type Employee1 readonly & record {| Id1 id; Role1 role; diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts index 45acc9e053d..040cfa52d65 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/type.spec.ts @@ -61,6 +61,26 @@ export default function createTests() { await typeUtils.saveAndWait(unionForm); await typeUtils.verifyTypeNodeExists(unionName); + // RECORD: Organization + await typeUtils.clickAddType(); + const organizationName = `Organization${testAttempt}`; + const organizationForm = await typeUtils.createRecordType(organizationName, [ + { name: 'id', type: unionName }, + { name: 'name', type: 'string' }, + { name: 'location', type: 'string' } + ]); + + // Test Advanced Options functionality + console.log('Expanding Advanced Options...'); + await typeUtils.toggleDropdown('Advanced Options'); + + console.log('Testing Allow Additional Fields checkbox...'); + await typeUtils.setCheckbox('Allow Additional Fields', true); + await typeUtils.saveAndWait(organizationForm); + await typeUtils.verifyTypeNodeExists(organizationName); + await typeUtils.verifyTypeLink(organizationName, 'id', unionName); + + // RECORD: Employee (initially with just id field) await typeUtils.clickAddType(); const recordName = `Employee${testAttempt}`; @@ -87,45 +107,13 @@ export default function createTests() { // Toggle drop down await typeUtils.toggleFieldOptionsByChevron(2); await typeUtils.setCheckbox('Readonly', true); - await typeUtils.verifyCheckboxStates( - { - 'Readonly': true, - } - ); - // Test Advanced Options functionality console.log('Expanding Advanced Options...'); - await typeUtils.toggleDropdown('Advanced Options'); - - console.log('Testing Allow Additional Fields checkbox...'); - await typeUtils.setCheckbox('Allow Additional Fields', true); - await typeUtils.verifyCheckboxStates( - { - 'Allow Additional Fields': true, - 'Is Readonly Type': false - } - - ); - + await typeUtils.toggleDropdown('Advanced Options'); console.log('Testing Is Readonly Type checkbox...'); await typeUtils.setCheckbox('Is Readonly Type', true); - await typeUtils.verifyCheckboxStates( - { - 'Allow Additional Fields': true, - 'Is Readonly Type': true - } - ); - - console.log('Testing unchecking Allow Additional Fields...'); - await typeUtils.setCheckbox('Allow Additional Fields', false); - await typeUtils.verifyCheckboxStates( - { - 'Allow Additional Fields': false, - 'Is Readonly Type': true - } - ); await typeUtils.saveAndWait(recordForm); await typeUtils.verifyTypeNodeExists(recordName); From b47fa5a60704e80149ce227e7d80d0cff196b35e Mon Sep 17 00:00:00 2001 From: samithkavishke Date: Mon, 21 Jul 2025 10:15:20 +0530 Subject: [PATCH 004/602] Remove unwanted code --- .../src/test/e2e-playwright-tests/type/TypeEditorUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts index 16dd78a7223..9be93bf8719 100644 --- a/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts +++ b/workspaces/bi/bi-extension/src/test/e2e-playwright-tests/type/TypeEditorUtils.ts @@ -264,14 +264,9 @@ export class TypeEditorUtils { async toggleFieldOptionsByChevron(fieldIndex: number = 0): Promise { // Find all field rows - // const fieldRows = this.webView.locator('div[style*="display: flex"][style*="gap: 8px"][style*="align-items: start"]'); const chevronIcons = this.webView.locator('[data-testid="field-expand-btn"]'); - // await this.page.pause(); const chevronIcon = chevronIcons.nth(fieldIndex); - // Find the element with the chevron icon - // const chevronIcon = targetRow.locator('i.codicon.codicon-chevron-right, i.codicon.codicon-chevron-down'); - try { await chevronIcon.waitFor({ state: 'visible', timeout: 3000 }); From 20c378d35c85b5229237b64c269b7a907150b7a3 Mon Sep 17 00:00:00 2001 From: Ravindu Wegiriya Date: Mon, 21 Jul 2025 14:15:33 +0530 Subject: [PATCH 005/602] Add Support for Synapse Expressions in Unit Test Framework --- .../mi-core/src/rpc-types/mi-diagram/types.ts | 1 + .../Form/FormExpressionField/index.tsx | 25 +- .../src/components/Form/FormGenerator.tsx | 4 + .../components/Form/FormTokenEditor/index.tsx | 1 + .../Form/HelperPane/CategoryPage.tsx | 60 ++- .../Form/HelperPane/ConfigsPage.tsx | 5 +- .../Form/HelperPane/FunctionsPage.tsx | 6 +- .../Form/HelperPane/HeadersPage.tsx | 25 +- .../components/Form/HelperPane/ParamsPage.tsx | 24 +- .../Form/HelperPane/PayloadPage.tsx | 24 +- .../Form/HelperPane/PropertiesPage.tsx | 24 +- .../Form/HelperPane/VariablesPage.tsx | 25 +- .../src/components/Form/HelperPane/index.tsx | 20 +- .../mi-diagram/src/components/Form/index.tsx | 1 + .../src/views/Forms/Tests/TestCaseForm.tsx | 365 ++++++++++++------ .../src/views/Forms/Tests/TestSuiteForm.tsx | 3 +- 16 files changed, 448 insertions(+), 165 deletions(-) diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts index bd195fc1a31..237a333b9c8 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts @@ -2111,6 +2111,7 @@ export interface GenerateConnectorResponse { export interface GetHelperPaneInfoRequest { documentUri: string; position: Position; + needLastMediator?: boolean; } export type GetHelperPaneInfoResponse = HelperPaneData; diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx index 8ed516bcd22..40d03edb57b 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormExpressionField/index.tsx @@ -91,6 +91,8 @@ type FormExpressionFieldProps = { nodeRange: Range; canChange: boolean; supportsAIValues?: boolean; + artifactPath?: string; + artifactType?: string; onChange: (value: FormExpressionFieldValue) => void; onFocus?: (e?: any) => void | Promise; onBlur?: (e?: any) => void | Promise; @@ -173,6 +175,8 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { nodeRange, canChange, supportsAIValues, + artifactPath, + artifactType, onChange, onCancel, errorMsg, @@ -246,6 +250,10 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { }; const handleChangeHelperPaneState = (isOpen: boolean) => { + // Prevent opening helper pane if artifact type is API + if (isOpen && artifactType === "API") { + return; + } setIsHelperPaneOpen(isOpen); } @@ -269,12 +277,18 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { nodeRange?.start == nodeRange?.end ? nodeRange.start : { line: nodeRange.start.line, character: nodeRange.start.character + 1 } : undefined; - + + // Don't return helper pane if artifact type is API + if (artifactType === "API") { + return null; + } + return getHelperPane( position, 'default', () => handleChangeHelperPaneState(false), - handleHelperPaneChange + handleHelperPaneChange, + artifactPath ); }, [expressionRef.current, handleChangeHelperPaneState, nodeRange, getHelperPane]); @@ -347,7 +361,7 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { } ] : []), - ...(value.isExpression + ...(value.isExpression && artifactType !== "API" ? [ { tooltip: 'Open Helper Pane', @@ -368,7 +382,8 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { expressionRef.current, handleChangeHelperPaneState, openExpressionEditor, - onChange + onChange, + artifactType ]); const expressionValue = useMemo(() => { @@ -403,7 +418,7 @@ export const FormExpressionField = (params: FormExpressionFieldProps) => { onCancel={handleCancel} getExpressionEditorIcon={handleGetExpressionEditorIcon} actionButtons={actionButtons} - {...(value.isExpression && { + {...(value.isExpression && artifactType !== "API" && { completions, isHelperPaneOpen, changeHelperPaneState: handleChangeHelperPaneState, diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx index b606ab23091..bb6e481ebb6 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx @@ -129,6 +129,8 @@ export interface Element { expressionType?: 'xpath/jsonPath' | 'synapse'; supportsAIValues?: boolean; rowCount?: number; + artifactPath?: string; + artifactType?: string; } interface ExpressionValueWithSetter { @@ -583,6 +585,8 @@ export function FormGenerator(props: FormGeneratorProps) { canChange={element.inputType !== 'expression'} supportsAIValues={element.supportsAIValues} errorMsg={errorMsg} + artifactPath={element.artifactPath} + artifactType={element.artifactType} openExpressionEditor={(value, setValue) => { setCurrentExpressionValue({ value, setValue }); setExpressionEditorField(name); diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx index 78b37da89f6..2282e89e665 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx @@ -107,6 +107,7 @@ export const FormTokenEditor = ({ 'default', () => handleChangeHelperPaneState(false), onChange, + undefined, // artifactPath - not available in FormTokenEditorte addFunction, { width: 'auto', border: '1px solid var(--dropdown-border)' } ); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx index dea73526b32..9a3b90ac9af 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx @@ -16,15 +16,17 @@ * under the License. */ -import React from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Position } from 'vscode-languageserver-types'; import { Divider, HelperPane } from '@wso2/ui-toolkit'; import { FunctionsPage } from './FunctionsPage'; import { ConfigsPage } from './ConfigsPage'; import { PAGE, Page } from './index'; +import { useVisualizerContext } from '@wso2/mi-rpc-client'; type PanelPageProps = { setCurrentPage: (page: Page) => void; + helperPaneResponse?: any; }; type CategoryPageProps = { @@ -33,9 +35,10 @@ type CategoryPageProps = { onClose: () => void; onChange: (value: string) => void; addFunction?: (value: string) => void; + artifactPath?: string; }; -const DataPanel = ({ setCurrentPage }: PanelPageProps) => { +const DataPanel = ({ setCurrentPage, helperPaneResponse }: PanelPageProps) => { return ( <> setCurrentPage(PAGE.PAYLOAD)} /> @@ -52,24 +55,63 @@ export const CategoryPage = ({ position, setCurrentPage, onChange, - addFunction + addFunction, + artifactPath }: CategoryPageProps) => { + const { rpcClient } = useVisualizerContext(); + const [helperPaneResponse, setHelperPaneResponse] = useState(null); + + const getHelperPaneInfo = useCallback(() => { + rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } + rpcClient + .getMiDiagramRpcClient() + .getHelperPaneInfo(requestBody) + .then((response) => { + console.log('Response from getHelperPaneInfo:', response); + setHelperPaneResponse(response); + }); + }); + }, [rpcClient, position, artifactPath]); + + useEffect(() => { + getHelperPaneInfo(); + }, [getHelperPaneInfo]); + return ( <> - + {helperPaneResponse?.configs && helperPaneResponse.configs.length > 0 && ( + + )} - + - - - - + + {helperPaneResponse?.configs && helperPaneResponse.configs.length > 0 && ( + + + + )} diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx index 3ea69413d77..f58e415a1e6 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ConfigsPage.tsx @@ -53,6 +53,7 @@ const ButtonPanel = styled.div` type ConfigsPageProps = { position: Position; onChange: (value: string) => void; + artifactPath?: string; }; /* Validation schema for the config form */ @@ -64,7 +65,7 @@ const schema = yup.object({ type ConfigFormData = yup.InferType; -export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { +export const ConfigsPage = ({ position, onChange, artifactPath }: ConfigsPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); const [isLoading, setIsLoading] = useState(false); @@ -88,7 +89,7 @@ export const ConfigsPage = ({ position, onChange }: ConfigsPageProps) => { rpcClient .getMiDiagramRpcClient() .getHelperPaneInfo({ - documentUri: machineView.documentUri, + documentUri: artifactPath ? artifactPath : machineView.documentUri, position: position, }) .then((response) => { diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/FunctionsPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/FunctionsPage.tsx index 74d81ac856a..4960b2570d5 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/FunctionsPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/FunctionsPage.tsx @@ -28,12 +28,14 @@ type FunctionsPageProps = { position: Position; onChange: (value: string) => void; addFunction?: (value: string) => void; + artifactPath?: string; }; export const FunctionsPage = ({ position, onChange, - addFunction + addFunction, + artifactPath }: FunctionsPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -47,7 +49,7 @@ export const FunctionsPage = ({ setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { rpcClient.getMiDiagramRpcClient().getHelperPaneInfo({ - documentUri: machineView.documentUri, + documentUri: artifactPath ? artifactPath : machineView.documentUri, position: position, }) .then((response) => { diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx index c22493be62d..1240814d740 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx @@ -30,13 +30,15 @@ type HeadersPageProps = { setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; + artifactPath?: string; }; export const HeadersPage = ({ position, setCurrentPage, onClose, - onChange + onChange, + artifactPath }: HeadersPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -49,12 +51,25 @@ export const HeadersPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } + rpcClient .getMiDiagramRpcClient() - .getHelperPaneInfo({ - documentUri: machineView.documentUri, - position: position, - }) + .getHelperPaneInfo(requestBody) .then((response) => { if (response.headers?.length) { setHeaderInfo(response.headers); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx index ad3062b8a98..6fd3a943ebc 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx @@ -30,13 +30,15 @@ type ParamsPageProps = { setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; + artifactPath?: string; }; export const ParamsPage = ({ position, setCurrentPage, onClose, - onChange + onChange, + artifactPath }: ParamsPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -49,12 +51,24 @@ export const ParamsPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } rpcClient .getMiDiagramRpcClient() - .getHelperPaneInfo({ - documentUri: machineView.documentUri, - position: position, - }) + .getHelperPaneInfo(requestBody) .then((response) => { if (response.params?.length) { setParamInfo(response.params); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx index 1fed161897d..6a4b1a4a2c0 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx @@ -37,13 +37,15 @@ type PayloadPageProps = { setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; + artifactPath?: string; }; export const PayloadPage = ({ position, setCurrentPage, onClose, - onChange + onChange, + artifactPath }: PayloadPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -75,12 +77,24 @@ export const PayloadPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } rpcClient .getMiDiagramRpcClient() - .getHelperPaneInfo({ - documentUri: machineView.documentUri, - position: position, - }) + .getHelperPaneInfo(requestBody) .then((response) => { if (response.payload?.length) { setPayloadInfo(response.payload); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx index eaf0ff901d4..92728a68476 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx @@ -30,13 +30,15 @@ type PropertiesPageProps = { setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; + artifactPath?: string; }; export const PropertiesPage = ({ position, setCurrentPage, onClose, - onChange + onChange, + artifactPath }: PropertiesPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -49,12 +51,24 @@ export const PropertiesPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } rpcClient .getMiDiagramRpcClient() - .getHelperPaneInfo({ - documentUri: machineView.documentUri, - position: position, - }) + .getHelperPaneInfo(requestBody) .then((response) => { if (response.properties?.length) { setPropertiesInfo(response.properties); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx index 245bbfa33bf..83b8fd737b5 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx @@ -30,13 +30,15 @@ type VariablesPageProps = { setCurrentPage: (page: Page) => void; onClose: () => void; onChange: (value: string) => void; + artifactPath?: string; }; export const VariablesPage = ({ position, setCurrentPage, onClose, - onChange + onChange, + artifactPath }: VariablesPageProps) => { const { rpcClient } = useVisualizerContext(); const firstRender = useRef(true); @@ -49,12 +51,25 @@ export const VariablesPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { + let requestBody; + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri.includes('src/test/')) { + requestBody = { + documentUri: documentUri, + position: {line: 0, character: 0 }, + needLastMediator: true + } + }else{ + requestBody = { + documentUri: documentUri, + position: position + } + } + rpcClient .getMiDiagramRpcClient() - .getHelperPaneInfo({ - documentUri: machineView.documentUri, - position: position, - }) + .getHelperPaneInfo(requestBody) .then((response) => { if (response.variables?.length) { setVariableInfo(response.variables); diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx index 07918d08039..651aadd4579 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx @@ -32,6 +32,7 @@ export type HelperPaneProps = { onClose: () => void; onChange: (value: string) => void; addFunction?: (value: string) => void; + artifactPath?: string; sx?: CSSProperties; }; @@ -46,9 +47,9 @@ export const PAGE = { export type Page = (typeof PAGE)[keyof typeof PAGE]; -const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFunction }: HelperPaneProps) => { +const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFunction, artifactPath }: HelperPaneProps) => { const [currentPage, setCurrentPage] = useState(PAGE.CATEGORY); - + return ( {currentPage === PAGE.CATEGORY && ( @@ -58,6 +59,7 @@ const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFu onClose={onClose} onChange={onChange} addFunction={addFunction} + artifactPath={artifactPath} /> )} {currentPage === PAGE.PAYLOAD && ( @@ -66,6 +68,7 @@ const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFu setCurrentPage={setCurrentPage} onClose={onClose} onChange={onChange} + artifactPath={artifactPath} /> )} {currentPage === PAGE.VARIABLES && ( @@ -74,6 +77,7 @@ const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFu setCurrentPage={setCurrentPage} onClose={onClose} onChange={onChange} + artifactPath={artifactPath} /> )} {currentPage === PAGE.HEADERS && ( @@ -82,10 +86,17 @@ const HelperPaneEl = ({ position, helperPaneHeight, sx, onClose, onChange, addFu setCurrentPage={setCurrentPage} onClose={onClose} onChange={onChange} + artifactPath={artifactPath} /> )} {currentPage === PAGE.PARAMS && ( - + )} {currentPage === PAGE.PROPERTIES && ( )} @@ -104,6 +116,7 @@ export const getHelperPane = ( helperPaneHeight: HelperPaneHeight, onClose: () => void, onChange: (value: string) => void, + artifactPath?: string, addFunction?: (value: string) => void, sx?: CSSProperties ) => { @@ -115,6 +128,7 @@ export const getHelperPane = ( onClose={onClose} onChange={onChange} addFunction={addFunction} + artifactPath={artifactPath} /> ); }; diff --git a/workspaces/mi/mi-diagram/src/components/Form/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/index.tsx index 8c32371d492..b5228053905 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/index.tsx @@ -22,3 +22,4 @@ export * from './ExpressionField/ExpressionInput'; export * from './CodeTextArea'; export * from './FormExpressionField'; export * from './FormTokenEditor'; +export { default as ParameterManager } from './GigaParamManager/ParameterManager'; diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx index fc020b90653..e416f16e05b 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx @@ -21,11 +21,13 @@ import { ParamManager, ParamValue, getParamManagerFromValues, getParamManagerVal import { useVisualizerContext } from "@wso2/mi-rpc-client"; import { Button, ComponentCard, Dropdown, FormActions, FormView, ProgressIndicator, TextArea, TextField, Typography } from "@wso2/ui-toolkit"; import { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { TagRange } from '@wso2/mi-syntax-tree/lib/src'; import * as yup from "yup"; import { getTestCaseXML } from "../../../utils/template-engine/mustache-templates/TestSuite"; - +import { ParameterManager } from "@wso2/mi-diagram"; +import { compareVersions } from "@wso2/mi-diagram/lib/utils/commons"; +import { getProjectRuntimeVersion } from "../../AIPanel/utils"; export enum TestSuiteType { API = "API", SEQUENCE = "Sequence" @@ -67,6 +69,137 @@ export function TestCaseForm(props: TestCaseFormProps) { const { rpcClient } = useVisualizerContext(); const [isLoaded, setIsLoaded] = useState(false); + const [inputProperties, setInputProperties] = useState([]); + const [assertions, setAssertions] = useState([]); + + // Form data configurations for ParameterManager + const inputPropertiesFormData = { + elements: [ + { + type: "attribute", + value: { + name: "propertyName", + displayName: "Property Name", + inputType: "string", + required: true, + helpTip: "", + }, + }, + { + type: "attribute", + value: { + name: "propertyScope", + displayName: "Property Scope", + inputType: "combo", + required: true, + comboValues: ["default", "transport", "axis2", "axis2-client"], + defaultValue: "default", + helpTip: "", + }, + }, + { + type: "attribute", + value: { + name: "propertyValue", + displayName: "Property Value", + inputType: "string", + required: true, + helpTip: "", + }, + } + ], + tableKey: 'propertyName', + tableValue: 'propertyValue', + addParamText: 'Add Property', + }; + + // Helper function to create assertions form data based on version + const createAssertionsFormData = (useStringOrExpression: boolean, testSuiteType: TestSuiteType) => ({ + elements: [ + { + type: "attribute", + value: { + name: "assertionType", + displayName: "Assertion Type", + inputType: "combo", + required: false, + comboValues: ["Assert Equals", "Assert Not Null"], + defaultValue: "Assert Equals", + helpTip: "", + }, + }, + { + type: "attribute", + value: { + name: "actualExpressionType", + displayName: "Assertion", + inputType: "combo", + required: true, + comboValues: testSuiteType === TestSuiteType.SEQUENCE ? ["Payload", "Custom"] : ["Payload", "Status Code", "Transport Header", "HTTP Version"], + defaultValue: "Payload", + helpTip: "", + }, + }, + { + type: "attribute", + value: { + name: "transportHeader", + displayName: "Transport Header", + inputType: "string", + required: true, + helpTip: "", + enableCondition: [ + { + actualExpressionType: "Transport Header", + } + ] + }, + }, + { + type: "attribute", + value: { + name: "actualExpression", + displayName: "Expression", + inputType: useStringOrExpression ? "stringOrExpression" : "string", + required: true, + helpTip: "", + artifactPath: props.filePath, + artifactType: props.testSuiteType, + enableCondition: [ + { + actualExpressionType: "Custom", + } + ] + }, + }, + { + type: "attribute", + value: { + name: "expectedValue", + displayName: "Expected Value", + inputType: "codeTextArea", + required: false, + helpTip: "", + }, + }, + { + type: "attribute", + value: { + name: "errorMessage", + displayName: "Error Message", + inputType: "string", + required: true, + helpTip: "", + }, + } + ], + tableKey: 'assertionType', + tableValue: 'actualExpressionType', + addParamText: 'Add Assertion', + }); + + const [assertionsFormData, setAssertionsFormData] = useState(createAssertionsFormData(true, props.testSuiteType)); + const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']; const requestProtocols = ['http', 'https']; const isUpdate = !!props.testCase; @@ -83,13 +216,10 @@ export function TestCaseForm(props: TestCaseFormProps) { requestMethod: !isSequence ? yup.string().oneOf(requestMethods).required("Resource method is required") : yup.string(), requestProtocol: !isSequence ? yup.string().oneOf(requestProtocols).required("Resource protocol is required") : yup.string(), payload: yup.string(), - properties: yup.mixed(), }), - assertions: yup.mixed(), }); const { - control, handleSubmit, formState: { errors }, register, @@ -100,60 +230,23 @@ export function TestCaseForm(props: TestCaseFormProps) { }); useEffect(() => { - (async () => { - const inputPropertiesFields = [ - { - "type": "TextField", - "label": "Property Name", - "defaultValue": "", - "isRequired": true - }, - { - "type": "Dropdown", - "label": "Property Scope", - "defaultValue": "default", - "isRequired": true, - "values": ["default", "transport", "axis2", "axis2-client"] - }, - { - "type": "TextField", - "label": "Property Value", - "defaultValue": "", - "isRequired": true - } - ]; + const checkRuntimeVersion = async () => { + try { + const runtimeVersion = await getProjectRuntimeVersion(rpcClient); + const useStringOrExpression = runtimeVersion && compareVersions(runtimeVersion, "4.4.0") >= 0; + setAssertionsFormData(createAssertionsFormData(useStringOrExpression, props.testSuiteType)); + } catch (error) { + console.error('Error getting runtime version:', error); + // Fallback to default configuration (stringOrExpression) + setAssertionsFormData(createAssertionsFormData(true, props.testSuiteType)); + } + }; - const assertionsFields = [ - { - type: "Dropdown", - label: "Assertion Type", - defaultValue: "Assert Equals", - isRequired: false, - values: ["Assert Equals", "Assert Not Null"] - }, - { - "type": "TextField", - "label": "Actual Expression", - "defaultValue": "", - "isRequired": true - }, - { - "type": "TextArea", - "label": "Expected Value", - "defaultValue": "", - "isRequired": false, - "enableCondition": [ - { 0: "Assert Equals" } - ] - }, - { - "type": "TextField", - "label": "Error Message", - "defaultValue": "", - "isRequired": true, - } - ]; + checkRuntimeVersion(); + }, [rpcClient]); + useEffect(() => { + (async () => { if (isUpdate) { const testCase = structuredClone(props?.testCase); if (testCase.input?.payload?.startsWith(" ({ + propertyName: prop[0], + propertyScope: prop[1] || "default", + propertyValue: prop[2] + })) : []; + // Helper function to determine actualExpressionType from actualExpression value + const getActualExpressionType = (actualExpression: string): string => { + // if actualExpression starts with $trp, it's a transport header + if (actualExpression.startsWith("$trp:")) { + return "Transport Header"; + } + switch (actualExpression) { + case "$body": + return "Payload"; + case "$statusCode": + return "Status Code"; + case "$httpVersion": + return "HTTP Version"; + default: + return "Custom"; + } + }; + + // Convert assertions to new format + const assertionsData = testCase.assertions ? + testCase.assertions.map((assertion: string[]) => ({ + assertionType: assertion[0], + actualExpressionType: getActualExpressionType(assertion[1]), + transportHeader: assertion[1]?.startsWith("$trp:") ? assertion[1].substring(5) : undefined, + actualExpression: assertion[1], + expectedValue: assertion[2] || "", + errorMessage: assertion[3] || "", + })) : []; + + setInputProperties(properties); + setAssertions(assertionsData); + reset({ - ...testCase, - assertions: { - paramValues: testCase.assertions ? getParamManagerFromValues(testCase.assertions, 0) : [], - paramFields: assertionsFields - }, + name: testCase.name, + input: { + requestPath: testCase.input?.requestPath, + requestMethod: testCase.input?.requestMethod, + requestProtocol: testCase.input?.requestProtocol, + payload: testCase.input?.payload || "" + } }); setIsLoaded(true); return; @@ -191,17 +321,11 @@ export function TestCaseForm(props: TestCaseFormProps) { requestPath: !isSequence ? "/" : undefined, requestMethod: !isSequence ? "GET" : undefined, requestProtocol: !isSequence ? "http" : undefined, - payload: "", - properties: { - paramValues: [], - paramFields: inputPropertiesFields - }, - }, - assertions: { - paramValues: [], - paramFields: assertionsFields - }, + payload: "" + } }); + setInputProperties([]); + setAssertions([]); setIsLoaded(true); })(); }, [props.filePath, props.testCase]); @@ -219,8 +343,45 @@ export function TestCaseForm(props: TestCaseFormProps) { }; const submitForm = async (values: any) => { - values.input.properties = getParamManagerValues(values.input.properties); - values.assertions = getParamManagerValues(values.assertions); + // Convert properties back to array format + values.input.properties = inputProperties.map(prop => [ + prop.propertyName, + prop.propertyScope, + prop.propertyValue + ]); + + // Convert assertions back to array format + values.assertions = assertions.map(assertion => { + // Handle actualExpression field - convert from JSON object to string + let actualExpression = assertion.actualExpression; + if(actualExpression === undefined){ + switch (assertion.actualExpressionType) { + case "Payload": + actualExpression = "$body"; + break; + case "Status Code": + actualExpression = "$statusCode"; + break; + case "Transport Header": + const header = assertion.transportHeader; + actualExpression = "$trp:" + header; + break; + case "HTTP Version": + actualExpression = "$httpVersion"; + break; + } + } + if (typeof actualExpression === 'object' && actualExpression !== null) { + actualExpression = actualExpression.value; + } + + return [ + assertion.assertionType, + actualExpression, + assertion.expectedValue, + assertion.errorMessage, + ]; + }); if (props.onSubmit) { delete values.filePath; @@ -285,26 +446,10 @@ export function TestCaseForm(props: TestCaseFormProps) {
Editing of the properties of an input - ( - { - values.paramValues = values.paramValues.map((param: any) => { - const property: ParamValue[] = param.paramValues; - param.key = property[0].value; - param.value = property[2].value; - param.icon = 'query'; - return param; - }); - onChange(values); - }} - /> - )} + @@ -313,26 +458,10 @@ export function TestCaseForm(props: TestCaseFormProps) { Assertions Editing of the properties of an assertion - ( - { - values.paramValues = values.paramValues.map((param: any) => { - const property: ParamValue[] = param.paramValues; - param.key = property[0].value; - param.value = property[1].value; - param.icon = 'query'; - return param; - }); - onChange(values); - }} - /> - )} + diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestSuiteForm.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestSuiteForm.tsx index d552248468a..d120942300d 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestSuiteForm.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestSuiteForm.tsx @@ -803,7 +803,8 @@ export function TestSuiteForm(props: TestSuiteFormProps) { setShowAddTestCase(false); }; const availableTestCases = testCases.map((testCase) => testCase.name); - return + const artifactType = getValues('artifactType') as TestSuiteType; + return } if (showAddMockService) { From 511c2021dcc7b814f83dd4fbb6015530e7f21d77 Mon Sep 17 00:00:00 2001 From: Ravindu Wegiriya Date: Mon, 28 Jul 2025 12:05:04 +0530 Subject: [PATCH 006/602] Fix e2e tests --- .../test/e2e-playwright-tests/components/Form.ts | 4 ++-- .../e2e-playwright-tests/components/UnitTest.ts | 14 +++++++------- .../e2e-playwright-tests/unitTestSuite.spec.ts | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/Form.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/Form.ts index b2360630317..18824a1a15a 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/Form.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/Form.ts @@ -127,9 +127,9 @@ export class Form { case 'combo': { let parentDiv; if (data.additionalProps?.nthValue !== undefined) { - parentDiv = this.container.locator(`label:text("${key}")`).nth(data.additionalProps?.nthValue).locator('../../..'); + parentDiv = this.container.locator(`label:text-matches("${key}")`).nth(data.additionalProps?.nthValue).locator('../../..'); } else { - parentDiv = this.container.locator(`label:text("${key}")`).locator('../../..'); + parentDiv = this.container.locator(`label:text-matches("${key}")`).locator('../../..'); } await parentDiv.waitFor(); const input = parentDiv.locator('input[role="combobox"]'); diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/UnitTest.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/UnitTest.ts index ca1961ea51e..6fe8058406e 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/UnitTest.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/UnitTest.ts @@ -172,14 +172,14 @@ export class UnitTest { const propertiesParamManager = await form.getDefaultParamManager('Properties', 'Add Property', 'card-select-testCasePropertiesCard'); const propertiesForm = await propertiesParamManager.getAddNewForm(); await this.fillTestCasePropertyForm(propertiesForm, property); - await propertiesForm.submit('Save'); + await propertiesForm.submit('Add'); } console.log('Filling Test Case Assertions'); for (const assertion of testCase.assertions ?? []) { const assertionsParamManager = await form.getDefaultParamManager('Assertions', 'Add Assertion', 'card-select-testCaseAssertionsCard'); const assertionsForm = await assertionsParamManager.getAddNewForm(); await this.fillTestCaseAssertionForm(assertionsForm, assertion); - await assertionsForm.submit('Save'); + await assertionsForm.submit('Add'); } } @@ -219,8 +219,8 @@ export class UnitTest { type: 'input', value: property.name }, - 'Property Scope*': { - type: 'dropdown', + 'Property Scope': { + type: 'combo', value: property.scope }, 'Property Value*': { @@ -236,11 +236,11 @@ export class UnitTest { await form.fill({ values: { 'Assertion Type': { - type: 'dropdown', + type: 'combo', value: assertion.type }, - 'Actual Expression*': { - type: 'input', + '^Assertion$': { + type: 'combo', value: assertion.actualExpression }, 'Error Message*': { diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/unitTestSuite.spec.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/unitTestSuite.spec.ts index 3a65da85662..01ee62c4bf4 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/unitTestSuite.spec.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/unitTestSuite.spec.ts @@ -163,13 +163,13 @@ export default function createTests() { assertions: [ { type: 'Assert Equals', - actualExpression: 'var!=null', - expectedValue: 'true', + actualExpression: 'Payload', + expectedValue: '{ "key": "value" }', errorMessage: 'Assertion failed' }, { type: 'Assert Not Null', - actualExpression: 'var==null', + actualExpression: 'Status Code', errorMessage: 'Assertion failed' } ] From 2bab6a2ed0c37c5b05e91bf813235e3f9945e819 Mon Sep 17 00:00:00 2001 From: Ravindu Wegiriya Date: Tue, 29 Jul 2025 10:28:39 +0530 Subject: [PATCH 007/602] Add reviewed PR suggestions --- .../components/Form/FormTokenEditor/index.tsx | 2 +- .../Form/HelperPane/CategoryPage.tsx | 18 ++---------- .../Form/HelperPane/HeadersPage.tsx | 17 ++--------- .../components/Form/HelperPane/ParamsPage.tsx | 18 ++---------- .../Form/HelperPane/PayloadPage.tsx | 18 ++---------- .../Form/HelperPane/PropertiesPage.tsx | 18 ++---------- .../Form/HelperPane/VariablesPage.tsx | 17 ++--------- .../src/components/Form/HelperPane/index.tsx | 2 +- .../mi-diagram/src/components/Form/utils.tsx | 29 +++++++++++++++++++ .../src/views/Forms/Tests/TestCaseForm.tsx | 2 ++ 10 files changed, 49 insertions(+), 92 deletions(-) diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx index 2668cf450f7..01f499b343a 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormTokenEditor/index.tsx @@ -106,7 +106,7 @@ export const FormTokenEditor = ({ 'default', () => handleChangeHelperPaneState(false), onChange, - undefined, // artifactPath - not available in FormTokenEditorte + undefined, // artifactPath - not available in FormTokenEditor addFunction, { width: 'auto', border: '1px solid var(--dropdown-border)' }, height, diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx index 7b91ba1e543..9854029ae54 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/CategoryPage.tsx @@ -21,6 +21,7 @@ import { Position } from 'vscode-languageserver-types'; import { Divider, HelperPane } from '@wso2/ui-toolkit'; import { FunctionsPage } from './FunctionsPage'; import { ConfigsPage } from './ConfigsPage'; +import { createHelperPaneRequestBody } from '../utils'; import { PAGE, Page } from './index'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; @@ -65,21 +66,8 @@ export const CategoryPage = ({ const getHelperPaneInfo = useCallback(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); + rpcClient .getMiDiagramRpcClient() .getHelperPaneInfo(requestBody) diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx index 1240814d740..9b03c7da396 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/HeadersPage.tsx @@ -21,6 +21,7 @@ import { Position } from 'vscode-languageserver-types'; import { COMPLETION_ITEM_KIND, getIcon, HelperPane } from '@wso2/ui-toolkit'; import { HelperPaneCompletionItem } from '@wso2/mi-core'; import { filterHelperPaneCompletionItems, getHelperPaneCompletionItem } from '../FormExpressionField/utils'; +import { createHelperPaneRequestBody } from '../utils'; import { debounce } from 'lodash'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { PAGE, Page } from './index'; @@ -51,21 +52,7 @@ export const HeadersPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); rpcClient .getMiDiagramRpcClient() diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx index 6fd3a943ebc..8170bc666d1 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/ParamsPage.tsx @@ -23,6 +23,7 @@ import { HelperPaneCompletionItem } from '@wso2/mi-core'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { COMPLETION_ITEM_KIND, getIcon, HelperPane } from '@wso2/ui-toolkit'; import { filterHelperPaneCompletionItems, getHelperPaneCompletionItem } from '../FormExpressionField/utils'; +import { createHelperPaneRequestBody } from '../utils'; import { PAGE, Page } from './index'; type ParamsPageProps = { @@ -51,21 +52,8 @@ export const ParamsPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); + rpcClient .getMiDiagramRpcClient() .getHelperPaneInfo(requestBody) diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx index 6a4b1a4a2c0..f95afb02ab0 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PayloadPage.tsx @@ -24,6 +24,7 @@ import { HelperPaneCompletionItem } from '@wso2/mi-core'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { Alert, COMPLETION_ITEM_KIND, getIcon, HelperPane, Icon } from '@wso2/ui-toolkit'; import { filterHelperPaneCompletionItems, getHelperPaneCompletionItem } from '../FormExpressionField/utils'; +import { createHelperPaneRequestBody } from '../utils'; import { PAGE, Page } from './index'; const InfoMessage = styled.div` @@ -77,21 +78,8 @@ export const PayloadPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); + rpcClient .getMiDiagramRpcClient() .getHelperPaneInfo(requestBody) diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx index 92728a68476..cab00788969 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/PropertiesPage.tsx @@ -22,6 +22,7 @@ import { Position } from 'vscode-languageserver-types'; import { HelperPaneCompletionItem } from '@wso2/mi-core'; import { COMPLETION_ITEM_KIND, getIcon, HelperPane } from '@wso2/ui-toolkit'; import { filterHelperPaneCompletionItems, getHelperPaneCompletionItem } from '../FormExpressionField/utils'; +import { createHelperPaneRequestBody } from '../utils'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { PAGE, Page } from './index'; @@ -51,21 +52,8 @@ export const PropertiesPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); + rpcClient .getMiDiagramRpcClient() .getHelperPaneInfo(requestBody) diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx index 83b8fd737b5..0ff457a839c 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/VariablesPage.tsx @@ -23,6 +23,7 @@ import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { COMPLETION_ITEM_KIND, getIcon, HelperPane } from '@wso2/ui-toolkit'; import { HelperPaneCompletionItem } from '@wso2/mi-core'; import { filterHelperPaneCompletionItems, getHelperPaneCompletionItem } from '../FormExpressionField/utils'; +import { createHelperPaneRequestBody } from '../utils'; import { PAGE, Page } from './index'; type VariablesPageProps = { @@ -51,21 +52,7 @@ export const VariablesPage = ({ setIsLoading(true); setTimeout(() => { rpcClient.getVisualizerState().then((machineView) => { - let requestBody; - const documentUri = artifactPath ? artifactPath : machineView.documentUri; - - if (machineView.documentUri.includes('src/test/')) { - requestBody = { - documentUri: documentUri, - position: {line: 0, character: 0 }, - needLastMediator: true - } - }else{ - requestBody = { - documentUri: documentUri, - position: position - } - } + const requestBody = createHelperPaneRequestBody(machineView, position, artifactPath); rpcClient .getMiDiagramRpcClient() diff --git a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx index 8cb6f9d6d03..16657cfbe58 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/HelperPane/index.tsx @@ -147,7 +147,7 @@ const HelperPaneEl = ({ position, helperPaneHeight, isTokenEditor, isFullscreen, handleFullscreenChange(); } }, [isFullscreen]); - + return (
diff --git a/workspaces/mi/mi-diagram/src/components/Form/utils.tsx b/workspaces/mi/mi-diagram/src/components/Form/utils.tsx index ea15fcc9b69..703c719704c 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/utils.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/utils.tsx @@ -77,3 +77,32 @@ export function isLegacyExpression( /* If non of the conditions are met -> enable the new expression editor */ return false; } + +/** + * Creates a request body for helper pane information based on the document URI and context. + * + * @param machineView - The machine view containing document URI and other context. + * @param position - The position in the document. + * @param artifactPath - Optional artifact path to override the document URI. + * @returns - The request body object for getHelperPaneInfo RPC call. + */ +export function createHelperPaneRequestBody( + machineView: { documentUri?: string }, + position: { line: number; character: number }, + artifactPath?: string +) { + const documentUri = artifactPath ? artifactPath : machineView.documentUri; + + if (machineView.documentUri?.includes('src/test/')) { + return { + documentUri: documentUri, + position: { line: 0, character: 0 }, + needLastMediator: true + }; + } else { + return { + documentUri: documentUri, + position: position + }; + } +} diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx index e416f16e05b..9be515bb185 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/Tests/TestCaseForm.tsx @@ -28,10 +28,12 @@ import { getTestCaseXML } from "../../../utils/template-engine/mustache-template import { ParameterManager } from "@wso2/mi-diagram"; import { compareVersions } from "@wso2/mi-diagram/lib/utils/commons"; import { getProjectRuntimeVersion } from "../../AIPanel/utils"; + export enum TestSuiteType { API = "API", SEQUENCE = "Sequence" } + interface TestCaseFormProps { filePath?: string; range?: TagRange; From 3d793fb308768b76be68a690b8244504ff52c389 Mon Sep 17 00:00:00 2001 From: LakshanWeerasinghe Date: Tue, 29 Jul 2025 13:28:57 +0530 Subject: [PATCH 008/602] Remove display annotation uages from service designer --- .../views/BI/ServiceClassEditor/ServiceClassDesigner.tsx | 5 +---- .../views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx | 8 ++++---- .../views/BI/ServiceDesigner/Forms/ServiceConfigForm.tsx | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceClassEditor/ServiceClassDesigner.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceClassEditor/ServiceClassDesigner.tsx index 359122803a7..ab78b21dacb 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceClassEditor/ServiceClassDesigner.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceClassEditor/ServiceClassDesigner.tsx @@ -315,10 +315,7 @@ export function ServiceClassDesigner(props: ServiceClassDesignerProps) { line: serviceClassModel.codedata.lineRange.endLine.line, offset: serviceClassModel.codedata.lineRange.endLine.offset } - }, - inListenerInit: false, - isBasePath: false, - inDisplayAnnotation: false + } }, type: { metadata: { diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx index a39017ccbcf..191f76a9d41 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/ServiceDesigner/Forms/ListenerConfigForm.tsx @@ -110,8 +110,8 @@ export function ListenerConfigForm(props: ListenerConfigFormProps) { onSubmit(response); }; - const createTitle = `Provide the necessary configuration details for the ${listenerModel.displayAnnotation.label} to complete the setup.`; - const editTitle = `Update the configuration details for the ${listenerModel.displayAnnotation.label} as needed.` + const createTitle = `Provide the necessary configuration details for the ${listenerModel.name} to complete the setup.`; + const editTitle = `Update the configuration details for the ${listenerModel.name} as needed.` useEffect(() => { if (filePath && rpcClient) { @@ -141,11 +141,11 @@ export function ListenerConfigForm(props: ListenerConfigFormProps) { <> {listenerFields.length > 0 && - {/* {listenerModel.displayAnnotation.label.charAt(0).toUpperCase() + listenerModel.displayAnnotation.label.slice(1)} Configuration + {/* {listenerModel.name.charAt(0).toUpperCase() + listenerModel.name.slice(1)} Configuration {formSubmitText === "Save" ? editTitle : createTitle} */} - + {filePath && targetLineRange && (); const [recordTypeFields, setRecordTypeFields] = useState([]); - const createTitle = `Provide the necessary configuration details for the ${serviceModel.displayAnnotation.label} to complete the setup.`; - const editTitle = `Update the configuration details for the ${serviceModel.displayAnnotation.label} as needed.` + const createTitle = `Provide the necessary configuration details for the ${serviceModel.name} to complete the setup.`; + const editTitle = `Update the configuration details for the ${serviceModel.name} as needed.` useEffect(() => { // Check if the service is HTTP protocol and any properties with choices @@ -197,7 +197,7 @@ export function ServiceConfigForm(props: ServiceConfigFormProps) { <> {serviceFields.length > 0 && - + {filePath && targetLineRange && Date: Thu, 31 Jul 2025 10:40:15 +0530 Subject: [PATCH 009/602] movinf from old repo --- .../grammar/ballerina-grammar | 2 +- .../Components/EmptyItemsPlaceHolder.tsx | 15 + .../Components/ExpandableList.tsx | 68 ++ .../Components/FooterButtons.tsx | 42 + .../BI/HelperPaneNew/Components/Modal.tsx | 125 +++ .../RecordConstructView/ParameterBranch.tsx | 98 +++ .../Types/CustomType/index.tsx | 91 ++ .../Types/InclusionType/index.tsx | 117 +++ .../Types/RecordType/index.tsx | 105 +++ .../Types/UnionType/index.tsx | 184 ++++ .../RecordConstructView/Types/index.ts | 28 + .../Components/RecordConstructView/styles.ts | 317 +++++++ .../RecordConstructView/utils/index.tsx | 82 ++ .../Components/ScrollableContainer.tsx | 7 + .../Components/SelectableDropdown.tsx | 0 .../Components/SelectableItem.tsx | 16 + .../Components/VariableTypeIndicator.tsx | 19 + .../src/views/BI/HelperPaneNew/Utils/types.ts | 27 + .../BI/HelperPaneNew/Views/Configurables.tsx | 271 ++++++ .../BI/HelperPaneNew/Views/CreateValue.tsx | 141 ++++ .../BI/HelperPaneNew/Views/Functions.tsx | 280 +++++++ .../HelperPaneNew/Views/GenerateBICopilot.tsx | 105 +++ .../HelperPaneNew/Views/RecordConfigView.tsx | 70 ++ .../BI/HelperPaneNew/Views/Variables.tsx | 417 ++++++++++ .../src/views/BI/HelperPaneNew/index.tsx | 329 ++++++++ .../BI/HelperPaneNew/styles/Backgrounds.tsx | 13 + .../HelperPaneNew/styles/HorizontalList.tsx | 19 + .../Common/HelperPaneCustom/context.tsx | 26 + .../Common/HelperPaneCustom/index.tsx | 785 ++++++++++++++++++ .../components/Common/SlidingPane/context.tsx | 28 + .../components/Common/SlidingPane/index.tsx | 263 ++++++ 31 files changed, 4089 insertions(+), 1 deletion(-) create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/Modal.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableDropdown.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/RecordConfigView.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Variables.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/index.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/styles/Backgrounds.tsx create mode 100644 workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/styles/HorizontalList.tsx create mode 100644 workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPaneCustom/context.tsx create mode 100644 workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/HelperPaneCustom/index.tsx create mode 100644 workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/SlidingPane/context.tsx create mode 100644 workspaces/common-libs/ui-toolkit/src/components/ExpressionEditor/components/Common/SlidingPane/index.tsx diff --git a/workspaces/ballerina/ballerina-extension/grammar/ballerina-grammar b/workspaces/ballerina/ballerina-extension/grammar/ballerina-grammar index 1d5efa4d49d..eb62358724d 160000 --- a/workspaces/ballerina/ballerina-extension/grammar/ballerina-grammar +++ b/workspaces/ballerina/ballerina-extension/grammar/ballerina-grammar @@ -1 +1 @@ -Subproject commit 1d5efa4d49d7f1b85c89db9bfbd9e1f4428d720d +Subproject commit eb62358724deaad784458ae1d5ec33c4d164e483 diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx new file mode 100644 index 00000000000..d683bda414c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/EmptyItemsPlaceHolder.tsx @@ -0,0 +1,15 @@ + + +export const EmptyItemsPlaceHolder = () => { + return ( +
+ No items found +
+ ) +} \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx new file mode 100644 index 00000000000..ad48c2bc5df --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ExpandableList.tsx @@ -0,0 +1,68 @@ +import { HorizontalListContainer, HorizontalListItem, HorizontalListItemLeftContent } from "../styles/HorizontalList" +import React from "react"; +import styled from "@emotion/styled"; + + +type ExpandableListProps = { + children: React.ReactNode; + sx?: React.CSSProperties; +}; + +export const ExpandableList = ({ children, sx }: ExpandableListProps) => { + return ( + + {children} + + ); +}; + +interface ExpandableListItemProps { + children: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; + sx?: React.CSSProperties; +} + +const Item = ({ children, onClick, sx }: ExpandableListItemProps) => { + return ( + + + {children} + + + ); +}; + +interface ExpandableListSectionProps { + children: React.ReactNode; + title: string; + level?: number; + sx?: React.CSSProperties; +} + + +const Section = ({ children, title, level = 0, sx }: ExpandableListSectionProps) => { + return ( + + {title} + {children} + + ); +}; + + + + +ExpandableList.Item = Item; +ExpandableList.Section = Section; + +export default ExpandableList; + + +const ExpandableListSection = styled.div<{ level?: number }>` + padding-left: ${({ level = 0 }) => (level * 5) + 10}px; +`; + + +const ExpandableListSectionTitle = styled.span` + font-weight: 600; +`; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx new file mode 100644 index 00000000000..c067fbb28b7 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/FooterButtons.tsx @@ -0,0 +1,42 @@ +import { Button, Codicon, ThemeColors } from "@wso2/ui-toolkit"; +import styled from '@emotion/styled'; + +const InvisibleButton = styled.button` + background: none; + border: none; + padding: 0; + margin: 0; + text-align: inherit; + color: inherit; + font: inherit; + cursor: pointer; + outline: none; + box-shadow: none; + appearance: none; + display: inline-flex; + align-items: center; +`; + +type FooterButtonProps = { + onClick?: () => void; + startIcon: string; + title: string; + sx?: React.CSSProperties; + disabled?:boolean; +} + +const FooterButtons = (props: FooterButtonProps) => { + const { onClick, startIcon, title, sx } = props; + return ( +
+ + + {title} + +
+ ) +} + +export default FooterButtons; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/Modal.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/Modal.tsx new file mode 100644 index 00000000000..122c94cd720 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/Modal.tsx @@ -0,0 +1,125 @@ +import React, { useState, cloneElement, isValidElement, ReactNode, ReactElement, useEffect } from "react"; +import { createPortal } from "react-dom"; +import styled from "@emotion/styled"; +import { Codicon, Divider, ThemeColors } from "@wso2/ui-toolkit"; + +export type DynamicModalProps = { + children: ReactNode; + onClose?: () => void; + title: string; + anchorRef: React.RefObject; + width?: number; + height?: number; + openState: boolean; + setOpenState: (state: boolean)=>void; +}; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 30000 !important; + display: flex; + justify-content: center; + align-items: center; +`; + +const ModalBox = styled.div<{ width?: number; height?: number }>` + width: ${({ width }: { width?: number }) => (width ? `${width}px` : 'auto')}; + height: ${({ height }: { height?: number }) => (height ? `${height}px` : 'auto')}; + background-color: ${ThemeColors.PRIMARY_CONTAINER}; + position: relative; + padding: 10px; + display: flex; + flex-direction: column; + overflow-y: auto; +`; + +const InvisibleButton = styled.button` + background: none; + border: none; + padding: 0; + margin: 0; + text-align: inherit; + color: inherit; + font: inherit; + cursor: pointer; + outline: none; + box-shadow: none; + appearance: none; + display: inline-flex; + align-items: center; +`; + +const Title = styled.h1` + font-size: 1.5rem; + font-weight: 600; + margin: 0; + position: absolute; + top: 8px; + left: 8px; +`; + +type TriggerProps = React.ButtonHTMLAttributes & { children: ReactNode }; +const Trigger: React.FC = (props) => {props.children}; + +const DynamicModal: React.FC & { Trigger: typeof Trigger } = ({ + children, + onClose, + title, + anchorRef, + width, + height, + openState, + setOpenState, +}) => { + + let trigger: ReactElement | null = null; + const content: ReactNode[] = []; + + React.Children.forEach(children, child => { + if (isValidElement(child) && child.type === DynamicModal.Trigger) { + trigger = cloneElement(child as React.ReactElement, { + onClick: () => setOpenState(true) + }); + } else { + content.push(child); + } + }); + + const handleClose = () => { + setOpenState(false); + onClose && onClose(); + }; + + useEffect(()=>{ + console.log("inside") + setOpenState(openState) + }, [openState]); + + return ( + <> + {trigger} + {openState && createPortal( + + + + + + {title} + +
{content}
+
+
, + document.body + )} + + ); +}; + +DynamicModal.Trigger = Trigger; + +export default DynamicModal; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx new file mode 100644 index 00000000000..6ff47a25c4c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/ParameterBranch.tsx @@ -0,0 +1,98 @@ +/** + * 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 } from "react"; + +import { TypeField } from "@wso2/ballerina-core"; +import { Button } from "@wso2/ui-toolkit"; + + +import { useHelperPaneStyles } from "./styles"; +import { isAnyFieldSelected, isRequiredParam } from "./utils"; + +import * as Types from "./Types"; + +export interface ParameterBranchProps { + parameters: TypeField[]; + depth: number; + onChange: () => void; +} + +export interface TypeProps { + param: TypeField; + depth: number; + onChange: () => void; +} + +export function ParameterBranch(props: ParameterBranchProps) { + const { parameters, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + + const [showOptionalParams, setShowOptionalParams] = useState(isAnyFieldSelected(parameters)); + + const requiredParams: JSX.Element[] = []; + const optionalParams: JSX.Element[] = []; + + parameters?.forEach((param: TypeField, index: number) => { + let TypeComponent = (Types as any)[param.typeName]; + const typeProps: TypeProps = { + param, + depth, + onChange, + }; + if (!TypeComponent) { + TypeComponent = (Types as any).custom; + } + if (isRequiredParam(param)) { + requiredParams.push(); + } else { + optionalParams.push(); + } + }); + + function toggleOptionalParams(e: any) { + setShowOptionalParams(!showOptionalParams); + } + + return ( +
+ {requiredParams} + {(optionalParams.length > 0 && depth === 1) ? ( + optionalParams + ) : ( + <> + {optionalParams.length > 0 && ( +
+ {/*
Optional fields
*/} + +
+ )} + {showOptionalParams && optionalParams.length > 0 && optionalParams} + + )} +
+ ); +} + +export const MemoizedParameterBranch = React.memo(ParameterBranch); diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx new file mode 100644 index 00000000000..0ef73447d39 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/CustomType/index.tsx @@ -0,0 +1,91 @@ +/** + * 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 } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { isRequiredParam } from "../../utils"; + +export default function CustomType(props: TypeProps) { + const { param, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param); + if (requiredParam) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + + const toggleParamCheck = () => { + if (!requiredParam) { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + setParamSelected(newSelectedState); + onChange(); + } + }; + + return ( +
+
+
+ + + {param.name} + + + {param.optional || param.defaultable ? param.typeName + " (Optional)" : param.typeName} + + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx new file mode 100644 index 00000000000..37ec0427e5e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/InclusionType/index.tsx @@ -0,0 +1,117 @@ +/** + * 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 } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { ParameterBranch } from "../../ParameterBranch"; +import { isAllDefaultableFields, isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function InclusionType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + const isAllIncludedParamDefaultable = isAllDefaultableFields(param.inclusionType?.fields); + if (requiredParam && !isAllIncludedParamDefaultable) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState( + param.selected || (requiredParam && !isAllIncludedParamDefaultable) + ); + + const toggleParamCheck = () => { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + param.inclusionType.selected = newSelectedState; + + // If the inclusion type has fields, update their selection state + if (param.inclusionType?.fields && param.inclusionType.fields.length > 0) { + updateFieldsSelection(param.inclusionType.fields, newSelectedState); + } + + setParamSelected(newSelectedState); + onChange(); + }; + + const handleOnChange = () => { + param.selected = param.inclusionType.selected; + onChange(); + }; + + return ( +
+
+
+ + + {param.name} + + {param.inclusionType?.typeInfo && ( + + {(param.optional || param.defaultable) && " (Optional)"} * + {param.inclusionType.typeInfo.name} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ {paramSelected && param.inclusionType?.fields?.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx new file mode 100644 index 00000000000..750c7dcea01 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/RecordType/index.tsx @@ -0,0 +1,105 @@ +/** + * 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 } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { Codicon, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { MemoizedParameterBranch } from "../../ParameterBranch"; +import { isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function RecordType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + if (requiredParam) { + param.selected = true; + } + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + + const toggleParamCheck = () => { + if (!requiredParam) { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + + // If the record has fields, update their selection state + if (param.fields && param.fields.length > 0) { + updateFieldsSelection(param.fields, newSelectedState); + } + + setParamSelected(newSelectedState); + onChange(); + } + }; + + return ( +
+
+
+ + + {param.name} + + {param.typeInfo && ( + + {(param.optional || param.defaultable) && " (Optional)"} {param.typeInfo.name} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ {paramSelected && param.fields?.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx new file mode 100644 index 00000000000..dfd1ceaf27c --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/UnionType/index.tsx @@ -0,0 +1,184 @@ +/** + * 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, { useRef, useState, useEffect } from "react"; + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; +import { TypeField } from "@wso2/ballerina-core"; +import { Codicon, Dropdown, Tooltip, Typography } from "@wso2/ui-toolkit"; + +import { TypeProps } from "../../ParameterBranch"; +import { useHelperPaneStyles } from "../../styles"; +import { ParameterBranch } from "../../ParameterBranch"; +import { getSelectedUnionMember, isRequiredParam, updateFieldsSelection } from "../../utils"; + +export default function UnionType(props: TypeProps) { + const { param, depth, onChange } = props; + const helperStyleClass = useHelperPaneStyles(); + + const requiredParam = isRequiredParam(param) && depth > 1; // Only apply required param logic after depth 1 + if (requiredParam) { + param.selected = true; + } + const memberTypes = param.members?.map((field, index) => ({ id: index.toString(), value: getUnionParamName(field) })); + const initSelectedMember = getSelectedUnionMember(param); + + const [paramSelected, setParamSelected] = useState(param.selected || requiredParam); + const [selectedMemberType, setSelectedMemberType] = useState(getUnionParamName(initSelectedMember)); + const [parameter, setParameter] = useState(initSelectedMember); + + // Initialize: If the union is selected, ensure the selected member and its required fields are also selected + useEffect(() => { + if (paramSelected && initSelectedMember) { + handleMemberType(paramSelected ? selectedMemberType : ""); + } + }, []); + + if (!(param.members && param.members.length > 0)) { + return <>; + } + + const updateFormFieldMemberSelection = (unionField: TypeField) => { + const unionFieldName = getUnionParamName(unionField); + param.members.forEach((field) => { + field.selected = getUnionParamName(field) === unionFieldName; + + // If this is the selected field and it has nested fields, update them + if (field.selected && field.fields && field.fields.length > 0) { + // Set required fields to selected + updateFieldsSelection(field.fields, true); + } else if (!field.selected && field.fields && field.fields.length > 0) { + // Deselect all fields of non-selected members + updateFieldsSelection(field.fields, false); + } + }); + }; + + const handleMemberType = (type: string) => { + const selectedMember = param.members.find((field) => getUnionParamName(field) === type); + updateFormFieldMemberSelection(selectedMember); + setSelectedMemberType(type); + setParameter(selectedMember); + + // If the parent is selected and the selected member has fields, ensure required fields are selected + if (param.selected && selectedMember && selectedMember.fields && selectedMember.fields.length > 0) { + updateFieldsSelection(selectedMember.fields, true); + } + + onChange(); + }; + + const toggleParamCheck = () => { + const newSelectedState = !paramSelected; + param.selected = newSelectedState; + + // When checkbox is checked, ensure the currently selected member is also marked as selected + if (newSelectedState) { + const selectedMember = param.members.find((field) => getUnionParamName(field) === selectedMemberType); + if (selectedMember) { + updateFormFieldMemberSelection(selectedMember); + + // If the selected member has fields, recursively set required fields to selected + if (selectedMember.fields && selectedMember.fields.length > 0) { + updateFieldsSelection(selectedMember.fields, true); + } + } + } else { + // When unchecking, clear all member selections + param.members.forEach((field) => { + field.selected = false; + + // If the member has fields, recursively deselect all fields + if (field.fields && field.fields.length > 0) { + updateFieldsSelection(field.fields, false); + } + }); + } + + setParamSelected(newSelectedState); + onChange(); + }; + + return ( +
+
+
+ + + {param.name} + + {(param.optional || param.defaultable) && ( + + {"(Optional)"} + + )} + {param.documentation && ( + + {param.documentation} + + } + position="right" + sx={{ maxWidth: '300px', whiteSpace: 'normal', pointerEvents: 'none' }} + > + + + )} +
+ +
+
+ {paramSelected && parameter && ( +
+ +
+ )} +
+
+ ); +} + +export function getUnionParamName(param: TypeField) { + return param ? param.name || param.typeName : ""; +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts new file mode 100644 index 00000000000..74a8994a46e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/Types/index.ts @@ -0,0 +1,28 @@ +/** + * 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 CustomType from './CustomType'; +import InclusionType from "./InclusionType"; +import RecordType from "./RecordType"; +import UnionType from "./UnionType"; + +export {RecordType as record}; +export {UnionType as union}; +export {UnionType as enum}; +export {InclusionType as inclusion}; +export {CustomType as custom}; diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts new file mode 100644 index 00000000000..eabf726c434 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/styles.ts @@ -0,0 +1,317 @@ +/** + * 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 { css } from "@emotion/css"; + + +const removePadding = { + padding: '0px' +} + +const statementFontStyles = { + fontSize: "15px", + 'user-select': 'none', + fontFamily: 'monospace' +} + +const truncateText = { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' +} + +export const useHelperPaneStyles = () => ({ + suggestionsInner: css({ + overflowY: 'hidden', + height: '100%', + width: '100%' + }), + suggestionListContainer: css({ + overflowY: 'scroll', + marginTop: '5px', + }), + suggestionListItem: css({ + display: 'flex' + }), + suggestionDataType: css({ + color: 'var(--vscode-terminal-ansiGreen)', + ...truncateText, + }), + suggestionValue: css({ + ...truncateText, + }), + listItem: css({ + display: 'flex', + maxWidth: '155px', + }), + suggestionListInner: css({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column' + }), + expressionExample: css({ + fontSize: '13px', + }), + searchBox: css({ + width: '100%' + }), + librarySearchBox: css({ + position: 'relative', + height: '32px', + width: 'inherit', + border: '1px var(--custom-input-border-color)', + color: '#8D91A3', + textIndent: '12px', + textAlign: 'left', + marginBottom: '16px', + paddingLeft: '10px' + }), + helperPaneSubHeader: css({ + color: 'var(--vscode-editor-foreground)', + marginBottom: '4px', + fontWeight: 500 + }), + groupHeaderWrapper: css({ + display: 'flex', + flexDirection: 'row', + marginBottom: '14px' + }), + selectionWrapper: css({ + display: 'flex', + flexDirection: 'row', + marginTop: '5px' + }), + suggestionDividerWrapper: css({ + marginTop: '5px', + }), + groupHeader: css({ + color: 'var(--vscode-editor-foreground)', + fontWeight: 500 + }), + selectionSubHeader: css({ + color: 'var(--vscode-settings-textInputForeground)', + borderRadius: '5px', + backgroundColor: 'var(--vscode-editor-selectionBackground)', + marginRight: '5px', + ...statementFontStyles + }), + selectionSeparator: css({ + height: '1px', + width: '100%', + flex: '1 0', + backgroundColor: 'var(--vscode-panel-border)', + alignSelf: 'flex-end' + }), + loadingContainer: css({ + height: '60vh', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }), + libraryWrapper: css({ + marginTop: '5px', + overflowY: 'scroll', + }), + libraryBrowser: css({ + height: '100%' + }), + libraryBrowserHeader: css({ + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', + zIndex: 1, + color: 'var(--vscode-sideBar-foreground)' + }), + searchResult: css({ + paddingTop: '15px', + }), + + moduleTitle: css({ + fontSize: '14px', + margin: '0 10px' + }), + libraryReturnIcon: css({ + marginRight: '8px' + }), + libraryElementBlock: css({ + top: '5%', + display: 'flex', + flexDirection: 'column' + }), + libraryElementBlockLabel: css({ + height: '10%', + padding: '0 10px' + }), + parameterCheckbox: css({ + margin: '0px' + }), + checked: css({}), + docParamSuggestions: css({ + height: '100%', + overflowY: 'scroll' + }), + returnSeparator: css({ + height: '1px', + opacity: '0.52', + backgroundColor: 'var(--vscode-panel-border)', + marginBottom: '15px' + }), + docParamDescriptionText: css({ + flex: "inherit", + ...removePadding + }), + includedRecordPlusBtn: css({ + display: 'block', + alignSelf: 'center', + padding: '0px', + marginLeft: '10px' + }), + paramHeader: css({ + marginTop: '0px', + color: 'var(--vscode-editor-foreground)' + }), + paramDataType: css({ + marginLeft: '8px', + marginRight: '8px', + flex: 'inherit', + ...removePadding + }), + requiredArgList: css({ + display: 'flex', + alignItems: 'flex-start', + overflowX: 'hidden', + width: 'fit-content', + ...removePadding + }), + docDescription: css({ + maxHeight: '50%', + overflowY: 'scroll', + whiteSpace: 'break-spaces', + display: 'block', + margin: '10px 0px', + ...removePadding + }), + returnDescription: css({ + maxHeight: '15%', + overflowY: 'scroll', + whiteSpace: 'break-spaces', + "& .MuiListItem-root": { + paddingLeft: '0px' + }, + ...removePadding + }), + paramList: css({ + overflowY: 'auto', + margin: '10px 0px', + }), + documentationWrapper: css({ + marginLeft: '28px', + }), + includedRecordHeaderList: css({ + "& .MuiListItem-root": { + padding: '0px', + alignItems: 'flex-start' + }, + "& .MuiListItemText-root": { + flex: "inherit" + }, + ...removePadding + }), + docListDefault: css({ + "& .MuiListItem-root": { + padding: '0px' + }, + "& .MuiListItemText-root": { + flex: 'inherit', + minWidth: 'auto', + margin: '0 6px 0 0' + }, + alignItems: 'flex-start', + width: 'fit-content', + ...removePadding + }), + docListCustom: css({ + marginBottom: '12px', + "& .MuiListItem-root": { + padding: '0px' + }, + "& .MuiListItemText-root": { + flex: 'inherit', + minWidth: 'auto', + margin: '0 6px 0 0' + }, + alignItems: 'flex-start', + width: 'fit-content', + ...removePadding + }), + exampleCode: css({ + display: 'flex', + padding: '5px', + fontFamily: 'monospace', + borderRadius: '0px' + }), + paramTreeDescriptionText: css({ + flex: "inherit", + whiteSpace: 'pre-wrap', + marginLeft: '24px', + ...removePadding + }), + listItemMultiLine: css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + minHeight: '32px' + }), + listItemHeader: css({ + display: 'flex', + alignItems: 'center', + height: '28px' + }), + listItemBody: css({ + marginLeft: '12px', + marginBottom: '8px', + paddingLeft: '16px', + borderLeft: "1px solid #d8d8d8", + }), + listDropdownWrapper: css({ + width: '200px', + }), + listOptionalWrapper: css({ + display: 'flex', + alignItems: 'center', + height: '32px', + marginBottom: '12px' + }), + listOptionalBtn: css({ + textTransform: 'none', + minWidth: '32px', + marginLeft: '8px' + }), + listOptionalHeader: css({ + fontSize: '13px', + color: "gray", + fontWeight: 500, + letterSpacing: '0', + lineHeight: '14px', + paddingLeft: '0px', + }), +}); + + diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx new file mode 100644 index 00000000000..2027af17715 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/RecordConstructView/utils/index.tsx @@ -0,0 +1,82 @@ +/** + * 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 { + FormField, + keywords, +} from "@wso2/ballerina-core"; + + +export function isRequiredParam(param: FormField): boolean { + return !(param.optional || param.defaultable); +} + +export function isAllDefaultableFields(recordFields: FormField[]): boolean { + return recordFields?.every((field) => field.defaultable || (field.fields && isAllDefaultableFields(field.fields))); +} + +export function isAnyFieldSelected(recordFields: FormField[]): boolean { + return recordFields?.some((field) => field.selected || (field.fields && isAnyFieldSelected(field.fields))); +} + +export function getSelectedUnionMember(unionFields: FormField): FormField { + let selectedMember = unionFields.members?.find((member) => member.selected === true); + if (!selectedMember) { + selectedMember = unionFields.members?.find( + (member) => getUnionFormFieldName(member) === unionFields.selectedDataType + ); + } + if (!selectedMember) { + selectedMember = unionFields.members?.find( + (member) => member.typeName === unionFields.value?.replace(/['"]+/g, "") + ); + } + if (!selectedMember && unionFields.members && unionFields.members.length > 0) { + selectedMember = unionFields.members[0]; + } + return selectedMember; +} + +export function getFieldName(fieldName: string): string { + return keywords.includes(fieldName) ? "'" + fieldName : fieldName; +} + +export function getUnionFormFieldName(field: FormField): string { + return field.name || field.typeInfo?.name || field.typeName; +} + +export function checkFormFieldValue(field: FormField): boolean { + return field.value !== undefined && field.value !== null; +} + +export function updateFieldsSelection(fields: FormField[], selected: boolean): void { + if (!fields || !fields.length) return; + + fields.forEach(field => { + // When selecting: only select required fields + // When deselecting: deselect all fields (both required and optional) + if (!selected || isRequiredParam(field)) { + field.selected = selected; + } + + // Recursively process nested fields + if (field.fields && field.fields.length > 0) { + updateFieldsSelection(field.fields, selected); + } + }); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx new file mode 100644 index 00000000000..e73c6ee571b --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/ScrollableContainer.tsx @@ -0,0 +1,7 @@ +import styled from "@emotion/styled"; + +export const ScrollableContainer = styled.div` + flex: 1; + overflow: auto; + min-height: 0; +`; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableDropdown.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableDropdown.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx new file mode 100644 index 00000000000..8609caf2c18 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/SelectableItem.tsx @@ -0,0 +1,16 @@ +import { ThemeColors } from "@wso2/ui-toolkit"; +import styled from "@emotion/styled"; + +const SelectableItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px; + &:hover { + background-color: ${ThemeColors.SURFACE_DIM}; + cursor: pointer; + } +` + +export default SelectableItem; \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx new file mode 100644 index 00000000000..5e54d2259f0 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Components/VariableTypeIndicator.tsx @@ -0,0 +1,19 @@ +import { ThemeColors } from "@wso2/ui-toolkit"; +import { pad } from "lodash"; + +const getTypeColor = (type: string, isRow: boolean): string => { + //TODO: change if need + return ThemeColors.PRIMARY +} + +type VariableTypeIndifcatorProps = { + type: string; + isRow?: boolean; +} +export const VariableTypeIndifcator = ({type, isRow = true}: VariableTypeIndifcatorProps) => { + return ( +
+ {`${type}`} +
+ ); +} \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts new file mode 100644 index 00000000000..666c682e45b --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Utils/types.ts @@ -0,0 +1,27 @@ +import { CompletionItem } from "@wso2/ui-toolkit"; + +export const DEFAULT_VALUE_MAP: Record = { + "struct": "{}", + "array": "[]", + "map": "{}", + "string": "\"\"", + "int": "0", + "float": "0.0", + "boolean": "false", + "any": "null", +} + +export const isRowType = (type: CompletionItem) => { + return type && type.kind === "struct"; +} + +export const isUnionType = (type: CompletionItem) => { + return type && type.kind === "enum"; +} + +export const getDefaultValue = (type: CompletionItem) => { + const typeKind = type?.kind; + if (typeKind && typeKind === 'type-parameter') { + return DEFAULT_VALUE_MAP[type.label] || ""; + } +} \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx new file mode 100644 index 00000000000..03e438c5bb7 --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Configurables.tsx @@ -0,0 +1,271 @@ +import { CompletionInsertText, ConfigVariable, FlowNode, LineRange, TomalPackage } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { ReactNode, useEffect, useState } from "react"; +import ExpandableList from "../Components/ExpandableList"; +import { SlidingPaneNavContainer } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane"; +import { COMPLETION_ITEM_KIND, Divider, getIcon, ThemeColors } from "@wso2/ui-toolkit"; +import { ScrollableContainer } from "../Components/ScrollableContainer"; +import DynamicModal from "../Components/Modal"; +import FooterButtons from "../Components/FooterButtons"; +import FormGenerator from "../../Forms/FormGenerator"; +import { URI, Utils } from "vscode-uri"; + + +type ConfigVariablesState = { + [category: string]: { + [module: string]: ConfigVariable[]; + }; +}; + +type ListItem = { + name: string; + items: any[] +} + +type ConfigurablesPageProps = { + onChange: (insertText: string | CompletionInsertText, isRecordConfigureChange?: boolean) => void; + isInModal?: boolean; + anchorRef: React.RefObject; + fileName: string; + targetLineRange: LineRange; +} + +type AddNewConfigFormProps = { + isImportEnv: boolean; + title: string; +} + + + +export const Configurables = (props: ConfigurablesPageProps) => { + const { onChange, anchorRef, fileName, targetLineRange } = props; + + const { rpcClient } = useRpcContext(); + const [configVariables, setConfigVariables] = useState({}); + const [errorMessage, setErrorMessage] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const [configVarNode, setCofigVarNode] = useState(); + const [isSaving, setIsSaving] = useState(false); + const [packageInfo, setPackageInfo] = useState(); + const [isImportEnv, setIsImportEnv] = useState(false); + const [projectPathUri, setProjectPathUri] = useState(); + + + useEffect(() => { + const fetchNode = async () => { + const node = await rpcClient.getBIDiagramRpcClient().getConfigVariableNodeTemplate({ + isNew: true, + isEnvVariable: isImportEnv + }); + console.log("node: ", node) + setCofigVarNode(node.flowNode); + }; + + fetchNode(); + }, [isImportEnv]); + + useEffect(() => { + getConfigVariables() + }, []) + + useEffect(()=>{ + getProjectInfo() + },[]); + + useEffect(() => { + const fetchTomlValues = async () => { + try { + const tomValues = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + setPackageInfo(tomValues?.package); + } catch (error) { + console.error("Failed to fetch TOML values:", error); + setPackageInfo({ + org: "", + name: "", + version: "" + }); + } + }; + + fetchTomlValues(); + }, []); + + const getProjectInfo = async () => { + const projectPath = await rpcClient.getVisualizerLocation(); + setProjectPathUri(URI.file(projectPath.projectUri).fsPath); + } + + const getConfigVariables = async () => { + + let data: ConfigVariablesState = {}; + let errorMsg: string = ''; + + await rpcClient + .getBIDiagramRpcClient() + .getConfigVariablesV2() + .then((variables) => { + data = (variables as any).configVariables; + errorMsg = (variables as any).errorMsg; + }); + + setConfigVariables(data); + console.log(translateToArrayFormat(data)) + setErrorMessage(errorMsg); + console.log(data) + }; + + const handleFormClose = () => { + setIsModalOpen(false) + } + + const handleSave = async (node: FlowNode) => { + setIsSaving(true); + await rpcClient.getBIDiagramRpcClient().updateConfigVariablesV2({ + configFilePath: Utils.joinPath(URI.file(projectPathUri), 'config.bal').fsPath, + configVariable: node, + packageName: `${packageInfo.org}/${packageInfo.name}`, + moduleName: "", + }).finally(() => { + handleFormClose() + setIsSaving(false); + getConfigVariables + }); + }; + + const translateToArrayFormat = (object: object): ListItem[] => { + if (Array.isArray(object)) return object; + const keys = Object.keys(object); + return keys.map((key): { name: string; items: object[] } => { + return { + name: key, + items: translateToArrayFormat((object as Record)[key]) + } + }); + } + + const handleSubmit = (updatedNode?: FlowNode, isDataMapperFormUpdate?: boolean) => { + + // newNodeNameRef.current = ""; + // // Safely extract the variable name as a string, fallback to empty string if not available + // const varName = typeof updatedNode?.properties?.variable?.value === "string" + // ? updatedNode.properties.variable.value + // : ""; + // newNodeNameRef.current = varName; + // handleOnFormSubmit?.(updatedNode, isDataMapperFormUpdate, { shouldCloseSidePanel: false, shouldUpdateTargetLine: true }); + // if (isModalOpen) { + // setIsModalOpen(false) + // getVariableInfo(); + // } + }; + + const handleItemClicked = (name: string) => { + onChange(name, true) + } + + const AddNewForms = (props: AddNewConfigFormProps) => { + return ( + + { + setIsImportEnv(props.isImportEnv) + }} + /> + + { }} + isInModal={true} + /> + ) + } + return ( +
+ + + {translateToArrayFormat(configVariables) + .filter(category => + Array.isArray(category.items) && + category.items.some(sub => Array.isArray(sub.items) && sub.items.length > 0) + ) + .map(category => ( + +
+ {category.items + .filter(subCategory => subCategory.items && subCategory.items.length > 0) + .map(subCategory => ( + <> + {subCategory.name !== '' ? <> + +
+ {subCategory.items.map((item: ConfigVariable) => ( + + { handleItemClicked(item?.properties?.variable?.value as string) }}> + {getIcon(COMPLETION_ITEM_KIND.Parameter)} + {item?.properties?.variable?.value as ReactNode} + + + ))} +
+
+ : <> + {subCategory.items.map((item: ConfigVariable) => ( + + { handleItemClicked(item?.properties?.variable?.value as string) }}> + {getIcon(COMPLETION_ITEM_KIND.Parameter)} + {item?.properties?.variable?.value as ReactNode} + + + ))}} + + ))} +
+
+ ))} +
+
+ {
+ + + + {/* + + + + */} +
} +
+ + + ) +} \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx new file mode 100644 index 00000000000..f26675e041e --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/CreateValue.tsx @@ -0,0 +1,141 @@ +import { GetRecordConfigRequest, GetRecordConfigResponse, PropertyTypeMemberInfo, RecordSourceGenRequest, RecordSourceGenResponse, RecordTypeField, TypeField } from "@wso2/ballerina-core"; +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { useSlidingPane } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane/context"; +import { useEffect, useRef, useState } from "react"; +import { RecordConfig } from "./RecordConfigView"; +import { CompletionItem } from "@wso2/ui-toolkit"; +import { getDefaultValue, isRowType } from "../Utils/types"; +import ExpandableList from "../Components/ExpandableList"; +import SelectableItem from "../Components/SelectableItem"; + +type CreateValuePageProps = { + fileName: string; + currentValue: string; + onChange: (value: string, isRecordConfigureChange: boolean) => void; + selectedType?: CompletionItem; + recordTypeField?: RecordTypeField; +} + +const passPackageInfoIfExists = (recordTypeMember: PropertyTypeMemberInfo) => { + let org = ""; + let module = ""; + let version = ""; + if (recordTypeMember?.packageInfo) { + const parts = recordTypeMember?.packageInfo.split(':'); + if (parts.length === 3) { + [org, module, version] = parts; + } + } + return { org, module, version } +} + +const getPropertyMember = (field: RecordTypeField) => { + return field?.recordTypeMembers.at(0); +} + +export const CreateValue = (props: CreateValuePageProps) => { + const { fileName, currentValue, onChange, selectedType, recordTypeField } = props; + const [recordModel, setRecordModel] = useState([]); + + const { rpcClient } = useRpcContext(); + const propertyMember = getPropertyMember(recordTypeField) + + const sourceCode = useRef(currentValue); + + const getRecordConfigRequest = async () => { + if (recordTypeField) { + const packageInfo = passPackageInfoIfExists(recordTypeField?.recordTypeMembers.at(0)) + return { + filePath: fileName, + codedata: { + org: packageInfo.org, + module: packageInfo.module, + version: packageInfo.version, + packageName: propertyMember?.packageName, + }, + typeConstraint: propertyMember?.type, + } + } + else{ + const tomValues = await rpcClient.getCommonRpcClient().getCurrentProjectTomlValues(); + return { + filePath: fileName, + codedata: { + org: tomValues.package.org, + module: tomValues.package.name, + version: tomValues.package.version, + packageName: propertyMember?.packageName, + }, + typeConstraint: propertyMember?.type || selectedType.label, + } + } + } + + const fetchRecordModel = async () => { + const request = await getRecordConfigRequest(); + const typeFieldResponse: GetRecordConfigResponse = await rpcClient.getBIDiagramRpcClient().getRecordConfig(request); + if (typeFieldResponse.recordConfig) { + const recordConfig: TypeField = { + name: propertyMember?.type, + ...typeFieldResponse.recordConfig + } + + setRecordModel([recordConfig]); + } + } + + const handleModelChange = async (updatedModel: TypeField[]) => { + const request: RecordSourceGenRequest = { + filePath: fileName, + type: updatedModel[0] + } + const recordSourceResponse: RecordSourceGenResponse = await rpcClient.getBIDiagramRpcClient().getRecordSource(request); + console.log(">>> recordSourceResponse", recordSourceResponse); + + if (recordSourceResponse.recordValue !== undefined) { + const content = recordSourceResponse.recordValue; + sourceCode.current = content; + onChange(content, true); + } + } + + + useEffect(() => { + fetchRecordModel() + }, []); + + return ( + (isRowType(selectedType)) || recordTypeField ? : + ) +} + +const NonRecordCreateValue = (props: CreateValuePageProps) => { + const { selectedType } = props; + + const handleValueSelect = (value: string) => { + console.log("value", value) + } + + const defaultValue = getDefaultValue(selectedType); + return ( + <> + {defaultValue && ( + + { handleValueSelect(defaultValue) }} className="selectable-list-item"> + + {defaultValue} + + + + )} + + ); +} diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx new file mode 100644 index 00000000000..e42d833812f --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/Functions.tsx @@ -0,0 +1,280 @@ +import { useRpcContext } from "@wso2/ballerina-rpc-client"; +import { HelperPaneCompletionItem, HelperPaneFunctionInfo } from "@wso2/ballerina-side-panel"; +import { debounce } from "lodash"; +import { useRef, useState, useCallback, RefObject, useEffect } from "react"; +import { convertToHelperPaneFunction, extractFunctionInsertText } from "../../../../utils/bi"; +import { CompletionInsertText, FunctionKind, LineRange } from "@wso2/ballerina-core"; +import { useMutation } from "@tanstack/react-query"; +import { ExpandableList } from "../Components/ExpandableList"; +import { SlidingPaneNavContainer } from "@wso2/ui-toolkit/lib/components/ExpressionEditor/components/Common/SlidingPane"; +import { COMPLETION_ITEM_KIND, getIcon, HelperPaneCustom } from "@wso2/ui-toolkit/lib/components/ExpressionEditor"; +import { EmptyItemsPlaceHolder } from "../Components/EmptyItemsPlaceHolder"; +import styled from "@emotion/styled"; +import { Divider, Overlay, SearchBox, ThemeColors } from "@wso2/ui-toolkit"; +import { LoadingContainer } from "../../../styles"; +import { createPortal } from "react-dom"; +import { LibraryBrowser } from "../../HelperPane/LibraryBrowser"; +import { LoadingRing } from "../../../../components/Loader"; +import { ScrollableContainer } from "../Components/ScrollableContainer"; +import FormGenerator from "../../Forms/FormGenerator"; +import FooterButtons from "../Components/FooterButtons"; +import DynamicModal from "../Components/Modal"; +import { URI, Utils } from "vscode-uri"; +import { FunctionFormStatic } from "../../FunctionFormStatic"; +type FunctionsPageProps = { + fieldKey: string; + anchorRef: RefObject; + fileName: string; + targetLineRange: LineRange; + onClose: () => void; + onChange: (insertText: CompletionInsertText) => void; + updateImports: (key: string, imports: { [key: string]: string }) => void; +}; + +export const FunctionsPage = ({ + fieldKey, + anchorRef, + fileName, + targetLineRange, + onClose, + onChange, + updateImports, +}: FunctionsPageProps) => { + + const { rpcClient } = useRpcContext(); + const firstRender = useRef(true); + const [searchValue, setSearchValue] = useState(''); + const [isLibraryBrowserOpen, setIsLibraryBrowserOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [functionInfo, setFunctionInfo] = useState(undefined); + const [libraryBrowserInfo, setLibraryBrowserInfo] = useState(undefined); + const [projectUri, setProjectUri] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + + + + //TODO: get the correct filepath + let defaultFunctionsFile = Utils.joinPath(URI.file(projectUri), 'functions.bal').fsPath; + + const debounceFetchFunctionInfo = useCallback( + debounce((searchText: string, includeAvailableFunctions?: string) => { + setIsLoading(true); + rpcClient + .getBIDiagramRpcClient() + .search({ + position: targetLineRange, + filePath: fileName, + queryMap: { + q: searchText.trim(), + limit: 12, + offset: 0, + ...(!!includeAvailableFunctions && { includeAvailableFunctions }) + }, + searchKind: "FUNCTION" + }) + .then((response) => { + if (response.categories?.length) { + if (!!includeAvailableFunctions) { + setLibraryBrowserInfo(convertToHelperPaneFunction(response.categories)); + } else { + setFunctionInfo(convertToHelperPaneFunction(response.categories)); + } + } + console.log(response); + }) + .then(() => setIsLoading(false)); + }, 150), + [rpcClient, fileName, targetLineRange] + ); + + const fetchFunctionInfo = useCallback( + (searchText: string, includeAvailableFunctions?: string) => { + debounceFetchFunctionInfo(searchText, includeAvailableFunctions); + }, + [debounceFetchFunctionInfo, searchValue] + ); + + const { mutateAsync: addFunction, isPending: isAddingFunction } = useMutation({ + mutationFn: (item: HelperPaneCompletionItem) => + rpcClient.getBIDiagramRpcClient().addFunction({ + filePath: fileName, + codedata: item.codedata, + kind: item.kind as FunctionKind, + searchKind: 'FUNCTION' + }) + }); + + const onFunctionItemSelect = async (item: HelperPaneCompletionItem) => { + setIsLoading(true); + const response = await addFunction(item); + + setIsLoading(false) + if (response) { + const importStatement = { + [response.prefix]: response.moduleId + }; + updateImports(fieldKey, importStatement); + return extractFunctionInsertText(response.template); + } + + return { value: '' }; + }; + + useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + fetchFunctionInfo(''); + } + + setDefaultFunctionsPath() + }, []); + + const setDefaultFunctionsPath = () => { + rpcClient.getVisualizerLocation().then((location)=> { + setProjectUri(location?.projectUri || '') + }) + } + + const handleFunctionSearch = (searchText: string) => { + setSearchValue(searchText); + + // Search functions + if (isLibraryBrowserOpen) { + fetchFunctionInfo(searchText, 'true'); + } else { + fetchFunctionInfo(searchText); + } + }; + + const handleFunctionItemSelect = async (item: HelperPaneCompletionItem) => { + const { value, cursorOffset } = await onFunctionItemSelect(item); + onChange({ value, cursorOffset }); + onClose(); + }; + + return ( +
+
+ +
+ + { + + isLoading ? ( + + ) : ( + <> + { + !functionInfo || !functionInfo.category || functionInfo.category.length === 0 ? ( + + ) : ( + functionInfo.category.map((category) => { + if (!category.subCategory) { + if (!category.items || category.items.length === 0) { + return null; + } + + return ( + + +
+ {category.items.map((item) => ( + + await handleFunctionItemSelect(item)} + > + {getIcon(COMPLETION_ITEM_KIND.Function)} + {`${item.label}()`} + + + ))} +
+
+
+ ) + } + + //if sub category is empty + if (category.subCategory.length === 0) { + return null; + } + + return ( + + {category.subCategory.map((subCategory) => ( + +
+ {subCategory.items.map((item) => ( + + await handleFunctionItemSelect(item)} + > + {getIcon(COMPLETION_ITEM_KIND.Function)} + {`${item.label}()`} + + + ))} +
+
+ ))} +
+ ) + }) + ) + } + + ) + } +
+
+ + + + + + + + setIsLibraryBrowserOpen(true)} /> + +
+ {isLibraryBrowserOpen && ( + setIsLibraryBrowserOpen(false)} + onClose={onClose} + onChange={onChange} + onFunctionItemSelect={onFunctionItemSelect} + /> + )} + {isAddingFunction && createPortal( + <> + + + + , document.body + )} +
+ ) +} + +const FunctionItemLabel = styled.span` + font-size: 13px; +`; + +// +// {cat.items.map((item) => ( +// {item.label} +// ))} +// \ No newline at end of file diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx new file mode 100644 index 00000000000..ca19366303d --- /dev/null +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/HelperPaneNew/Views/GenerateBICopilot.tsx @@ -0,0 +1,105 @@ +import { AutoResizeTextArea, Button, Codicon, TextArea, ThemeColors } from "@wso2/ui-toolkit" +import { TextField } from "@wso2/ui-toolkit/lib/components/TextField/TextField" +import styled from "@emotion/styled"; +import { useState, useCallback } from "react"; +import debounce from "lodash/debounce"; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +` + +const PromptBox = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + justify-content: space-between; + align-items: center; + position: relative; +` + +const StyledTextArea = styled(AutoResizeTextArea)` + ::part(control) { + font-family: monospace; + font-size: 12px; + min-height: 20px; + padding: 5px 8px; + } +`; + +const GenerateButton = styled.button` + width: 30px; + height: 30px; + border: none; + border-radius: 100%; + background-color: ${ThemeColors.PRIMARY}; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +`; + +const ButtonContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + position: absolute; + right: 10px; + top: 0; + height: 100%; +` + +export const GenerateBICopilot = () => { + + const [prompt, setPrompt] = useState(''); + const [generatedText, setGeneratedText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const getGeneratedText = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve("This is the generated text you got!"); + }, 1000); + }); + } + + const handlePromptChange = async (event: React.ChangeEvent) => { + setPrompt(event?.target?.value || ''); + } + + const handleGenerate = async () => { + setGeneratedText(''); + setIsLoading(true); + const generatedText = await getGeneratedText(); + + let i = 0; + function animate() { + setGeneratedText(generatedText.slice(0, i)); + i++; + if (i <= generatedText.length) { + setTimeout(animate, 10); + } else { + setIsLoading(false); + setPrompt(''); + } + } + animate(); + }; + + return ( + + +