Skip to content
Merged
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
54 changes: 47 additions & 7 deletions api/src/services/aem.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,24 @@ const createEntry = async ({
const content: unknown = await fs.promises.readFile(filePath, 'utf-8');
if (typeof content === 'string') {
const parseData = JSON.parse(content);

// AEM can export a page TEMPLATE's own structure/schema definition (e.g.
// /conf/.../settings/wcm/templates/<template>/structure[.html]) as a separate file
// alongside real pages that use that template β€” it isn't content, just the
// template's own component-allow-list. Exclude it outright rather than letting it
// compete with a real page for the same derived id (see CMG-1112): this removes the
// ambiguity entirely instead of leaving the outcome dependent on directory-walk order.
const repoPath: string | undefined = parseData?.dataLayer?.[parseData?.id]?.['repo:path'];
if (repoPath && /\/settings\/wcm\/templates\/[^/]+\/structure(\.html)?$/.test(repoPath)) {
await customLogger(
projectId,
destinationStackId,
'warn',
getLogMessage(srcFunc, `Skipped entry from "${fileName}": AEM template structure/schema definition, not real content (repo:path "${repoPath}").`, {})
);
continue;
}

// Use the page model's stable "id" as the entry uid so uid-mapper keys
// stay consistent across delta iterations; random uuid only as fallback.
let modelId = typeof parseData?.id === 'string' && parseData.id.trim() !== ''
Expand All @@ -1381,25 +1399,47 @@ const createEntry = async ({
// pages like content-page) carry no stable page "id"; derive a stable uid
// from title + templateType (or just templateType when there's no title)
// so they track across iterations (must match extractEntries in
// upload-api's migration-aem).
// upload-api/migration-aem/libs/entries/index.ts).
if (!modelId && parseData?.templateType) {
modelId = parseData?.title
? uidCorrector(`${parseData.title}_${parseData.templateType}`)
: uidCorrector(parseData.templateType);
}
const uid = modelId && !usedEntryUids.has(modelId)
? modelId
: uuidv4?.()?.replace?.(/-/g, '');
usedEntryUids.add(uid);
// Locale must be part of the collision key, computed before the check: two
// locale variants of the SAME page legitimately share a modelId (that's how they
// end up localized onto one Contentstack entry), so keying on modelId alone would
// wrongly skip every locale variant after the first one instead of writing each to
// its own locale bucket.
const locale = getCurrentLocale(parseData);
const mappedLocale = locale ? getLocaleFromMapper(allLocales as Record<string, string>, locale) : Object?.keys?.(project?.master_locale ?? {})?.[0];
const collisionKey = modelId ? `${modelId}::${mappedLocale}` : '';
// A collisionKey (modelId + locale) already seen earlier in this same run means this
// file is a genuine duplicate export of the same page in the same locale β€” skip it
// instead of minting a fresh random uid. A random uid here would create a second,
// permanent duplicate entry that mints yet another untracked random uid (another
// duplicate) on every subsequent delta iteration, since it can never match anything
// recorded in entry_mapper (api/src/models/EntryMapper.ts) β€” the way extractEntries's
// own collision policy already works in upload-api/migration-aem/libs/entries/index.ts.
if (collisionKey && usedEntryUids.has(collisionKey)) {
await customLogger(
projectId,
destinationStackId,
'warn',
getLogMessage(srcFunc, `Skipped duplicate entry from "${fileName}": uid "${modelId}" (locale "${mappedLocale}") already used in this run.`, {})
);
continue;
Comment thread
shradha-nahar marked this conversation as resolved.
}
const uid = modelId || uuidv4?.()?.replace?.(/-/g, '');
Comment thread
shradha-nahar marked this conversation as resolved.
if (collisionKey) {
usedEntryUids.add(collisionKey);
}
const title = getTitle(parseData);
const isEFragment = isExperienceFragment(parseData);
const templateUid = isEFragment?.isXF ? parseData?.title : parseData?.templateName ?? parseData?.templateType;
let contentType = (contentTypes as ContentType[] | undefined)?.find?.((element) => element?.otherCmsUid === templateUid);
if (!contentType && parseData?.title) {
contentType = (contentTypes as ContentType[] | undefined)?.find?.((element) => element?.otherCmsUid === parseData?.title);
}
const locale = getCurrentLocale(parseData);
const mappedLocale = locale ? getLocaleFromMapper(allLocales as Record<string, string>, locale) : Object?.keys?.(project?.master_locale ?? {})?.[0];
const items = parseData?.[':items']?.root?.[':items'];
const data = containerCreator(contentType?.fieldMapping, items, title, pathToUidMap, assetDetailsMap);
data.uid = uid;
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/ContentMapper/assetMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const AssetMapper = ({
const tableHeight = useMeasuredTableHeight(tableWrapperRef, [tableData?.length], {
panelSelector: '.TablePanel',
footerSelector: '.mapper-footer',
toolbarSelector: '.asset-mapper-toolbar',
});

// Single server-paginated fetch (same pattern as entryMapper's fetchEntries). The
Expand Down
14 changes: 13 additions & 1 deletion ui/src/components/ContentMapper/useMeasuredTableHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export interface MeasuredTableHeightOptions {
panelSelector: string;
/** Selector for the Save footer, resolved within `wrapperRef`. */
footerSelector: string;
/**
* Selector for an extra chrome row above the table (e.g. the asset mapper's status-filter
* toolbar) that takes its own flex-flow height, resolved within `wrapperRef`. Omit when the
* mapper has no such row (e.g. the entry mapper, whose locale select is absolutely positioned
* and doesn't need reserving).
*/
toolbarSelector?: string;
}

// Fixed chrome fallbacks, used only until the real elements are mounted/measured.
Expand All @@ -42,7 +49,7 @@ const TOGGLE_SELECTOR = '.mapper-view-toggle';
export function useMeasuredTableHeight(
wrapperRef: RefObject<HTMLElement | null>,
deps: unknown[],
{ panelSelector, footerSelector }: MeasuredTableHeightOptions,
{ panelSelector, footerSelector, toolbarSelector }: MeasuredTableHeightOptions,
): number {
// Pre-measure guess: same model as measure() (box fallback βˆ’ reserve), clamped to the floor
// so the one frame react-window renders before the effect runs never gets a negative height.
Expand All @@ -61,13 +68,17 @@ export function useMeasuredTableHeight(
const toggle = box?.querySelector(TOGGLE_SELECTOR) as HTMLElement | null;
const panel = wrapper.querySelector(panelSelector) as HTMLElement | null;
const footer = wrapper.querySelector(footerSelector) as HTMLElement | null;
const toolbar = toolbarSelector
? (wrapper.querySelector(toolbarSelector) as HTMLElement | null)
: null;

if (import.meta.env.DEV) {
// A rename/markup change in venus would drop us to the magic constants and quietly
// regress the layout β€” warn loudly in dev so it's caught rather than shipped.
if (!box) console.warn(`useMeasuredTableHeight: "${BOX_SELECTOR}" not found β€” falling back.`);
if (!panel) console.warn(`useMeasuredTableHeight: "${panelSelector}" not found β€” using ${PANEL_FALLBACK}px fallback.`);
if (!footer) console.warn(`useMeasuredTableHeight: "${footerSelector}" not found β€” using ${FOOTER_FALLBACK}px fallback.`);
if (toolbarSelector && !toolbar) console.warn(`useMeasuredTableHeight: "${toolbarSelector}" not found β€” not reserving space for it.`);
}

// `||` not `??`: a momentarily 0-height box (measured before layout settles) should
Expand All @@ -77,6 +88,7 @@ export function useMeasuredTableHeight(
(toggle?.offsetHeight ?? 0) +
(panel?.offsetHeight ?? PANEL_FALLBACK) +
(footer?.offsetHeight ?? FOOTER_FALLBACK) +
(toolbar?.offsetHeight ?? 0) +
PAGINATION_AND_BUFFER;
// Clamp rather than skip: at extreme zoom `avail` can dip low, but keeping the previous
// (possibly large) value would re-expose the overflow this hook exists to prevent.
Expand Down
10 changes: 7 additions & 3 deletions ui/src/components/LegacyCms/legacyCms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,20 @@
background-color: $color-base-white-5;
flex-direction: column;
justify-content: center;
align-items: flex-start;
// Stretch (not flex-start) so the path row keeps the container's full width β€” flex-start
// let it shrink-to-fit for short/invalid paths, visibly narrowing the input (CMG-1113).
align-items: stretch;
Comment thread
shradha-nahar marked this conversation as resolved.
margin-left: 20px !important;
border: 1px solid $color-brand-fail-base;
border-radius: var(--TermCount, 5px);
min-height: 72px;
width: 560px;
margin-left: 20px !important;
// Match .validation-container's own padding (line 75) instead of a child left-margin β€”
// a child margin only pushes content in from the left, leaving it flush against the
// right border, so the row was still a different width from the neutral state (CMG-1113).
padding: 10px 15px;
}
.error-container > * {
margin-left: 10px;
margin-top: 5px;
}

Expand Down
Loading