diff --git a/.talismanrc b/.talismanrc index ce5b6fb4e..5fd1ae8e6 100644 --- a/.talismanrc +++ b/.talismanrc @@ -68,6 +68,10 @@ fileignoreconfig: - filename: upload-api/src/config/index.ts checksum: bd1465785804b3e3942d79f4424498bec838e5aba431c715eb419f3d39cf8d30 + - filename: ui/src/components/ContentMapper/index.tsx + checksum: 376fc21e84880c760fab7af4b1bb653f526548f962126c1db7551d036eab765d + - filename: api/src/services/taxonomy.service.ts + checksum: bd2344e7277b41c7eb29c50504c88debf9a86d198c2508dea90d9a98f53d89e9 diff --git a/api/src/constants/index.ts b/api/src/constants/index.ts index b0efbcc78..f782930ec 100644 --- a/api/src/constants/index.ts +++ b/api/src/constants/index.ts @@ -230,6 +230,8 @@ export const MIGRATION_DATA_CONFIG = { EXTENSION_APPS_FILE_NAME: "extensions.json", REFERENCES_DIR_NAME: "reference", REFERENCES_FILE_NAME: "reference.json", + TAXONOMIES_DIR_NAME: "taxonomies", + TAXONOMIES_FILE_NAME: "taxonomies.json", RTE_REFERENCES_DIR_NAME: "rteReference", RTE_REFERENCES_FILE_NAME: "rteReference.json", diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index d10bf1638..9ae3bb4db 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -32,6 +32,7 @@ import { marketPlaceAppService } from './marketplace.service.js'; import { extensionService } from './extension.service.js'; import fsPromises from 'fs/promises'; import { matchesSearchText } from '../utils/search.util.js'; +import { taxonomyService } from './taxonomy.service.js'; // import { getSafePath } from "../utils/sanitize-path.utils.js"; /** @@ -324,6 +325,14 @@ const startTestMigration = async (req: Request): Promise => { await extensionService?.createExtension({ destinationStackId: project?.current_test_stack_id, }); + await taxonomyService?.createTaxonomy({ + orgId, + projectId, + stackId:project?.destination_stack_id, + current_test_stack_id: project?.current_test_stack_id, + region, + userId: user_id,}) + switch (cms) { case CMS.SITECORE_V8: case CMS.SITECORE_V9: diff --git a/api/src/services/taxonomy.service.ts b/api/src/services/taxonomy.service.ts new file mode 100644 index 000000000..c2215fde5 --- /dev/null +++ b/api/src/services/taxonomy.service.ts @@ -0,0 +1,210 @@ +import { getLogMessage, safePromise } from "../utils/index.js"; +import getAuthtoken from "../utils/auth.utils.js"; +import { config } from "../config/index.js"; +import https from "../utils/https.utils.js"; +import fs from 'fs'; +import { HTTP_TEXTS, MIGRATION_DATA_CONFIG } from "../constants/index.js"; +import path from "path"; +import logger from "../utils/logger.js"; + +const { + TAXONOMIES_DIR_NAME, + TAXONOMIES_FILE_NAME +} = MIGRATION_DATA_CONFIG; + +const getDescendantsTerm = async ( {authtoken,taxonomyUid, termUid, region, stackId}: + {authtoken: string,taxonomyUid : string, termUid: string, region : string, stackId : string}) => { + const srcFun = "getDescendantsTerm"; + + try { + const [err, res] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + region as keyof typeof config.CS_API + ]!}/taxonomies/${taxonomyUid}/terms/${termUid}/descendants?include_children_count=true&include_count=true&include_order=true`, + headers: { + api_key : stackId, + authtoken, + }, + })); + if (err) { + logger.error( + getLogMessage(srcFun, HTTP_TEXTS.CS_ERROR, {}, err?.response?.data) + ); + + return { + data: err?.response?.data, + status: err?.response?.status, + }; + } + const terms = res?.data?.terms || []; + const allTerms: { uid: string; name: string; parent_uid: string }[] = []; + for (const term of terms) { + // Push current term + allTerms.push({ + uid: term?.uid, + name: term?.name, + parent_uid: term?.parent_uid, + }); + + // Recursively fetch deeper descendants + if (term?.children_count > 0) { + const nestedTerms = await getDescendantsTerm({ + authtoken, + taxonomyUid, + termUid: term?.uid, + region, + stackId, + }); + + if (Array.isArray(nestedTerms)) { + allTerms.push(...nestedTerms); + } + } + } + return allTerms; + } catch (error) { + logger.error("🚀 ~ getDescendantsTerm ~ error:", error); + throw error; + + } +} + +const createTerms = async( + {authtoken,taxonomyUid, region, stackId}: + {authtoken: string,taxonomyUid : string, region : string, stackId : string}) => { + const srcFun = "createTerms"; + try { + const [err, res] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + region as keyof typeof config.CS_API + ]!}/taxonomies/${taxonomyUid}/terms?include_terms_count=true&include_count=true&include_children_count=true&include_referenced_entries_count=true`, + headers: { + api_key : stackId, + authtoken, + }, + })); + const termsData = res?.data?.terms; + const allTerms: any[] = []; + for (const term of termsData || []) { + if (term?.uid) { + allTerms.push({ + uid: term?.uid, + name: term?.name, + parent_uid: term?.parent_uid, + }); + + if (term?.children_count > 0) { + const nestedTerms = await getDescendantsTerm({ + authtoken, + taxonomyUid, + termUid: term?.uid, + region, + stackId, + }); + + if (Array.isArray(nestedTerms)) { + allTerms.push(...nestedTerms); + } + } + } + } + + + + + if (err) { + logger.error( + getLogMessage(srcFun, HTTP_TEXTS.CS_ERROR, {}, err?.response?.data) + ); + + return { + data: err?.response?.data, + status: err?.response?.status, + }; + } + return allTerms; + + } catch (error) { + logger.error("🚀 ~ createTaxonomy ~ error:", error); + throw error; + + } + + + +} +const createTaxonomy = async ({stackId,region,userId,current_test_stack_id} : + {orgId: string, stackId: string, projectId:string,region: string,userId: string,current_test_stack_id:string}) => { + const srcFun = "createTaxonomy"; + const taxonomiesPath = path.join(MIGRATION_DATA_CONFIG.DATA, current_test_stack_id, TAXONOMIES_DIR_NAME); + await fs.promises.mkdir(taxonomiesPath, { recursive: true }); + try { + const authtoken = await getAuthtoken( + region, + userId + ); + const [err, res] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + region as keyof typeof config.CS_API + ]!}/taxonomies?include_terms_count=true&include_count=true`, + headers: { + api_key : stackId, + authtoken, + }, + }) + ); + if (err) { + logger.error( + getLogMessage(srcFun, HTTP_TEXTS.CS_ERROR, {}, err?.response?.data) + ); + + return { + data: err?.response?.data, + status: err?.response?.status, + }; + } + + const taxonomiesDataObject: Record = {}; + if (res?.data?.taxonomies) { + for (const taxonomy of res.data.taxonomies) { + if (taxonomy?.uid) { + taxonomiesDataObject[taxonomy.uid] = { + uid: taxonomy?.uid, + name: taxonomy?.name, + description: taxonomy?.description, + }; + const singleTaxonomy: any = {}; + singleTaxonomy['taxonomy'] = { + uid: taxonomy?.uid, + name: taxonomy?.name, + description: taxonomy?.description, + }; + singleTaxonomy['terms'] = await createTerms({ authtoken, taxonomyUid: taxonomy?.uid, region, stackId }); + await fs.promises.writeFile(path.join(taxonomiesPath, `${taxonomy?.uid}.json`), JSON.stringify(singleTaxonomy, null, 2)); + } + } + } + + const filePath = path.join(taxonomiesPath, TAXONOMIES_FILE_NAME); + await fs.promises.writeFile(filePath, JSON.stringify(taxonomiesDataObject, null, 2)); + + + + } catch (error) { + logger.error("🚀 ~ createTaxonomy ~ error:", error); + throw error; + } + +} + + +export const taxonomyService = { + createTaxonomy +} + diff --git a/api/src/utils/content-type-creator.utils.ts b/api/src/utils/content-type-creator.utils.ts index 49e25a950..215d2829d 100644 --- a/api/src/utils/content-type-creator.utils.ts +++ b/api/src/utils/content-type-creator.utils.ts @@ -74,9 +74,19 @@ const arrangGroups = ({ schema, newStack }: any) => { schema?.forEach((item: any) => { if (item?.contentstackFieldType === 'group') { const groupSchema: any = { ...item, schema: [] } + if (item?.contentstackFieldUid?.includes('.')) { + const parts = item?.contentstackFieldUid?.split('.'); + groupSchema.contentstackFieldUid = parts?.[parts?.length - 1]; + } schema?.forEach((et: any) => { if (et?.contentstackFieldUid?.includes(`${item?.contentstackFieldUid}.`) || (newStack === false && et?.uid?.includes(`${item?.uid}.`))) { + const target = groupSchema?.contentstackFieldUid; + const index = et?.contentstackFieldUid?.indexOf(target); + + if (index > 0) { + et.contentstackFieldUid = et?.contentstackFieldUid?.substring?.(index); + } groupSchema?.schema?.push(et); } }) @@ -665,7 +675,19 @@ const mergeArrays = async (a: any[], b: any[]) => { } } return a; -} +}; + +// Recursive search to find a group by uid anywhere in the schema +const findGroupByUid = (schema: any[], uid: string): any | null => { + for (const field of schema) { + if (field?.data_type === 'group') { + if (field?.uid === uid) return field; + const nested = findGroupByUid(field?.schema ?? [], uid); + if (nested) return nested; + } + } + return null; +}; const mergeTwoCts = async (ct: any, mergeCts: any) => { const ctData: any = { @@ -673,27 +695,31 @@ const mergeTwoCts = async (ct: any, mergeCts: any) => { title: mergeCts?.title, uid: mergeCts?.uid, options: { - "singleton": false, + singleton: false, } - } - for await (const field of ctData?.schema ?? []) { - if (field?.data_type === 'group') { - const currentGroup = mergeCts?.schema?.find((grp: any) => grp?.uid === field?.uid && - grp?.data_type === 'group'); - const group = []; - for await (const fieldGp of currentGroup?.schema ?? []) { - const fieldNst = field?.schema?.find((fld: any) => fld?.uid === fieldGp?.uid && - fld?.data_type === fieldGp?.data_type); - if (fieldNst === undefined) { - group?.push(fieldGp); + }; + + const mergeGroupSchema = async (targetSchema: any[], sourceSchema: any[]) => { + for await (const targetField of targetSchema) { + if (targetField?.data_type === 'group') { + const matchingSourceGroup = findGroupByUid(sourceSchema, targetField?.uid); + if (matchingSourceGroup) { + if (!Array.isArray(targetField?.schema)) targetField.schema = []; + if (!Array.isArray(matchingSourceGroup?.schema)) matchingSourceGroup.schema = []; + + await mergeGroupSchema(targetField?.schema, matchingSourceGroup?.schema); + targetField.schema = await mergeArrays(targetField?.schema, matchingSourceGroup?.schema); } } - field.schema = [...field?.schema ?? [], ...group]; } - } - ctData.schema = await mergeArrays(ctData?.schema, mergeCts?.schema) ?? []; + }; + + await mergeGroupSchema(ctData?.schema ?? [], mergeCts?.schema ?? []); + ctData.schema = await mergeArrays(ctData?.schema, mergeCts?.schema ?? []); + return ctData; -} +}; + export const contenTypeMaker = async ({ contentType, destinationStackId, projectId, newStack, keyMapper, region, user_id }: any) => { const marketPlacePath = path.join(process.cwd(), MIGRATION_DATA_CONFIG.DATA, destinationStackId); diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index a21da2cce..0d0f40f50 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -485,6 +485,71 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: }; }, []); + const checkAndUpdateField = ( + item: any, + value: any, + key: string, + parentLabel = '' +) => { + // Construct label with group hierarchy + const currentLabel = parentLabel ? `${parentLabel} > ${item?.display_name}` : item?.display_name; + + // Check for match + if (value?.value?.uid === item?.uid && value?.label === currentLabel) { + if (!updatedSelectedOptions?.includes?.(currentLabel)) { + updatedSelectedOptions?.push?.(currentLabel); + } + + setSelectedOptions(updatedSelectedOptions); + setExistingField((prevOptions: ExistingFieldType) => ({ + ...prevOptions, + [key]: { label: currentLabel, value: item }, + })); + + return true; + } + + // Check children recursively + if (item?.data_type === 'group' && Array?.isArray(item?.schema)) { + for (const child of item.schema) { + const found = checkAndUpdateField(child, value, key, currentLabel); + if (found) return true; + } + + // If no match and it was part of the label, remove it + if ( + !item?.schema?.some((schema:any) => schema?.uid === value?.value?.uid) && + value?.data_type !== 'group' && + value?.label?.includes(item?.display_name) + ) { + setIsUpdated(true); + updatedRows = updatedRows?.map((row: FieldMapType) => { + + if (row?.uid === key && row?.backupFieldType === value?.value?.data_type) { + return { + ...row, + contentstackField: row?.otherCmsField, + contentstackFieldUid: row?.backupFieldUid, + contentstackFieldType: row?.backupFieldType, + + }; + } + return row; + }); + + setTableData(updatedRows); + setSelectedEntries(updatedRows) + setExistingField((prevOptions: ExistingFieldType) => { + const { [key]: _, ...rest } = prevOptions; + return { ...rest }; + }); + + } + } + + return false; +}; + // if exsting content type is changed in contentstack, reflect those changes for // maaped fields useEffect(() => { @@ -536,6 +601,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } }); } + checkAndUpdateField(item, value, key); } }); @@ -1255,7 +1321,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: //utility function to map the source cms field type to content type field type function checkConditions(fieldTypeToMatch: string | string[], value: ContentTypesSchema, data: FieldMapType) { - const fieldTypes = new Set(['number', 'isodate', 'file', 'reference', 'boolean', 'group', 'link', 'global_field', 'json', 'blocks']); + const fieldTypes = new Set(['number', 'isodate', 'file', 'reference', 'boolean', 'group', 'link', 'global_field', 'json', 'blocks','taxonomy']); switch (fieldTypeToMatch) { case 'text': return ( @@ -1539,9 +1605,9 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } // Add 'Content Type(s)' as an option if not already present - if (!option?.some(opt => opt.label === 'Content Type(s)')) { - option.unshift({ label: 'Content Type(s)', value: 'Content Type(s)' }); - } + // if (!option?.some(opt => opt.label === 'Content Type(s)')) { + // option.unshift({ label: 'Content Type(s)', value: 'Content Type(s)' }); + // } const OptionValue: FieldTypes = @@ -1584,7 +1650,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: