From 9845a44e0aad7a8f3503a2638b22b32b2e539cc5 Mon Sep 17 00:00:00 2001 From: Stig Ofstad Date: Thu, 28 Dec 2023 14:29:30 +0100 Subject: [PATCH] fix(ListPlugin): 'selectFromScope' support relative addresses --- .../plugins/list/templates/task1.entity.json | 2 +- .../templates/uncontainedTaskList.entity.json | 4 +- .../dm-core-plugins/src/list/ListPlugin.tsx | 6 +- packages/dm-core/src/components/TreeView.tsx | 17 ++-- .../src/utils/addressUtilities.test.ts | 64 +++++++++++--- .../dm-core/src/utils/addressUtilities.ts | 87 +++++++++++++++---- 6 files changed, 136 insertions(+), 44 deletions(-) diff --git a/example/app/data/DemoDataSource/plugins/list/templates/task1.entity.json b/example/app/data/DemoDataSource/plugins/list/templates/task1.entity.json index dd0fddd05..70876fac7 100644 --- a/example/app/data/DemoDataSource/plugins/list/templates/task1.entity.json +++ b/example/app/data/DemoDataSource/plugins/list/templates/task1.entity.json @@ -1,5 +1,5 @@ { - "_id": "wash_task", + "_id": "wash_task_templates", "name": "Wash_the_car", "type": "./blueprints/Task", "description": "Car needs a thorough wash inside and outside.", diff --git a/example/app/data/DemoDataSource/plugins/list/templates/uncontainedTaskList.entity.json b/example/app/data/DemoDataSource/plugins/list/templates/uncontainedTaskList.entity.json index 02801026e..17f3cce6b 100644 --- a/example/app/data/DemoDataSource/plugins/list/templates/uncontainedTaskList.entity.json +++ b/example/app/data/DemoDataSource/plugins/list/templates/uncontainedTaskList.entity.json @@ -1,11 +1,11 @@ { - "_id": "uncontainedTaskList", + "_id": "uncontainedTaskList_templates", "name": "uncontainedTaskList", "type": "./blueprints/UncontainedTaskList", "task_list": [ { "type": "CORE:Reference", - "address": "/$wash_task", + "address": "/$wash_task_templates", "referenceType": "link" } ] diff --git a/packages/dm-core-plugins/src/list/ListPlugin.tsx b/packages/dm-core-plugins/src/list/ListPlugin.tsx index 6c0b92438..0fa36940c 100644 --- a/packages/dm-core-plugins/src/list/ListPlugin.tsx +++ b/packages/dm-core-plugins/src/list/ListPlugin.tsx @@ -16,6 +16,7 @@ import { TEntityPickerReturn, TemplateMenu, TTemplate, + resolveRelativeAddressSimplified, } from '@development-framework/dm-core' import { toast } from 'react-toastify' import { @@ -183,7 +184,10 @@ export const ListPlugin = (props: IUIPlugin & { config?: TListConfig }) => { showModal={showModal} setShowModal={setShowModal} typeFilter={type} - scope={config.selectFromScope} + scope={resolveRelativeAddressSimplified( + config.selectFromScope, + idReference + )} onChange={async (entities: TEntityPickerReturn[]) => { const newKeys: Record = {} for (const { address, entity } of entities) { diff --git a/packages/dm-core/src/components/TreeView.tsx b/packages/dm-core/src/components/TreeView.tsx index d1d2470df..943e8371d 100644 --- a/packages/dm-core/src/components/TreeView.tsx +++ b/packages/dm-core/src/components/TreeView.tsx @@ -46,6 +46,14 @@ export type TNodeWrapperProps = { const TypeIcon = (props: { node: TreeNode; expanded: boolean }) => { const { node, expanded } = props + if (node.type === 'error') + return ( + + ) + const showAsReference = node.parent?.type !== EBlueprint.PACKAGE && node?.type !== EBlueprint.PACKAGE && @@ -75,13 +83,6 @@ const TypeIcon = (props: { node: TreeNode; expanded: boolean }) => { } case 'dataSource': return - case 'error': - return ( - - ) case EBlueprint.BLUEPRINT: return case EBlueprint.PACKAGE: @@ -224,8 +225,8 @@ export const TreeView = (props: { includeTypes?: string[] // Types to include in the tree (excludes the 'ignoredTypes' option) }) => { const { nodes, onSelect, NodeWrapper, ignoredTypes, includeTypes } = props - if (includeTypes && includeTypes.length) { + includeTypes?.push('error') // Never hide error nodes return ( {nodes diff --git a/packages/dm-core/src/utils/addressUtilities.test.ts b/packages/dm-core/src/utils/addressUtilities.test.ts index 34ba6c34b..a2735a1bb 100644 --- a/packages/dm-core/src/utils/addressUtilities.test.ts +++ b/packages/dm-core/src/utils/addressUtilities.test.ts @@ -1,4 +1,8 @@ -import { resolveRelativeAddress, splitAddress } from './addressUtilities' +import { + resolveRelativeAddress, + resolveRelativeAddressSimplified, + splitAddress, +} from './addressUtilities' test('split dmss://test_source/complex/myCarRental.cars[0].engine', async () => { const { protocol, dataSource, documentPath, attributePath } = splitAddress( @@ -58,27 +62,35 @@ test('split test_source/$1', async () => { }) test('resolve address with ^', () => { - const resolved = resolveRelativeAddress('^.cars[0]', '$1', 'test_source') - expect(resolved).toBe('/test_source/$1.cars[0]') + const resolved = resolveRelativeAddressSimplified( + '^.cars[0]', + 'dmss://test_source/$1' + ) + expect(resolved).toBe('dmss://test_source/$1.cars[0]') }) test('resolve address with multiple attributes', () => { - const resolved = resolveRelativeAddress( + const resolved = resolveRelativeAddressSimplified( '$2.cars[0].engine', - '$1', - 'test_source' + 'dmss://test_source/$1' ) - expect(resolved).toBe('/test_source/$2.cars[0].engine') + expect(resolved).toBe('dmss://test_source/$2.cars[0].engine') }) test('resolve address with no attributes', () => { - const resolved = resolveRelativeAddress('$2', '$1', 'test_source') - expect(resolved).toBe('/test_source/$2') + const resolved = resolveRelativeAddressSimplified( + '$2', + 'dmss://test_source/$1' + ) + expect(resolved).toBe('dmss://test_source/$2') }) test('resolve address with id', () => { - const resolved = resolveRelativeAddress('$2.cars[0]', '$1', 'test_source') - expect(resolved).toBe('/test_source/$2.cars[0]') + const resolved = resolveRelativeAddressSimplified( + '$2.cars[0]', + 'dmss://test_source/$1' + ) + expect(resolved).toBe('dmss://test_source/$2.cars[0]') }) test('resolve address with slash and id', () => { @@ -107,10 +119,34 @@ test('resolve address with package path', () => { }) test('resolve address with data source', () => { - const resolved = resolveRelativeAddress( + const resolved = resolveRelativeAddressSimplified( 'dmss://other_source/$2.cars[0]', - '$1', - 'test_source' + 'whatever' ) expect(resolved).toBe('dmss://other_source/$2.cars[0]') }) + +test('resolve relative to parent address should raise error', () => { + function badReference() { + resolveRelativeAddressSimplified('~.cars[0]', 'dmss://test_source/$2') + } + expect(badReference).toThrowError( + 'Invalid relative reference. Cannot traverse out of uncontained parent' + ) +}) + +test('resolve relative to parent address', () => { + const resolved = resolveRelativeAddressSimplified( + '~.cars[0]', + 'dmss://test_source/$2.bravo' + ) + expect(resolved).toBe('dmss://test_source/$2.cars[0]') +}) + +test('resolve relative to parent address again', () => { + const resolved = resolveRelativeAddressSimplified( + '~.~.~.car_s[0]', + 'dmss://test_source/$2.t_y[2].char-lie' + ) + expect(resolved).toBe('dmss://test_source/$2.car_s[0]') +}) diff --git a/packages/dm-core/src/utils/addressUtilities.ts b/packages/dm-core/src/utils/addressUtilities.ts index b3a9ed78a..6960e08fd 100644 --- a/packages/dm-core/src/utils/addressUtilities.ts +++ b/packages/dm-core/src/utils/addressUtilities.ts @@ -5,7 +5,6 @@ import { splitString } from './stringUtilities' * * @param address An absolute address. Valid formats include (both DOCUMENT_PATH and ATTRIBUTE_PATH is optional): * PROTOCOL://DATA_SOURCE/DOCUMENT_PATH.ATTRIBUTE_PATH - * /DATA_SOURCE/DOCUMENT_PATH.ATTRIBUTE_PATH * DATA_SOURCE/DOCUMENT_PATH.ATTRIBUTE_PATH */ export const splitAddress = (address: string) => { @@ -24,43 +23,95 @@ export const splitAddress = (address: string) => { /** * - * @param address A relative address. Valid formats include (ATTRIBUTE_PATH is optional): + * @param relativeAddress A relative address. Valid formats include (ATTRIBUTE_PATH is optional): * PROTOCOL://DATA_SOURCE/DOCUMENT_PATH.ATTRIBUTE_PATH - * /DOCUMENT_PATH.ATTRIBUTE_PATH * DOCUMENT_PATH.ATTRIBUTE_PATH * ^.ATTRIBUTE_PATH - * @param fallbackDocumentPath + * @param knownPrefix * @param dataSource * @returns */ export const resolveRelativeAddress = ( - address: string, - fallbackDocumentPath: string, + relativeAddress: string, + knownPrefix: string, dataSource: string, relative_path?: string[] ) => { - address = address.replace(/^[/. ]+|[/. ]+$/g, '') - if (address.includes('://')) return address - const [documentPath, attributePath] = address.includes('.') - ? splitString(address, '.', 1) - : [address, ''] + relativeAddress = relativeAddress.replace(/^[/. ]+|[/. ]+$/g, '') + if (relativeAddress.includes('://')) return relativeAddress + const [documentPath, attributePath] = relativeAddress.includes('.') + ? splitString(relativeAddress, '.', 1) + : [relativeAddress, ''] if (documentPath === '~') { if (!relative_path) throw 'Missing relative path to be able to resolve the address' - let go_up = address.split('~').length - 1 + let go_up = relativeAddress.split('~').length - 1 const path = Array.from(relative_path) while (go_up !== 0) { path.pop() go_up -= 1 } - const rest = address.split('~', -1).slice(-1) + const rest = relativeAddress.split('~', -1).slice(-1) if (path.length > 0) - return `/${dataSource}/${fallbackDocumentPath}.${path.join('.')}${rest}` - else return `/${dataSource}/${fallbackDocumentPath}${rest}` + return `/${dataSource}/${knownPrefix}.${path.join('.')}${rest}` + else return `/${dataSource}/${knownPrefix}${rest}` } - return `/${dataSource}/${ - documentPath === '^' ? fallbackDocumentPath : documentPath - }${attributePath ? '.' + attributePath : ''}` + return `/${dataSource}/${documentPath === '^' ? knownPrefix : documentPath}${ + attributePath ? '.' + attributePath : '' + }` +} + +/** + * + * Aim's to have a simpler to use interface than resolveRelativeAddress + * + * @param relativeAddress An address, possibly relative . + * Valid formats include (ATTRIBUTE_PATH is optional): + * PROTOCOL://DATA_SOURCE/DOCUMENT_PATH.ATTRIBUTE_PATH + * $2.ATTRIBUTE_PATH + * ~.~.ATTRIBUTE_PATH + * ^.ATTRIBUTE_PATH + * @param location Where the relative address is being encountered. Must be an absolute address. + * @returns absoluteAddress + */ +export const resolveRelativeAddressSimplified = ( + relativeAddress: string, + location: string +): string => { + if (relativeAddress.includes('://')) return relativeAddress // It's absolute + + const { dataSource, documentPath, attributePath, protocol } = + splitAddress(location) + relativeAddress = relativeAddress.replace(/^[/. ]+|[/. ]+$/g, '') + + if (relativeAddress[0] === '~') { + let go_up = relativeAddress.split('~').length - 1 + + // Split a string like "$23.car[5].b" into ["$23", "car", "[5]", "b"] + const regex = /(\$[\w]+|[a-zA-Z0-9-_]+|\[\d+\])/g + const pathElements = `${documentPath}.${attributePath}`.match( + regex + ) as string[] + while (go_up !== 0) { + pathElements.pop() + go_up -= 1 + } + const rest = relativeAddress.split('~', -1).slice(-1) + if (!pathElements.length) + throw 'Invalid relative reference. Cannot traverse out of uncontained parent' + + return `${protocol}://${dataSource}/${pathElements.join('.')}${rest}` + } + if (relativeAddress[0] === '$') { + return `${protocol}://${dataSource}/${relativeAddress}` + } + if (relativeAddress[0] === '^') { + const attributes = relativeAddress.slice(1) + return `${protocol}://${dataSource}/${documentPath}${attributes}` + } + return `/${documentPath === '^' ? location : documentPath}${ + attributePath ? '.' + attributePath : '' + }` }