Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/studio/build/api/docs.jsonopenapi.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions assets/studio/build/api/openapi-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ const config: ConfigFile = {
{
pattern: 'bundleDataImporterConfigCalculateTransformationResultType',
type: 'query'
},
{
pattern: 'bundleDataImporterMappingLoadTransformationResult',
type: 'query'
},
{
pattern: 'bundleDataImporterMappingCalculateTransformationResultType',
type: 'query'
}
],
outputFiles: {
Expand Down
37 changes: 28 additions & 9 deletions assets/studio/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,35 @@

import { type Config } from 'jest'

const transform: Config['transform'] = {
'^.+\\.(t|j)sx?$': ['@swc/jest', {
jsc: {
parser: { syntax: 'typescript', tsx: true, decorators: true },
transform: { react: { runtime: 'automatic' } }
}
}]
}

// Studio and Data Hub are federated remotes at runtime; tests mock what they use from them, one stub per entry
const moduleNameMapper: Config['moduleNameMapper'] = {
'^@pimcore/studio-ui-bundle$': '<rootDir>/js/test/__mocks__/studio-ui-bundle/index',
'^@pimcore/studio-ui-bundle/(.+)$': '<rootDir>/js/test/__mocks__/studio-ui-bundle/$1',
'^@pimcore/data-hub$': '<rootDir>/js/test/__mocks__/data-hub/index'
}

const config: Config = {
testEnvironment: 'node',
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', {
jsc: {
parser: { syntax: 'typescript', tsx: true, decorators: true },
transform: { react: { runtime: 'automatic' } }
}
}]
}
collectCoverageFrom: ['js/src/**/*.{ts,tsx}', '!js/src/**/*.test.{ts,tsx}'],
projects: [
{ displayName: 'node', testEnvironment: 'node', testMatch: ['<rootDir>/js/**/*.test.ts'], transform, moduleNameMapper },
// components render into a DOM
{
displayName: 'dom',
testEnvironment: '<rootDir>/js/test/support/dom-environment.ts',
testMatch: ['<rootDir>/js/**/*.test.tsx'],
transform,
moduleNameMapper
}
]
}

export default config
11 changes: 10 additions & 1 deletion assets/studio/js/src/config/service-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,14 @@ export const bundleServiceIds = {
'DataImporter/DynamicTypes/DataTarget/Direct': 'DataImporter/DynamicTypes/DataTarget/Direct',
'DataImporter/DynamicTypes/DataTarget/Classificationstore': 'DataImporter/DynamicTypes/DataTarget/Classificationstore',
'DataImporter/DynamicTypes/DataTarget/ClassificationStoreBatch': 'DataImporter/DynamicTypes/DataTarget/ClassificationStoreBatch',
'DataImporter/DynamicTypes/DataTarget/ManyToManyRelation': 'DataImporter/DynamicTypes/DataTarget/ManyToManyRelation'
'DataImporter/DynamicTypes/DataTarget/ManyToManyRelation': 'DataImporter/DynamicTypes/DataTarget/ManyToManyRelation',

// The resolver step, the mapping dialog and the source it reads, for other bundles to host in their own form
'DataImporter/Components/ResolverStep': 'DataImporter/Components/ResolverStep',
'DataImporter/Components/AdvancedMappingModal': 'DataImporter/Components/AdvancedMappingModal',
'DataImporter/Components/PostedMappingSource': 'DataImporter/Components/PostedMappingSource',

// Converters between the stored configuration and the steps' form values
'DataImporter/Utils/TransformBackendToForm': 'DataImporter/Utils/TransformBackendToForm',
'DataImporter/Utils/TransformFormToBackend': 'DataImporter/Utils/TransformFormToBackend'
} as const
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DataSourceStep } from './steps/data-source-step'
import { PreviewImportStep } from './steps/preview-import-step'
import { ResolverStep } from './steps/resolver-step'
import { MappingStep } from './steps/mapping-step'
import { StoredConfigMappingSource } from './steps/mapping-source/stored-config-mapping-source'
import { ProcessingSettingsStep } from './steps/processing-settings-step'
import { useStyles } from './data-setup-tab.styles'
import { useColumnHeaderOptions } from '../../hooks/use-column-header-options'
Expand Down Expand Up @@ -54,50 +55,48 @@ export const DataSetupTab = ({ configName }: DataSetupTabProps): React.JSX.Eleme
const isMappingStep = currentStep === MAPPING_STEP_INDEX

return (
<Flex
className={ styles.tabLayout }
vertical
>
<Box margin={ { x: 'small' } }>
<Steps
current={ currentStep }
items={ steps }
onChange={ setCurrentStep }
size="small"
type="navigation"
/>
</Box>
<StoredConfigMappingSource configName={ configName }>
<Flex
className={ styles.tabLayout }
vertical
>
<Box margin={ { x: 'small' } }>
<Steps
current={ currentStep }
items={ steps }
onChange={ setCurrentStep }
size="small"
type="navigation"
/>
</Box>

<div className={ cn(styles.stepContentMapping, !isMappingStep && styles.stepContentMappingHidden) }>
<MappingStep
configName={ configName }
isActive={ isMappingStep }
/>
</div>
<div className={ cn(styles.stepContentMapping, !isMappingStep && styles.stepContentMappingHidden) }>
<MappingStep isActive={ isMappingStep } />
</div>

<div className={ cn(styles.stepContent, currentStep !== 0 && styles.stepContentHidden) }>
<DataSourceStep configName={ configName } />
</div>
<div className={ cn(styles.stepContent, currentStep !== 0 && styles.stepContentHidden) }>
<DataSourceStep configName={ configName } />
</div>

<div className={ cn(styles.stepContent, currentStep !== 1 && styles.stepContentHidden) }>
<PreviewImportStep
configName={ configName }
isActive={ currentStep === 1 }
onPreviewDataChange={ () => { setPreviewVersion((v) => v + 1) } }
/>
</div>
<div className={ cn(styles.stepContent, currentStep !== 1 && styles.stepContentHidden) }>
<PreviewImportStep
configName={ configName }
isActive={ currentStep === 1 }
onPreviewDataChange={ () => { setPreviewVersion((v) => v + 1) } }
/>
</div>

<div className={ cn(styles.stepContent, currentStep !== 2 && styles.stepContentHidden) }>
<ResolverStep
columnHeaderOptions={ columnHeaderOptions }
configName={ configName }
isActive={ currentStep === 2 }
/>
</div>
<div className={ cn(styles.stepContent, currentStep !== 2 && styles.stepContentHidden) }>
<ResolverStep
columnHeaderOptions={ columnHeaderOptions }
isActive={ currentStep === 2 }
/>
</div>

<div className={ cn(styles.stepContent, currentStep !== 4 && styles.stepContentHidden) }>
<ProcessingSettingsStep columnHeaderOptions={ columnHeaderOptions } />
</div>
</Flex>
<div className={ cn(styles.stepContent, currentStep !== 4 && styles.stepContentHidden) }>
<ProcessingSettingsStep columnHeaderOptions={ columnHeaderOptions } />
</div>
</Flex>
</StoredConfigMappingSource>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import React from 'react'
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
import { type ClassAttribute, type MappingConfigItem } from '../../../../types'
import { PostedMappingSource } from '../mapping-source/posted-mapping-source'
import { AdvancedMappingModal } from '.'
import type * as ResultPreviewModule from './result-preview/result-preview'

const typeQuery = jest.fn()
const resultQuery = jest.fn()
const attributesQuery = jest.fn()
const refetch = jest.fn(async () => { await Promise.resolve() })

jest.mock('../../../../data-importer-api-slice.gen', () => ({
useBundleDataImporterMappingCalculateTransformationResultTypeQuery: (...args: unknown[]) => typeQuery(...args),
useBundleDataImporterMappingLoadTransformationResultQuery: (...args: unknown[]) => resultQuery(...args),
useBundleDataImporterDataTypeLoadClassAttributesQuery: (...args: unknown[]) => attributesQuery(...args)
}))
jest.mock('@pimcore/studio-ui-bundle/app', () => ({ useTranslation: () => ({ t: (key: string) => key }) }))

// the pipeline editor is out of scope; the target step stand-in shows what the dialog hands it, next to the real result preview
jest.mock('./step-transformations/step-transformations', () => ({
StepTransformations: () => null
}))
jest.mock('./step-target/step-target', () => {
const { ResultPreview } = jest.requireActual<typeof ResultPreviewModule>('./result-preview/result-preview')
return {
StepTarget: ({ attributesMap, transformationResultType }: { attributesMap: Record<string, ClassAttribute[]>, transformationResultType?: string }) => (
<div data-testid="target">
<span data-testid="target-attributes">
{ Object.entries(attributesMap).map(([key, attributes]) => `${key}:${attributes.map(a => a.key).join(',')}`).join(';') }
</span>
<span data-testid="target-type">{ transformationResultType ?? '' }</span>
<ResultPreview />
</div>
)
}
})

const columns = [{ dataIndex: 'sku', label: 'SKU' }, { dataIndex: 'name' }]
const records = [{ sku: 'A-1', name: 'Chair' }, { sku: 'A-2', name: 'Table' }]
const item = {
id: 'm1',
label: 'SKU',
dataSourceIndex: ['sku'],
transformationPipeline: [{ type: 'trim', settings: { mode: 'both' } }]
} as unknown as MappingConfigItem

// RTK Query hands out the same result while its request is unchanged; effects in the dialog rely on that
const settled = (data?: unknown): Record<string, unknown> => ({ data, isLoading: false, isFetching: false, isError: false, isSuccess: data !== undefined, refetch })
const idle = settled()
const typeResult = settled({ type: 'numeric' })
const previewResult = settled({ transformationResultPreviews: ['trimmed A-1'] })
const attributesResult = settled({ attributes: [{ key: 'sku', title: 'SKU' }] })

const renderDialog = (attributesMap: Record<string, ClassAttribute[]> = {}): void => {
render(
<PostedMappingSource
columns={ columns }
configuration={ { mappingConfig: [item] } }
id="format"
records={ records }
>
<AdvancedMappingModal
attributesMap={ attributesMap }
classId="Product"
columnHeaderOptions={ [{ value: 'sku', label: 'SKU' }] }
item={ item }
onClose={ () => undefined }
onSave={ () => undefined }
open
/>
</PostedMappingSource>
)
}

beforeEach(() => {
typeQuery.mockImplementation((_request: unknown, options: { skip: boolean }) => options.skip ? idle : typeResult)
resultQuery.mockImplementation((_request: unknown, options: { skip: boolean }) => options.skip ? idle : previewResult)
attributesQuery.mockImplementation((_request: unknown, options: { skip: boolean }) => options.skip ? idle : attributesResult)
})

afterEach(() => {
cleanup()
jest.clearAllMocks()
})

describe('AdvancedMappingModal on a posted mapping source', () => {
it('previews the posted record and the transformation result of the edited entry', () => {
renderDialog()

expect(screen.getByText('A-1')).toBeTruthy()
expect(screen.getByText('Chair')).toBeTruthy()
expect(within(screen.getByTestId('target')).getByText('trimmed A-1')).toBeTruthy()
expect(resultQuery).toHaveBeenCalledWith(
{ bundleDataImporterTransformationResultParameters: { mappingConfig: [item], dataRow: records[0] } },
{ refetchOnMountOrArgChange: false, skip: false }
)
})

it('loads the default class attributes when its host has none', () => {
renderDialog()

expect(attributesQuery).toHaveBeenLastCalledWith(
{ classId: 'Product', transformationResultType: undefined, systemWrite: true },
{ skip: false }
)
expect(screen.getByTestId('target-attributes').textContent).toBe('__default__:sku')
})

it('uses the host\'s attributes when it has them', () => {
renderDialog({ __default__: [{ key: 'name', title: 'Name', localized: false }] })

expect(attributesQuery).toHaveBeenLastCalledWith(expect.anything(), { skip: true })
expect(screen.getByTestId('target-attributes').textContent).toBe('__default__:name')
})

it('posts the edited entry to the type endpoint on refresh and adopts the type', () => {
renderDialog()
expect(screen.getByTestId('target-type').textContent).toBe('')

fireEvent.click(screen.getByRole('button', { name: 'refresh' }))

expect(typeQuery).toHaveBeenCalledWith(
{
bundleDataImporterCalculateTransformationResultTypeParameters: {
currentConfig: { label: 'SKU', dataSourceIndex: ['sku'], transformationPipeline: item.transformationPipeline }
}
},
{ refetchOnMountOrArgChange: false, skip: false }
)
expect(screen.getByTestId('target-type').textContent).toBe('numeric')
})
})
Loading
Loading