From fac18b626c52ee0299fba66061d9d85b5c8936ea Mon Sep 17 00:00:00 2001 From: Lukas Giger Date: Tue, 17 Dec 2024 13:51:46 +0100 Subject: [PATCH] XIVY-15648 import when manually adding variable When manually adding a variable that is present in a required project, the known variable is imported instead. --- .../variables/detail/DetailContent.tsx | 17 +- .../components/variables/dialog/AddDialog.tsx | 19 +- .../variables/dialog/OverwriteDialog.tsx | 33 +--- .../variables/dialog/known-variables.test.ts | 177 +++++++++++++++++- .../variables/dialog/known-variables.ts | 39 ++++ .../tests/integration/mock/import.spec.ts | 36 ++-- 6 files changed, 262 insertions(+), 59 deletions(-) diff --git a/packages/variable-editor/src/components/variables/detail/DetailContent.tsx b/packages/variable-editor/src/components/variables/detail/DetailContent.tsx index 5fc82ae8..1b852a99 100644 --- a/packages/variable-editor/src/components/variables/detail/DetailContent.tsx +++ b/packages/variable-editor/src/components/variables/detail/DetailContent.tsx @@ -1,28 +1,23 @@ import { BasicField, BasicInput, Flex, PanelMessage, ReadonlyProvider, Textarea, useReadonly } from '@axonivy/ui-components'; -import { EMPTY_KNOWN_VARIABLES, type KnownVariables } from '@axonivy/variable-editor-protocol'; +import { EMPTY_KNOWN_VARIABLES } from '@axonivy/variable-editor-protocol'; import { useMemo } from 'react'; import { useAppContext } from '../../../context/AppContext'; import { useMeta } from '../../../context/useMeta'; import { getNode, getNodesOnPath, updateNode, hasChildren as variableHasChildren } from '../../../utils/tree/tree-data'; import { type VariableUpdates } from '../data/variable'; +import { findKnownVariable } from '../dialog/known-variables'; import './DetailContent.css'; import { Metadata } from './Metadata'; import { Value } from './Value'; export const useOverwrites = () => { const { context, variables, selectedVariable } = useAppContext(); - let currentNode: KnownVariables | undefined = useMeta('meta/knownVariables', context, EMPTY_KNOWN_VARIABLES).data; - if (currentNode === undefined || currentNode.children.length === 0) { + const knownVariables = useMeta('meta/knownVariables', context, EMPTY_KNOWN_VARIABLES).data; + if (knownVariables.children.length === 0) { return false; } - const variableNodes = getNodesOnPath(variables, selectedVariable); - for (const variableNode of variableNodes) { - currentNode = currentNode.children.find(child => child.name === variableNode?.name); - if (!currentNode) { - return false; - } - } - return true; + const key = getNodesOnPath(variables, selectedVariable).map(node => (node ? node.name : '')); + return findKnownVariable(knownVariables, ...key) !== undefined; }; export const VariablesDetailContent = () => { diff --git a/packages/variable-editor/src/components/variables/dialog/AddDialog.tsx b/packages/variable-editor/src/components/variables/dialog/AddDialog.tsx index 891531eb..730d0d3f 100644 --- a/packages/variable-editor/src/components/variables/dialog/AddDialog.tsx +++ b/packages/variable-editor/src/components/variables/dialog/AddDialog.tsx @@ -14,21 +14,24 @@ import { selectRow } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; +import { EMPTY_KNOWN_VARIABLES } from '@axonivy/variable-editor-protocol'; import { type Table } from '@tanstack/react-table'; import { useMemo, useState } from 'react'; import { useAppContext } from '../../../context/AppContext'; +import { useMeta } from '../../../context/useMeta'; import { keyOfFirstSelectedNonLeafRow, keysOfAllNonLeafRows, newNodeName, subRowNamesOfRow, toRowId } from '../../../utils/tree/tree'; import { addNode } from '../../../utils/tree/tree-data'; import { validateName, validateNamespace } from '../data/validation-utils'; import { createVariable, type Variable } from '../data/variable'; import './AddDialog.css'; +import { addKnownVariable, findKnownVariable } from './known-variables'; type AddVariableDialogProps = { table: Table; }; export const AddVariableDialog = ({ table }: AddVariableDialogProps) => { - const { variables, setVariables, setSelectedVariable } = useAppContext(); + const { context, variables, setVariables, setSelectedVariable } = useAppContext(); const [name, setName] = useState(''); const [namespace, setNamespace] = useState(''); @@ -44,13 +47,23 @@ export const AddVariableDialog = ({ table }: AddVariableDialogProps) => { const namespaceOptions = () => keysOfAllNonLeafRows(table).map(key => ({ value: key })); - const addVariable = () => + const knownVariables = useMeta('meta/knownVariables', context, EMPTY_KNOWN_VARIABLES).data; + + const addVariable = () => { + const namespaceKey = namespace ? namespace.split('.') : []; + const overwrittenVariable = findKnownVariable(knownVariables, ...namespaceKey, name); setVariables(old => { - const addNodeReturnValue = addNode(name, namespace, old, createVariable); + let addNodeReturnValue; + if (overwrittenVariable) { + addNodeReturnValue = addKnownVariable(old, overwrittenVariable); + } else { + addNodeReturnValue = addNode(name, namespace, old, createVariable); + } selectRow(table, toRowId(addNodeReturnValue.newNodePath)); setSelectedVariable(addNodeReturnValue.newNodePath); return addNodeReturnValue.newData; }); + }; const allInputsValid = () => !nameValidationMessage && !namespaceValidationMessage; diff --git a/packages/variable-editor/src/components/variables/dialog/OverwriteDialog.tsx b/packages/variable-editor/src/components/variables/dialog/OverwriteDialog.tsx index cff37ecd..e9471175 100644 --- a/packages/variable-editor/src/components/variables/dialog/OverwriteDialog.tsx +++ b/packages/variable-editor/src/components/variables/dialog/OverwriteDialog.tsx @@ -5,11 +5,9 @@ import { type Table } from '@tanstack/react-table'; import { useState } from 'react'; import { useAppContext } from '../../../context/AppContext'; import { toRowId } from '../../../utils/tree/tree'; -import { addNode } from '../../../utils/tree/tree-data'; -import type { AddNodeReturnType } from '../../../utils/tree/types'; -import { isMetadata, type Metadata } from '../data/metadata'; -import { createVariable, type Variable } from '../data/variable'; +import { type Variable } from '../data/variable'; import { VariableBrowser } from './VariableBrowser'; +import { addKnownVariable } from './known-variables'; type OverwriteProps = { table: Table; @@ -23,7 +21,7 @@ export const OverwriteDialog = ({ table }: OverwriteProps) => { return; } setVariables(old => { - const addNodeReturnValue = addVariable(old, node); + const addNodeReturnValue = addKnownVariable(old, node); selectRow(table, toRowId(addNodeReturnValue.newNodePath)); setSelectedVariable(addNodeReturnValue.newNodePath); return addNodeReturnValue.newData; @@ -51,28 +49,3 @@ export const OverwriteDialog = ({ table }: OverwriteProps) => { ); }; - -const addVariable = (variables: Array, node: KnownVariables): AddNodeReturnType => { - let metadata: Metadata = { type: '' }; - const nodeMetaData = node.metaData; - if (isMetadata(nodeMetaData)) { - metadata = nodeMetaData; - } - let returnValue = addNode(node.name, node.namespace, variables, name => { - if (name === node.name) { - return { - name, - value: node.value, - children: [], - description: node.description, - metadata - }; - } - return createVariable(name); - }); - const newNodePath = returnValue.newNodePath; - for (const child of node.children) { - returnValue = addVariable(returnValue.newData, child); - } - return { newData: returnValue.newData, newNodePath }; -}; diff --git a/packages/variable-editor/src/components/variables/dialog/known-variables.test.ts b/packages/variable-editor/src/components/variables/dialog/known-variables.test.ts index b40cec08..309d7921 100644 --- a/packages/variable-editor/src/components/variables/dialog/known-variables.test.ts +++ b/packages/variable-editor/src/components/variables/dialog/known-variables.test.ts @@ -1,5 +1,6 @@ -import { EMPTY_KNOWN_VARIABLES, type KnownVariables } from '@axonivy/variable-editor-protocol'; -import { toNodes } from './known-variables'; +import { EMPTY_KNOWN_VARIABLES, type KnownVariables, type MetaData } from '@axonivy/variable-editor-protocol'; +import type { Variable } from '../data/variable'; +import { addKnownVariable, findKnownVariable, toNodes } from './known-variables'; const knownVariables: KnownVariables = { namespace: '', @@ -37,6 +38,22 @@ const knownVariables: KnownVariables = { metaData: { type: 'string' }, description: 'Access key to access amazon comprehend', children: [] + }, + { + namespace: 'Amazon.Comprehend', + name: 'Enum', + value: 'two', + metaData: { type: 'enum', values: ['one', 'two', 'three'] } as MetaData, + description: '', + children: [] + }, + { + namespace: 'Amazon.Comprehend', + name: 'File', + value: '', + metaData: { type: 'file', extension: 'json' } as MetaData, + description: '', + children: [] } ] } @@ -55,7 +72,161 @@ test('toNodes', () => { expect(root.children).toHaveLength(1); const node = root.children[0]; expect(node).toMatchObject({ value: 'Comprehend', icon: 'folder-open', info: 'Amazon comprehend connector settings' }); - expect(node.children).toHaveLength(2); + expect(node.children).toHaveLength(4); expect(node.children[0]).toMatchObject({ value: 'SecretKey', icon: 'password', info: 'Secret key to access amazon comprehend' }); expect(node.children[1]).toMatchObject({ value: 'AccessKey', icon: 'quote', info: 'Access key to access amazon comprehend' }); + expect(node.children[2]).toMatchObject({ value: 'Enum', icon: 'list', info: '' }); + expect(node.children[3]).toMatchObject({ value: 'File', icon: 'note', info: '' }); +}); + +describe('findKnownVariable', () => { + test('known variables is empty', () => { + const knownVariables = { children: [] as Array } as KnownVariables; + expect(findKnownVariable(knownVariables, 'some', 'key')).toBeUndefined(); + }); + + test('find folder', () => { + expect(findKnownVariable(knownVariables, 'Amazon', 'Comprehend')).toEqual(knownVariables.children[0].children[0]); + }); + + test('find leaf', () => { + expect(findKnownVariable(knownVariables, 'Amazon', 'Comprehend', 'AccessKey')).toEqual( + knownVariables.children[0].children[0].children[1] + ); + }); + + test('variable does not exist', () => { + expect(findKnownVariable(knownVariables, 'notFound')).toBeUndefined(); + }); +}); + +describe('addKnownVariable', () => { + test('leaf', () => { + const variables = [{ name: 'Variable' }] as Array; + const originalVariables = structuredClone(variables); + const addNodeReturnValue = addKnownVariable(variables, knownVariables.children[0].children[0].children[0]); + const newData = addNodeReturnValue.newData; + const newNodePath = addNodeReturnValue.newNodePath; + expect(variables).toEqual(originalVariables); + expect(newData).not.toBe(variables); + expect(newNodePath).toEqual([1, 0, 0]); + expect(newData).toEqual([ + { name: 'Variable' }, + { + name: 'Amazon', + value: '', + description: '', + metadata: { type: '' }, + children: [ + { + name: 'Comprehend', + value: '', + description: '', + metadata: { type: '' }, + children: [ + { + name: 'SecretKey', + value: '', + description: 'Secret key to access amazon comprehend', + metadata: { type: 'password' }, + children: [] + } + ] + } + ] + } + ]); + }); + + test('folder', () => { + const variables = [{ name: 'Variable' }] as Array; + const originalVariables = structuredClone(variables); + const addNodeReturnValue = addKnownVariable(variables, knownVariables.children[0].children[0]); + const newData = addNodeReturnValue.newData; + const newNodePath = addNodeReturnValue.newNodePath; + expect(variables).toEqual(originalVariables); + expect(newData).not.toBe(variables); + expect(newNodePath).toEqual([1, 0]); + expect(newData).toEqual([ + { name: 'Variable' }, + { + name: 'Amazon', + value: '', + description: '', + metadata: { type: '' }, + children: [ + { + name: 'Comprehend', + value: '', + description: 'Amazon comprehend connector settings', + metadata: { type: '' }, + children: [ + { + name: 'SecretKey', + value: '', + description: 'Secret key to access amazon comprehend', + metadata: { type: 'password' }, + children: [] + }, + { + name: 'AccessKey', + value: '', + description: 'Access key to access amazon comprehend', + metadata: { type: '' }, + children: [] + }, + { + name: 'Enum', + value: 'two', + description: '', + metadata: { type: 'enum', values: ['one', 'two', 'three'] }, + children: [] + }, + { + name: 'File', + value: '', + description: '', + metadata: { type: 'file', extension: 'json' }, + children: [] + } + ] + } + ] + } + ]); + }); + + test('parent already exists', () => { + const variables = [ + { name: 'Variable' }, + { name: 'Amazon', children: [{ name: 'Comprehend', children: [] as Array }] } + ] as Array; + const originalVariables = structuredClone(variables); + const addNodeReturnValue = addKnownVariable(variables, knownVariables.children[0].children[0].children[0]); + const newData = addNodeReturnValue.newData; + const newNodePath = addNodeReturnValue.newNodePath; + expect(variables).toEqual(originalVariables); + expect(newData).not.toBe(variables); + expect(newNodePath).toEqual([1, 0, 0]); + expect(newData).toEqual([ + { name: 'Variable' }, + { + name: 'Amazon', + children: [ + { + name: 'Comprehend', + children: [ + { + name: 'SecretKey', + value: '', + description: 'Secret key to access amazon comprehend', + metadata: { type: 'password' }, + children: [] + } + ] + } + ] + } + ]); + }); }); diff --git a/packages/variable-editor/src/components/variables/dialog/known-variables.ts b/packages/variable-editor/src/components/variables/dialog/known-variables.ts index 8e5f744f..4e86c154 100644 --- a/packages/variable-editor/src/components/variables/dialog/known-variables.ts +++ b/packages/variable-editor/src/components/variables/dialog/known-variables.ts @@ -1,5 +1,8 @@ import type { BrowserNode } from '@axonivy/ui-components'; import type { KnownVariables } from '@axonivy/variable-editor-protocol'; +import { addNode } from '../../../utils/tree/tree-data'; +import { isMetadata, type Metadata } from '../data/metadata'; +import { createVariable, type Variable } from '../data/variable'; import { nodeIcon } from '../data/variable-utils'; export const toNodes = (root?: KnownVariables): Array => { @@ -21,3 +24,39 @@ const toNode = (node: KnownVariables): BrowserNode => { children }; }; + +export const findKnownVariable = (node: KnownVariables, ...key: Array) => { + let currentNode: KnownVariables | undefined = node; + for (const part of key) { + currentNode = currentNode.children.find(child => child.name === part); + if (!currentNode) { + return; + } + } + return currentNode; +}; + +export const addKnownVariable = (variables: Array, node: KnownVariables) => { + let metadata: Metadata = { type: '' }; + const nodeMetaData = node.metaData; + if (isMetadata(nodeMetaData)) { + metadata = nodeMetaData; + } + let returnValue = addNode(node.name, node.namespace, variables, name => { + if (name === node.name) { + return { + name, + value: node.value, + children: [], + description: node.description, + metadata + }; + } + return createVariable(name); + }); + const newNodePath = returnValue.newNodePath; + for (const child of node.children) { + returnValue = addKnownVariable(returnValue.newData, child); + } + return { newData: returnValue.newData, newNodePath }; +}; diff --git a/playwright/tests/integration/mock/import.spec.ts b/playwright/tests/integration/mock/import.spec.ts index 13de691b..ba40a6ee 100644 --- a/playwright/tests/integration/mock/import.spec.ts +++ b/playwright/tests/integration/mock/import.spec.ts @@ -2,9 +2,14 @@ import { expect, test } from '@playwright/test'; import { describe } from 'node:test'; import { VariableEditor } from '../../pageobjects/VariableEditor'; +let editor: VariableEditor; + +test.beforeEach(async ({ page }) => { + editor = await VariableEditor.openMock(page); +}); + describe('importAndOverwrite', async () => { - test('password', async ({ page }) => { - const editor = await VariableEditor.openMock(page); + test('password', async () => { await editor.tree.expectRowCount(11); const overwrite = editor.overwrite; @@ -22,8 +27,7 @@ describe('importAndOverwrite', async () => { await details.expectValues('SecretKey', '', 'Secret key to access amazon comprehend', 'Password'); }); - test('enum has values', async ({ page }) => { - const editor = await VariableEditor.openMock(page); + test('enum has values', async () => { await editor.overwrite.open(); await editor.overwrite.variables.row(2).expand(); await editor.overwrite.variables.row(3).click(); @@ -31,8 +35,7 @@ describe('importAndOverwrite', async () => { await editor.details.listOfPossibleValues.expectValues('one', 'two', 'three'); }); - test('file has extension', async ({ page }) => { - const editor = await VariableEditor.openMock(page); + test('file has extension', async () => { await editor.overwrite.open(); await editor.overwrite.variables.row(2).expand(); await editor.overwrite.variables.row(4).click(); @@ -41,8 +44,7 @@ describe('importAndOverwrite', async () => { }); }); -test('importAndOverwriteWholeSubTree', async ({ page }) => { - const editor = await VariableEditor.openMock(page); +test('importAndOverwriteWholeSubTree', async () => { const tree = editor.tree; await tree.expectRowCount(11); @@ -67,8 +69,7 @@ test('importAndOverwriteWholeSubTree', async ({ page }) => { }); describe('disabledMetadataOfOverwrittenVariable', async () => { - test('enum', async ({ page }) => { - const editor = await VariableEditor.openMock(page); + test('enum', async () => { await editor.overwrite.open(); await editor.overwrite.variables.row(2).click(); await editor.overwrite.importBtn.click(); @@ -77,8 +78,7 @@ describe('disabledMetadataOfOverwrittenVariable', async () => { await editor.details.listOfPossibleValues.expectToBeDisabled(); }); - test('file', async ({ page }) => { - const editor = await VariableEditor.openMock(page); + test('file', async () => { await editor.overwrite.open(); await editor.overwrite.variables.row(2).click(); await editor.overwrite.importBtn.click(); @@ -87,3 +87,15 @@ describe('disabledMetadataOfOverwrittenVariable', async () => { await expect(editor.details.fileNameExtension.locator).toBeDisabled(); }); }); + +test('import variable when manually adding a known variable', async () => { + await editor.addVariable('Comprehend', 'Amazon'); + await editor.details.expectFolderValues('Comprehend', 'Amazon comprehend connector settings'); + await editor.tree.expectRowCount(15); + await editor.tree.row(11).click(); + await editor.details.expectFolderValues('Amazon', ''); + await editor.tree.row(13).click(); + await editor.details.expectValues('SecretKey', '', 'Secret key to access amazon comprehend', 'Password'); + await editor.tree.row(14).click(); + await editor.details.expectValues('AccessKey', '', 'Access key to access amazon comprehend', 'Default'); +});