Skip to content
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
[CMG-736] - Multiple clicks on buttons or links that return results s…
…hould be prevented.
  • Loading branch information
sayalijoshi27 committed Nov 4, 2025
commit a6d356eef3130e0400d892ef08f99de2ffea23c1
257 changes: 151 additions & 106 deletions ui/src/components/ContentMapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
const [isCsCTypeUpdated, setsCsCTypeUpdated] = useState<boolean>(false);
const [isLoadingSaveButton, setisLoadingSaveButton] = useState<boolean>(false);
const [activeFilter, setActiveFilter] = useState<string>('');
const [isAllCheck, setIsAllCheck] = useState<boolean>(false);
const [isAllCheck, setIsAllCheck] = useState<boolean>(false);
const [isResetFetch, setIsResetFetch] = useState<boolean>(false);


/** ALL HOOKS Here */
Expand Down Expand Up @@ -496,6 +497,20 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
};
}, []);

/**
* Debounces a function call by delaying its execution until after the specified delay has elapsed since the last invocation.
* @param fn - The function to debounce
* @param delay - The delay in milliseconds to wait before executing the function
* @returns A debounced version of the function
*/
const debounce = (fn: (...args: any[]) => any, delay: number | undefined) => {
let timeoutId: string | number | NodeJS.Timeout | undefined;
return (...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};

const checkAndUpdateField = (
item: any,
value: any,
Expand Down Expand Up @@ -1998,11 +2013,14 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
handleDropdownState
}));

const handleResetContentType = async () => {
const handleResetContentType = debounce(async () => {
// Prevent duplicate clicks
if (isResetFetch) return;

const orgId = selectedOrganisation?.value;
const projectID = projectId;
setIsDropDownChanged(false);

setIsResetFetch(true);
const updatedRows: FieldMapType[] = tableData?.map?.((row) => {
return {
...row,
Expand Down Expand Up @@ -2056,6 +2074,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
});

if (status === 200) {
setIsResetFetch(false);
const updatedContentMapping = { ...newMigrationData?.content_mapping?.content_type_mapping };
delete updatedContentMapping[selectedContentType?.contentstackUid];

Expand Down Expand Up @@ -2089,9 +2108,10 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
notificationContent: { text: data?.message },
notificationProps: {
position: 'bottom-center',
hideProgressBar: false
hideProgressBar: true
},
type: 'success'
type: 'success',
autoClose: 2
});

try {
Expand All @@ -2106,112 +2126,30 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
} catch (error) {
console.error(error);
return error;
} finally {
// Re-enable icon after API completes
setIsResetFetch(false);
}
}
};

const handleCTDeleted = async (isContentType: boolean, contentTypes: ContentTypeList[]) => {
const updatedContentTypeMapping = Object.fromEntries(
Object.entries(newMigrationData?.content_mapping?.content_type_mapping || {})?.filter(
([key]) => !selectedContentType?.contentstackUid?.includes?.(key)
)
);

const orgId = selectedOrganisation?.value;
const projectID = projectId;
setIsDropDownChanged(false);

const updatedRows: FieldMapType[] = tableData.map((row) => {
return { ...row, contentstackFieldType: row?.backupFieldType };
});
setTableData(updatedRows);
setSelectedEntries(updatedRows);

const dataCs = {
contentTypeData: {
status: selectedContentType?.status,
id: selectedContentType?.id,
projectId: projectId,
otherCmsTitle: otherCmsTitle,
otherCmsUid: selectedContentType?.otherCmsUid,
isUpdated: true,
updateAt: new Date(),
contentstackTitle: selectedContentType?.contentstackTitle,
contentstackUid: selectedContentType?.contentstackUid,
fieldMapping: updatedRows
}
};
let newstate = {};
setContentTypeMapped((prevState: ContentTypeMap) => {
const newState = { ...prevState };

delete newState[selectedContentType?.contentstackUid ?? ''];
newstate = newState;

return newstate;
});

if (orgId && selectedContentType) {
try {
const { data, status } = await resetToInitialMapping(
orgId,
projectID,
selectedContentType?.id ?? '',
dataCs
);

setExistingField({});
setContentTypeSchema([]);
setOtherContentType({
label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`,
value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`
});

if (status === 200) {
const resetCT = filteredContentTypes?.map?.(ct =>
ct?.id === selectedContentType?.id ? { ...ct, status: data?.data?.status } : ct
);
setFilteredContentTypes(resetCT);
setContentTypes(resetCT);

try {
await updateContentMapper(orgId, projectID, { ...newstate });
} catch (err) {
console.error(err);
return err;
}

}
} catch (error) {
console.error(error);
return error;
}
}

const newMigrationDataObj: INewMigration = {
...newMigrationData,
content_mapping: {
...newMigrationData?.content_mapping,
[isContentType ? 'existingCT' : 'existingGlobal']: contentTypes,
content_type_mapping: updatedContentTypeMapping

}

}
dispatch(updateNewMigrationData(newMigrationDataObj));

}
}, 1500);
/**
* Retrieves existing content types for a given project.
* @returns An array containing the retrieved content types or global fields based on condition if itContentType true and if existing content type or global field id is passed then returns an object containing title, uid and schema of that particular content type or global field.
*/
const handleFetchContentType = debounce(async () => {
// Prevent duplicate clicks
if (isResetFetch) return;

/**
* Retrieves existing content types for a given project.
* @returns An array containing the retrieved content types or global fields based on condition if itContentType true and if existing content type or global field id is passed then returns an object containing title, uid and schema of that particular content type or global field.
*/
const handleFetchContentType = async () => {
setIsResetFetch(true);

if (isContentType) {
try {


const { data, status } = await getExistingContentTypes(projectId, otherContentType?.id ?? '');
if (status == 201 && data?.contentTypes?.length > 0) {
(otherContentType?.id === data?.selectedContentType?.uid) && setsCsCTypeUpdated(false);
setIsResetFetch(false);

(otherContentType?.id && otherContentType?.label !== data?.selectedContentType?.title && data?.selectedContentType?.title)
&& setOtherContentType({
Expand All @@ -2233,7 +2171,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
notificationContent: { text: 'Content Types fetched successfully' },
notificationProps: {
position: 'bottom-center',
hideProgressBar: false
hideProgressBar: true
},
type: 'success'
});
Expand All @@ -2256,13 +2194,17 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
} catch (error) {
console.error(error);
return error;
} finally {
// Re-enable icon after API completes
setIsResetFetch(false);
}
} else {
try {
const { data, status } = await getExistingGlobalFields(projectId, otherContentType?.id ?? '');

if (status == 201 && data?.globalFields?.length > 0) {
(otherContentType?.id === data?.selectedGlobalField?.uid) && setsCsCTypeUpdated(false);
setIsResetFetch(false);

(otherContentType?.id && otherContentType?.label !== data?.selectedGlobalField?.title && data?.selectedGlobalField?.title)
&& setOtherContentType({
Expand Down Expand Up @@ -2311,6 +2253,9 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
} catch (error) {
console.error(error);
return error;
} finally {
// Re-enable icon after API completes
setIsResetFetch(false);
}
}

Expand Down Expand Up @@ -2340,6 +2285,106 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:

});
}
}, 1500);

/**
* Handles the deletion of a content type or global field.
* @param isContentType - Whether the content type is a content type or global field.
* @param contentTypes - The content types to delete.
*/
const handleCTDeleted = async (isContentType: boolean, contentTypes: ContentTypeList[]) => {
// Prevent duplicate clicks
if (isResetFetch) return;

const updatedContentTypeMapping = Object.fromEntries(
Object.entries(newMigrationData?.content_mapping?.content_type_mapping || {})?.filter(
([key]) => !selectedContentType?.contentstackUid?.includes?.(key)
)
);

const orgId = selectedOrganisation?.value;
const projectID = projectId;
setIsDropDownChanged(false);

const updatedRows: FieldMapType[] = tableData.map((row) => {
return { ...row, contentstackFieldType: row?.backupFieldType };
});
setTableData(updatedRows);
setSelectedEntries(updatedRows);

const dataCs = {
contentTypeData: {
status: selectedContentType?.status,
id: selectedContentType?.id,
projectId: projectId,
otherCmsTitle: otherCmsTitle,
otherCmsUid: selectedContentType?.otherCmsUid,
isUpdated: true,
updateAt: new Date(),
contentstackTitle: selectedContentType?.contentstackTitle,
contentstackUid: selectedContentType?.contentstackUid,
fieldMapping: updatedRows
}
};
let newstate = {};
setContentTypeMapped((prevState: ContentTypeMap) => {
const newState = { ...prevState };

delete newState[selectedContentType?.contentstackUid ?? ''];
newstate = newState;

return newstate;
});

if (orgId && selectedContentType) {
try {
const { data, status } = await resetToInitialMapping(
orgId,
projectID,
selectedContentType?.id ?? '',
dataCs
);

setExistingField({});
setContentTypeSchema([]);
setOtherContentType({
label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`,
value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`
});

if (status === 200) {
const resetCT = filteredContentTypes?.map?.(ct =>
ct?.id === selectedContentType?.id ? { ...ct, status: data?.data?.status } : ct
);
setFilteredContentTypes(resetCT);
setContentTypes(resetCT);

try {
await updateContentMapper(orgId, projectID, { ...newstate });
} catch (err) {
console.error(err);
return err;
}

}
} catch (error) {
console.error(error);
return error;
}
}

const newMigrationDataObj: INewMigration = {
...newMigrationData,
content_mapping: {
...newMigrationData?.content_mapping,
[isContentType ? 'existingCT' : 'existingGlobal']: contentTypes,
content_type_mapping: updatedContentTypeMapping

}

}
dispatch(updateNewMigrationData(newMigrationDataObj));

}

const columns = [
Expand Down Expand Up @@ -2621,7 +2666,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
<Tooltip content={'Fetch content types from destination stack'} position="top">
<Button className='icon-padding' buttonType="light" icon={onlyIcon ? "v2-FetchTemplate" : ''}
version="v2" onlyIcon={true} onlyIconHoverColor={'primary'}
size='small' onClick={handleFetchContentType}>
size='small' onClick={handleFetchContentType} disabled={isResetFetch}>
</Button>
</Tooltip>
</>
Expand All @@ -2630,7 +2675,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
<Tooltip content={'Reset to system mapping'} position="top">
<Button className='icon-padding' buttonType="light" icon={onlyIcon ? "v2-ResetReverse" : ''}
version="v2" onlyIcon={true} onlyIconHoverColor={'primary'}
size='small' onClick={handleResetContentType}></Button>
size='small' onClick={handleResetContentType} disabled={isResetFetch}></Button>
</Tooltip>
</div>
),
Expand Down