Skip to content

Commit 323580f

Browse files
Merge pull request #1035 from contentstack/dev
Dev
2 parents 9c4881f + 55c2530 commit 323580f

13 files changed

Lines changed: 232 additions & 67 deletions

File tree

api/src/constants/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,6 @@ export const RESERVED_FIELD_MAPPINGS: Record<string, string> = {
326326
locale: 'cm_locale',
327327
// Add other reserved fields if needed
328328
};
329+
330+
export const MEDIA_BLOCK_NAMES = ['core/image', 'core/video', 'core/audio', 'core/file'];
331+
export const WORDPRESS_MISSSING_BLOCKS = 'core/missing';

api/src/services/contentful.service.ts

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,36 @@ const mapLocales = ({ masterLocale, locale, locales, isNull = false }: any) => {
9999
}
100100
}
101101

102+
function resolveEntryFieldKey(entry: Record<string, unknown>, baseKey: string): string | undefined {
103+
if (baseKey in entry) return baseKey;
104+
const snake = baseKey.replace(/([A-Z])/g, (m) => `_${m.toLowerCase()}`);
105+
if (snake in entry) return snake;
106+
return undefined;
107+
}
108+
109+
/**
110+
* Maps Contentful content type id → field id → whether that field is localized in the export schema.
111+
* Used so we only fan out values for fields with `localized: false`, not for localized fields that
112+
* happen to have a single locale in the entry (missing translations).
113+
*/
114+
function buildContentfulFieldLocalizedByContentType(
115+
contentTypesFromPackage: any[]
116+
): Map<string, Map<string, boolean>> {
117+
const byCt = new Map<string, Map<string, boolean>>();
118+
for (const ct of contentTypesFromPackage ?? []) {
119+
const ctId = ct?.sys?.id;
120+
if (!ctId) continue;
121+
const byField = new Map<string, boolean>();
122+
for (const f of ct?.fields ?? []) {
123+
if (f?.id != null) {
124+
byField.set(f.id, f.localized === true);
125+
}
126+
}
127+
byCt.set(ctId, byField);
128+
}
129+
return byCt;
130+
}
131+
102132
const transformCloudinaryObject = (input: any) => {
103133
const result: any = [];
104134
if (!Array.isArray(input)) {
@@ -777,6 +807,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
777807
const data = await fs.promises.readFile(packagePath, "utf8");
778808
const entries = JSON.parse(data)?.entries;
779809
const content = JSON.parse(data)?.contentTypes;
810+
const cfFieldLocalizedByCt = buildContentfulFieldLocalizedByContentType(content);
780811
const LocaleMapper = { masterLocale: project?.master_locale ?? LOCALE_MAPPER?.masterLocale, ...project?.locales ?? {} };
781812
if (entries && entries.length > 0) {
782813
const assetId = await readFile(assetsSave, ASSETS_SCHEMA_FILE) ?? [];
@@ -814,7 +845,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
814845
entryData[name][lang] ??= {};
815846
entryData[name][lang][id] ??= {};
816847
locales.push(lang);
817-
const fieldData = currentCT?.fieldMapping?.find?.((item: any) => (key === item?.uid) && (!["text", "url"]?.includes?.(item?.backupFieldType)));
848+
const fieldData = currentCT?.fieldMapping?.find?.((item: any) => key === item?.uid);
818849
const newId = fieldData?.contentstackFieldUid ?? `${key}`?.replace?.(/[^a-zA-Z0-9]+/g, "_");
819850
entryData[name][lang][id][newId] = processField(
820851
langValue,
@@ -860,18 +891,73 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
860891
);
861892
});
862893
});
894+
895+
// Non-localized Contentful fields (`localized: false` in the content type) only appear under
896+
// one locale in exports. Copy them to every other locale branch so each slice is complete.
897+
// Do not infer non-localized-ness from a single locale key — localized fields can legitimately
898+
// have only one locale when translations are missing.
899+
const entryLocaleKeys = new Set<string>();
900+
for (const [, v] of Object?.entries?.(fields)) {
901+
for (const lang of Object.keys(v as object)) {
902+
entryLocaleKeys.add(lang);
903+
}
904+
}
905+
const ct = contentTypes?.find((c: any) => c?.otherCmsUid === name);
906+
for (const [key, value] of Object?.entries?.(fields)) {
907+
const langs = Object?.keys(value as object);
908+
if (langs?.length !== 1) continue;
909+
const fd = ct?.fieldMapping?.find?.((item: any) => key === item?.uid);
910+
const localizedInCf = cfFieldLocalizedByCt.get(name)?.get(key);
911+
const explicitlyNonLocalized =
912+
localizedInCf === false ||
913+
(localizedInCf === undefined && fd?.advanced?.nonLocalizable === true);
914+
if (!explicitlyNonLocalized) continue;
915+
const srcLang = langs[0];
916+
const newId = fd?.contentstackFieldUid ?? `${key}`?.replace?.(/[^a-zA-Z0-9]+/g, "_");
917+
const srcEntry = entryData[name][srcLang]?.[id] as Record<string, unknown> | undefined;
918+
if (!srcEntry) continue;
919+
const fk = resolveEntryFieldKey(srcEntry, newId);
920+
if (fk === undefined) continue;
921+
for (const tgtLang of entryLocaleKeys) {
922+
if (tgtLang === srcLang) continue;
923+
entryData[name][tgtLang] ??= {};
924+
entryData[name][tgtLang][id] ??= {};
925+
const tgt = entryData[name][tgtLang][id] as Record<string, unknown>;
926+
if (tgt[fk] === undefined) {
927+
tgt[fk] = srcEntry[fk];
928+
}
929+
}
930+
}
931+
863932
return entryData;
864933
},
865934
{}
866935
);
867936
for await (const [newKey, values] of Object.entries(result)) {
868937
const currentCT = contentTypes?.find((ct: any) => ct?.otherCmsUid === newKey);
869938
const ctName = currentCT?.contentstackUid in mapperKeys ?
870-
mapperKeys?.[currentCT?.contentstackUid] : (currentCT?.contentstackUid ?? newKey.replace(/([A-Z])/g, "_$1").toLowerCase());
871-
for await (const [localeKey, localeValues] of Object.entries(
872-
values as { [key: string]: any }
873-
)) {
874-
const localeCode = mapLocales({ masterLocale: master_locale, locale: localeKey, locales: LocaleMapper, isNull: true });
939+
mapperKeys?.[currentCT?.contentstackUid] : (currentCT?.contentstackUid ?? newKey?.replace?.(/([A-Z])/g, "_$1")?.toLowerCase?.());
940+
const valuesByCfLocale = values as { [key: string]: { [uid: string]: Record<string, unknown> } };
941+
const mergedByDestinationLocale: { [localeCode: string]: { [uid: string]: Record<string, unknown> } } = {};
942+
for (const localeKey of Object.keys(valuesByCfLocale)) {
943+
const localeValues = valuesByCfLocale[localeKey];
944+
if (!localeValues) continue;
945+
const localeCode = mapLocales({
946+
masterLocale: master_locale,
947+
locale: localeKey,
948+
locales: LocaleMapper,
949+
isNull: true,
950+
});
951+
if (!localeCode) continue;
952+
mergedByDestinationLocale[localeCode] ??= {};
953+
for (const [uid, entry] of Object.entries(localeValues)) {
954+
mergedByDestinationLocale[localeCode][uid] = {
955+
...(mergedByDestinationLocale[localeCode][uid] ?? {}),
956+
...(entry ?? {}),
957+
};
958+
}
959+
}
960+
for await (const [localeCode, localeValues] of Object.entries(mergedByDestinationLocale)) {
875961
const chunks = makeChunks(localeValues);
876962
for (const [entryKey, entryValue] of Object.entries(localeValues)) {
877963
const message = getLogMessage(
@@ -883,18 +969,12 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
883969
}
884970
const refs: { [key: string]: any } = {};
885971
let chunkIndex = 1;
886-
if (localeCode) {
887-
const filePath = path.join(
888-
entriesSave,
889-
ctName,
890-
localeCode
891-
);
892-
for await (const [chunkId, chunkData] of Object.entries(chunks)) {
893-
refs[chunkIndex++] = `${chunkId}-entries.json`;
894-
await writeFile(filePath, `${chunkId}-entries.json`, chunkData);
895-
}
896-
await writeFile(filePath, ENTRIES_MASTER_FILE, refs);
972+
const filePath = path.join(entriesSave, ctName, localeCode);
973+
for await (const [chunkId, chunkData] of Object.entries(chunks)) {
974+
refs[chunkIndex++] = `${chunkId}-entries.json`;
975+
await writeFile(filePath, `${chunkId}-entries.json`, chunkData);
897976
}
977+
await writeFile(filePath, ENTRIES_MASTER_FILE, refs);
898978
}
899979
}
900980
} else {

api/src/services/wordpress.service.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { orgService } from "./org.service.js";
1414
import * as cheerio from 'cheerio';
1515
import { setupWordPressBlocks, stripHtmlTags } from "../utils/wordpressParseUtil.js";
1616
import { getMimeTypeFromExtension } from "../utils/mimeTypes.js";
17+
import { MEDIA_BLOCK_NAMES, WORDPRESS_MISSSING_BLOCKS } from "../constants/index.js";
1718

1819
const { JSDOM } = jsdom;
1920

@@ -124,6 +125,16 @@ function getLastUid(uid : string) {
124125
return uid?.split?.('.')?.[uid?.split?.('.')?.length - 1];
125126
}
126127

128+
129+
const resolvedBlockName = (block: any) => {
130+
if (block?.attrs?.metadata?.name) return block?.attrs?.metadata?.name;
131+
if (block?.blockName === WORDPRESS_MISSSING_BLOCKS) {
132+
return block?.attrs?.originalName || 'body';
133+
}
134+
if (MEDIA_BLOCK_NAMES?.includes?.(block?.blockName)) return 'media';
135+
return block?.blockName;
136+
}
137+
127138
async function createSchema(fields: any, blockJson : any, title: string, uid: string, assetData: any, duplicateBlockMappings?: Record<string, string>) {
128139
const schema : any = {
129140
title: title,
@@ -159,13 +170,13 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st
159170
// Process each block in blockJson to see if it matches any modular block child
160171
for (const block of blockJson) {
161172
try {
162-
const blockName = (block?.attrs?.metadata?.name?.toLowerCase() || getFieldName(block?.blockName?.toLowerCase()));
173+
const blockName = getFieldName(resolvedBlockName(block));
163174

164175
// Find which modular block child this block matches
165176
let matchingChildField = fields.find((childField: any) => {
166-
const fieldName = childField?.otherCmsField?.toLowerCase() ;
167-
168-
return (childField?.contentstackFieldType !== 'modular_blocks_child') && (blockName === fieldName)
177+
const fieldName = childField?.otherCmsField?.toLowerCase();
178+
const fieldType = childField?.otherCmsType?.toLowerCase();
179+
return (childField?.contentstackFieldType !== 'modular_blocks_child') && (blockName === fieldName || blockName === fieldType)
169180
});
170181

171182
let matchingModularBlockChild = modularBlockChildren.find((childField: any) => {
@@ -187,7 +198,8 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st
187198
//if (!matchingChildField) {
188199
matchingChildField = fields.find((childField: any) => {
189200
const fieldName = childField?.otherCmsField?.toLowerCase();
190-
return (childField?.contentstackFieldType !== 'modular_blocks_child') && (mappedName === fieldName);
201+
const fieldType = childField?.otherCmsType?.toLowerCase();
202+
return (childField?.contentstackFieldType !== 'modular_blocks_child') && (mappedName === fieldName || mappedName === fieldType);
191203
});
192204

193205
// }
@@ -206,12 +218,13 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st
206218
const childFieldUid = matchingModularBlockChild?.contentstackFieldUid || getLastUid(matchingModularBlockChild?.contentstackUid);
207219
const childField = fields.find((f: any) => {
208220
const fUid = f?.contentstackFieldUid || '';
209-
const fOtherCmsField = f?.otherCmsType?.toLowerCase();
210-
const childBlockName = matchingChildField ? matchingChildField?.otherCmsField?.toLowerCase() : (child?.attrs?.metadata?.name?.toLowerCase() || getFieldName(child?.blockName?.toLowerCase()));
221+
const fOtherCmsType = f?.otherCmsType?.toLowerCase();
222+
const fOtherCmsField = f?.otherCmsField?.toLowerCase();
223+
const childBlockName = matchingChildField ? matchingChildField?.otherCmsField?.toLowerCase() : (getFieldName(resolvedBlockName(child))?.toLowerCase() || getFieldName(resolvedBlockName(child)?.toLowerCase()));
211224
const childKey = getLastUid(f?.contentstackFieldUid);
212225
const alreadyPopulated = childrenObject[childKey] !== undefined && childrenObject[childKey] !== null;
213226
return fUid.startsWith(childFieldUid + '.') &&
214-
(fOtherCmsField === childBlockName) && (!alreadyPopulated || f?.advanced?.multiple === true);
227+
(fOtherCmsType === childBlockName || fOtherCmsField === childBlockName) && (!alreadyPopulated || f?.advanced?.multiple === true);
215228
});
216229

217230
if (childField) {
@@ -313,8 +326,9 @@ function processNestedGroup(child: any, childField: any, allFields: any[]): Reco
313326
child?.innerBlocks?.forEach((nestedChild: any, nestedIndex: number) => {
314327
try {
315328

329+
const nestedBlockName = (getFieldName(resolvedBlockName(nestedChild))?.toLowerCase() ?? getFieldName(resolvedBlockName(nestedChild)?.toLowerCase()))?.toLowerCase();
316330
const nestedChildField = nestedFields?.find((field: any) =>
317-
field?.otherCmsType?.toLowerCase() === (nestedChild?.attrs?.metadata?.name?.toLowerCase() ?? getFieldName(nestedChild?.blockName?.toLowerCase()))?.toLowerCase() && !nestedChildrenObject[getLastUid(field?.contentstackFieldUid)]?.length
331+
(field?.otherCmsType?.toLowerCase() === nestedBlockName || field?.otherCmsField?.toLowerCase() === nestedBlockName) && !nestedChildrenObject[getLastUid(field?.contentstackFieldUid)]?.length
318332
);
319333

320334
if (!nestedChildField) {
@@ -387,7 +401,7 @@ function formatChildByType(child: any, field: any, assetData: any) {
387401

388402
// Process attributes based on field type configuration
389403
//if (child?.attributes && typeof child.attributes === 'object') {
390-
const attrKey = getFieldName(child?.attr?.metadata?.name?.toLowerCase() || child?.blockName?.toLowerCase());
404+
const attrKey = getFieldName(getFieldName(resolvedBlockName(child))?.toLowerCase() || getFieldName(resolvedBlockName(child)?.toLowerCase()));
391405
try {
392406
const attrValue = child?.attrs?.innerHTML;
393407

@@ -477,6 +491,10 @@ function formatChildByType(child: any, field: any, assetData: any) {
477491
formatted = asset;
478492
break;
479493
}
494+
495+
case 'markdown':
496+
formatted = stripHtmlTags(child?.innerHTML);
497+
break;
480498
default:
481499
// Default formatting - preserve original structure with null check
482500
formatted = attrValue;
@@ -580,6 +598,7 @@ async function saveEntry(fields: any, entry: any, file_path: string, assetData
580598
// Extract individual content encoded for this specific item
581599
const contentEncoded = $(xmlItem)?.find("content\\:encoded")?.text() || '';
582600
const blocksJson = await setupWordPressBlocks(contentEncoded);
601+
583602
customLogger(project?.id, destinationStackId,'info', `Processed blocks for entry ${uid}`);
584603

585604

ui/package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"sass": "^1.68.0",
2525
"socket.io-client": "^4.7.5",
2626
"typescript": "^4.9.5",
27-
"vite": "^7.3.1",
27+
"vite": "^7.3.2",
2828
"vite-tsconfig-paths": "^6.1.1"
2929
},
3030
"scripts": {

ui/src/components/Common/AddStack/addStack.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const AddStack = (props: any): JSX.Element => {
5656
locale: formData?.locale?.value || props?.defaultValues?.locale
5757
});
5858

59-
if (resp) {
59+
if (resp === true) {
6060
Notification({
6161
notificationContent: { text: 'Stack created successfully' },
6262
notificationProps: {
@@ -67,8 +67,9 @@ const AddStack = (props: any): JSX.Element => {
6767
});
6868
props?.closeModal();
6969
} else {
70+
7071
Notification({
71-
notificationContent: { text: 'Stack creation failed. Please try again.' },
72+
notificationContent: { text: resp },
7273
notificationProps: {
7374
position: 'bottom-center',
7475
hideProgressBar: true

ui/src/components/ContentMapper/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ const Fields: MappingFields = {
230230
'taxonomy':{
231231
label: 'Taxonomy',
232232
options: {'Taxonomy':'taxonomy'},
233-
type:'taxonomy'
233+
type:''
234234
}
235235
}
236236
type contentMapperProps = {

ui/src/components/DestinationStack/Actions/LoadStacks.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ interface LoadFileFormatProps {
3535
handleStepChange: (stepIndex: number, closeStep?: boolean) => void;
3636
}
3737

38+
interface ErrorObject {
39+
error_message?: string;
40+
errors?: Errors;
41+
}
42+
interface Errors {
43+
org_uid?: string[];
44+
}
45+
46+
3847
const defaultStack = {
3948
description: 'Created from Migration Destination Stack Step',
4049
locale: '',
@@ -108,6 +117,21 @@ const LoadStacks = (props: LoadFileFormatProps) => {
108117
// setAllStack(newMigrationData?.destination_stack?.stackArray)
109118
}, [newMigrationData?.destination_stack?.selectedStack]);
110119

120+
/**
121+
* Function to format the error message
122+
*/
123+
const formatErrorMessage = (errorData: ErrorObject) => {
124+
let message = errorData.error_message;
125+
126+
if (errorData.errors) {
127+
Object.entries(errorData.errors).forEach(([key, value]) => {
128+
message += `\n${key}: ${(value as string[]).join(", ")}`;
129+
});
130+
}
131+
132+
return message;
133+
}
134+
111135
//Handle new stack details
112136
const handleOnSave = async (data: Stack) => {
113137
try {
@@ -159,8 +183,12 @@ const LoadStacks = (props: LoadFileFormatProps) => {
159183
setIsStackLoading(false);
160184
return true;
161185
}
162-
} catch (error) {
163-
return error;
186+
else {
187+
const errorMessage = formatErrorMessage(resp?.data?.data);
188+
return errorMessage;
189+
}
190+
} catch (error: any) {
191+
return error?.response?.data;
164192
}
165193
};
166194

0 commit comments

Comments
 (0)