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
28 changes: 27 additions & 1 deletion upload-api/migration-sap-smartedit/libs/contentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import config from '../config/index.json';
import { CT, DataConfig, Field } from '../interface/interface';
import mapField, { baseField } from './schemaMapper';
import { ensureDir, writeJson, parseImpexPath, ImpexColumnDef } from '../utils/helper';
import { ensureDir, writeJson, parseImpexPath, ImpexColumnDef, ImpexTypeBlock } from '../utils/helper';

const { contentTypes: contentTypesConfig } = config.modules;
const contentTypeFolderPath = path.resolve(config.data, contentTypesConfig.dirName);
Expand Down Expand Up @@ -252,6 +252,31 @@ function ensureMandatoryFields(fieldMapping: Field[]): void {
urlField.advanced = { ...urlField.advanced, mandatory: true };
}

/**
* The api writes the source id as the entry title when a row's label is blank or
* shared by several items (two CMSNavigationNodes both titled "Accessories"), so
* titles stay unique. That overwrote the only copy of the label: the SAP text was
* lost, and a storefront could not tell "untitled in SAP" from a real title. When
* any row would fall back like that, keep the label column as its own field too.
*/
function keepSourceLabel(fieldMapping: Field[], block: ImpexTypeBlock): void {
const titleField = fieldMapping.find((f) => f.contentstackFieldUid === 'title');
const col = titleField?.otherCmsField;
if (!col || !block.columns.has(col)) return; // synthesized title: no label column to keep
const owners = new Map<string, Set<string>>();
let blank = false;
for (const row of block.rows) {
const label = row[col];
if (!label) { blank = true; continue; }
if (!owners.has(label)) owners.set(label, new Set());
owners.get(label)!.add(row.uid ?? row.code ?? JSON.stringify(row));
}
if (!blank && [...owners.values()].every((ids) => ids.size === 1)) return;
const field = baseField(`source_${col}`, 'string', 'single_line_text');
field.otherCmsField = col;
fieldMapping.push(field);
}

/**
* Parse the ImpEx export into Contentstack content-type schemas.
*
Expand Down Expand Up @@ -327,6 +352,7 @@ async function extractContentTypes(
}

ensureMandatoryFields(fieldMapping);
keepSourceLabel(fieldMapping, block);

const contentType = {
otherCmsTitle: type,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
INSERT_UPDATE CMSNavigationNode; uid[unique=true]; title[lang=en]; parent(uid)
; AccessoriesNavNode; Accessories; Root
; AccessoriesCatNavNode; Accessories; Root
; LeafNavNode; ; Root
; BrandsNavNode; Brands; Root

INSERT_UPDATE CMSParagraphComponent; uid[unique=true]; name; content
; para1; First Paragraph; <p>a</p>
; para2; Second Paragraph; <p>b</p>

INSERT_UPDATE CMSNavigationEntry; uid[unique=true]; navigationNode(uid); item(uid)
; entry1; LeafNavNode; para1
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import path from 'path';
import fs from 'fs';
import { extractContentTypes } from '../../../migration-sap-smartedit/index';

/**
* The api titles an entry by its source id when the label is blank or shared, which
* overwrote the only copy of the SAP label (two nav nodes titled "Accessories" lost
* it). Types where that fallback can happen keep the label in its own field.
*/
const FIXTURE = path.join(__dirname, '../../fixtures/sap-smartedit-source-label/nav-node-label.impex');

let cts: any[] = [];
const fields = (type: string) => cts.find((c) => c.otherCmsTitle === type)?.fieldMapping ?? [];

beforeAll(async () => {
cts = (await extractContentTypes('cs', FIXTURE, {} as any)) as any[];
});

afterAll(() => {
fs.rmSync(path.join(process.cwd(), 'cmsMigrationData'), { recursive: true, force: true });
});

describe('keepSourceLabel — the label survives a title that falls back to the id', () => {
it('adds source_title, read from the title column, when labels are shared or blank', () => {
const f = fields('CMSNavigationNode').find((x: any) => x.contentstackFieldUid === 'source_title');
expect(f?.otherCmsField).toBe('title');
expect(f?.contentstackFieldType).toBe('single_line_text');
});

it('keeps title itself mapped to the same column', () => {
expect(fields('CMSNavigationNode').find((x: any) => x.contentstackFieldUid === 'title')?.otherCmsField).toBe('title');
});

it('adds nothing when the type has no label column (title is synthesized)', () => {
expect(fields('CMSNavigationEntry').some((x: any) => x.contentstackFieldUid.startsWith('source_'))).toBe(false);
});

it('adds nothing when every label is present and unique', () => {
expect(fields('CMSParagraphComponent').some((x: any) => x.contentstackFieldUid.startsWith('source_'))).toBe(false);
});
});
Loading