Skip to content

Commit a6d356e

Browse files
committed
[CMG-736] - Multiple clicks on buttons or links that return results should be prevented.
1 parent 087d330 commit a6d356e

File tree

1 file changed

+151
-106
lines changed

1 file changed

+151
-106
lines changed

ui/src/components/ContentMapper/index.tsx

Lines changed: 151 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
301301
const [isCsCTypeUpdated, setsCsCTypeUpdated] = useState<boolean>(false);
302302
const [isLoadingSaveButton, setisLoadingSaveButton] = useState<boolean>(false);
303303
const [activeFilter, setActiveFilter] = useState<string>('');
304-
const [isAllCheck, setIsAllCheck] = useState<boolean>(false);
304+
const [isAllCheck, setIsAllCheck] = useState<boolean>(false);
305+
const [isResetFetch, setIsResetFetch] = useState<boolean>(false);
305306

306307

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

500+
/**
501+
* Debounces a function call by delaying its execution until after the specified delay has elapsed since the last invocation.
502+
* @param fn - The function to debounce
503+
* @param delay - The delay in milliseconds to wait before executing the function
504+
* @returns A debounced version of the function
505+
*/
506+
const debounce = (fn: (...args: any[]) => any, delay: number | undefined) => {
507+
let timeoutId: string | number | NodeJS.Timeout | undefined;
508+
return (...args: any[]) => {
509+
clearTimeout(timeoutId);
510+
timeoutId = setTimeout(() => fn(...args), delay);
511+
};
512+
};
513+
499514
const checkAndUpdateField = (
500515
item: any,
501516
value: any,
@@ -1998,11 +2013,14 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
19982013
handleDropdownState
19992014
}));
20002015

2001-
const handleResetContentType = async () => {
2016+
const handleResetContentType = debounce(async () => {
2017+
// Prevent duplicate clicks
2018+
if (isResetFetch) return;
2019+
20022020
const orgId = selectedOrganisation?.value;
20032021
const projectID = projectId;
20042022
setIsDropDownChanged(false);
2005-
2023+
setIsResetFetch(true);
20062024
const updatedRows: FieldMapType[] = tableData?.map?.((row) => {
20072025
return {
20082026
...row,
@@ -2056,6 +2074,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
20562074
});
20572075

20582076
if (status === 200) {
2077+
setIsResetFetch(false);
20592078
const updatedContentMapping = { ...newMigrationData?.content_mapping?.content_type_mapping };
20602079
delete updatedContentMapping[selectedContentType?.contentstackUid];
20612080

@@ -2089,9 +2108,10 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
20892108
notificationContent: { text: data?.message },
20902109
notificationProps: {
20912110
position: 'bottom-center',
2092-
hideProgressBar: false
2111+
hideProgressBar: true
20932112
},
2094-
type: 'success'
2113+
type: 'success',
2114+
autoClose: 2
20952115
});
20962116

20972117
try {
@@ -2106,112 +2126,30 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
21062126
} catch (error) {
21072127
console.error(error);
21082128
return error;
2129+
} finally {
2130+
// Re-enable icon after API completes
2131+
setIsResetFetch(false);
21092132
}
21102133
}
2111-
};
2112-
2113-
const handleCTDeleted = async (isContentType: boolean, contentTypes: ContentTypeList[]) => {
2114-
const updatedContentTypeMapping = Object.fromEntries(
2115-
Object.entries(newMigrationData?.content_mapping?.content_type_mapping || {})?.filter(
2116-
([key]) => !selectedContentType?.contentstackUid?.includes?.(key)
2117-
)
2118-
);
2119-
2120-
const orgId = selectedOrganisation?.value;
2121-
const projectID = projectId;
2122-
setIsDropDownChanged(false);
2123-
2124-
const updatedRows: FieldMapType[] = tableData.map((row) => {
2125-
return { ...row, contentstackFieldType: row?.backupFieldType };
2126-
});
2127-
setTableData(updatedRows);
2128-
setSelectedEntries(updatedRows);
2129-
2130-
const dataCs = {
2131-
contentTypeData: {
2132-
status: selectedContentType?.status,
2133-
id: selectedContentType?.id,
2134-
projectId: projectId,
2135-
otherCmsTitle: otherCmsTitle,
2136-
otherCmsUid: selectedContentType?.otherCmsUid,
2137-
isUpdated: true,
2138-
updateAt: new Date(),
2139-
contentstackTitle: selectedContentType?.contentstackTitle,
2140-
contentstackUid: selectedContentType?.contentstackUid,
2141-
fieldMapping: updatedRows
2142-
}
2143-
};
2144-
let newstate = {};
2145-
setContentTypeMapped((prevState: ContentTypeMap) => {
2146-
const newState = { ...prevState };
2147-
2148-
delete newState[selectedContentType?.contentstackUid ?? ''];
2149-
newstate = newState;
2150-
2151-
return newstate;
2152-
});
2153-
2154-
if (orgId && selectedContentType) {
2155-
try {
2156-
const { data, status } = await resetToInitialMapping(
2157-
orgId,
2158-
projectID,
2159-
selectedContentType?.id ?? '',
2160-
dataCs
2161-
);
2162-
2163-
setExistingField({});
2164-
setContentTypeSchema([]);
2165-
setOtherContentType({
2166-
label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`,
2167-
value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`
2168-
});
2169-
2170-
if (status === 200) {
2171-
const resetCT = filteredContentTypes?.map?.(ct =>
2172-
ct?.id === selectedContentType?.id ? { ...ct, status: data?.data?.status } : ct
2173-
);
2174-
setFilteredContentTypes(resetCT);
2175-
setContentTypes(resetCT);
2176-
2177-
try {
2178-
await updateContentMapper(orgId, projectID, { ...newstate });
2179-
} catch (err) {
2180-
console.error(err);
2181-
return err;
2182-
}
2183-
2184-
}
2185-
} catch (error) {
2186-
console.error(error);
2187-
return error;
2188-
}
2189-
}
2190-
2191-
const newMigrationDataObj: INewMigration = {
2192-
...newMigrationData,
2193-
content_mapping: {
2194-
...newMigrationData?.content_mapping,
2195-
[isContentType ? 'existingCT' : 'existingGlobal']: contentTypes,
2196-
content_type_mapping: updatedContentTypeMapping
2197-
2198-
}
2199-
2200-
}
2201-
dispatch(updateNewMigrationData(newMigrationDataObj));
2202-
2203-
}
2134+
}, 1500);
2135+
/**
2136+
* Retrieves existing content types for a given project.
2137+
* @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.
2138+
*/
2139+
const handleFetchContentType = debounce(async () => {
2140+
// Prevent duplicate clicks
2141+
if (isResetFetch) return;
22042142

2205-
/**
2206-
* Retrieves existing content types for a given project.
2207-
* @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.
2208-
*/
2209-
const handleFetchContentType = async () => {
2143+
setIsResetFetch(true);
2144+
22102145
if (isContentType) {
22112146
try {
2147+
2148+
22122149
const { data, status } = await getExistingContentTypes(projectId, otherContentType?.id ?? '');
22132150
if (status == 201 && data?.contentTypes?.length > 0) {
22142151
(otherContentType?.id === data?.selectedContentType?.uid) && setsCsCTypeUpdated(false);
2152+
setIsResetFetch(false);
22152153

22162154
(otherContentType?.id && otherContentType?.label !== data?.selectedContentType?.title && data?.selectedContentType?.title)
22172155
&& setOtherContentType({
@@ -2233,7 +2171,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
22332171
notificationContent: { text: 'Content Types fetched successfully' },
22342172
notificationProps: {
22352173
position: 'bottom-center',
2236-
hideProgressBar: false
2174+
hideProgressBar: true
22372175
},
22382176
type: 'success'
22392177
});
@@ -2256,13 +2194,17 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
22562194
} catch (error) {
22572195
console.error(error);
22582196
return error;
2197+
} finally {
2198+
// Re-enable icon after API completes
2199+
setIsResetFetch(false);
22592200
}
22602201
} else {
22612202
try {
22622203
const { data, status } = await getExistingGlobalFields(projectId, otherContentType?.id ?? '');
22632204

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

22672209
(otherContentType?.id && otherContentType?.label !== data?.selectedGlobalField?.title && data?.selectedGlobalField?.title)
22682210
&& setOtherContentType({
@@ -2311,6 +2253,9 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
23112253
} catch (error) {
23122254
console.error(error);
23132255
return error;
2256+
} finally {
2257+
// Re-enable icon after API completes
2258+
setIsResetFetch(false);
23142259
}
23152260
}
23162261

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

23412286
});
23422287
}
2288+
}, 1500);
2289+
2290+
/**
2291+
* Handles the deletion of a content type or global field.
2292+
* @param isContentType - Whether the content type is a content type or global field.
2293+
* @param contentTypes - The content types to delete.
2294+
*/
2295+
const handleCTDeleted = async (isContentType: boolean, contentTypes: ContentTypeList[]) => {
2296+
// Prevent duplicate clicks
2297+
if (isResetFetch) return;
2298+
2299+
const updatedContentTypeMapping = Object.fromEntries(
2300+
Object.entries(newMigrationData?.content_mapping?.content_type_mapping || {})?.filter(
2301+
([key]) => !selectedContentType?.contentstackUid?.includes?.(key)
2302+
)
2303+
);
2304+
2305+
const orgId = selectedOrganisation?.value;
2306+
const projectID = projectId;
2307+
setIsDropDownChanged(false);
2308+
2309+
const updatedRows: FieldMapType[] = tableData.map((row) => {
2310+
return { ...row, contentstackFieldType: row?.backupFieldType };
2311+
});
2312+
setTableData(updatedRows);
2313+
setSelectedEntries(updatedRows);
2314+
2315+
const dataCs = {
2316+
contentTypeData: {
2317+
status: selectedContentType?.status,
2318+
id: selectedContentType?.id,
2319+
projectId: projectId,
2320+
otherCmsTitle: otherCmsTitle,
2321+
otherCmsUid: selectedContentType?.otherCmsUid,
2322+
isUpdated: true,
2323+
updateAt: new Date(),
2324+
contentstackTitle: selectedContentType?.contentstackTitle,
2325+
contentstackUid: selectedContentType?.contentstackUid,
2326+
fieldMapping: updatedRows
2327+
}
2328+
};
2329+
let newstate = {};
2330+
setContentTypeMapped((prevState: ContentTypeMap) => {
2331+
const newState = { ...prevState };
2332+
2333+
delete newState[selectedContentType?.contentstackUid ?? ''];
2334+
newstate = newState;
2335+
2336+
return newstate;
2337+
});
2338+
2339+
if (orgId && selectedContentType) {
2340+
try {
2341+
const { data, status } = await resetToInitialMapping(
2342+
orgId,
2343+
projectID,
2344+
selectedContentType?.id ?? '',
2345+
dataCs
2346+
);
2347+
2348+
setExistingField({});
2349+
setContentTypeSchema([]);
2350+
setOtherContentType({
2351+
label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`,
2352+
value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Destination Stack`
2353+
});
2354+
2355+
if (status === 200) {
2356+
const resetCT = filteredContentTypes?.map?.(ct =>
2357+
ct?.id === selectedContentType?.id ? { ...ct, status: data?.data?.status } : ct
2358+
);
2359+
setFilteredContentTypes(resetCT);
2360+
setContentTypes(resetCT);
2361+
2362+
try {
2363+
await updateContentMapper(orgId, projectID, { ...newstate });
2364+
} catch (err) {
2365+
console.error(err);
2366+
return err;
2367+
}
2368+
2369+
}
2370+
} catch (error) {
2371+
console.error(error);
2372+
return error;
2373+
}
2374+
}
2375+
2376+
const newMigrationDataObj: INewMigration = {
2377+
...newMigrationData,
2378+
content_mapping: {
2379+
...newMigrationData?.content_mapping,
2380+
[isContentType ? 'existingCT' : 'existingGlobal']: contentTypes,
2381+
content_type_mapping: updatedContentTypeMapping
2382+
2383+
}
2384+
2385+
}
2386+
dispatch(updateNewMigrationData(newMigrationDataObj));
2387+
23432388
}
23442389

23452390
const columns = [
@@ -2621,7 +2666,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
26212666
<Tooltip content={'Fetch content types from destination stack'} position="top">
26222667
<Button className='icon-padding' buttonType="light" icon={onlyIcon ? "v2-FetchTemplate" : ''}
26232668
version="v2" onlyIcon={true} onlyIconHoverColor={'primary'}
2624-
size='small' onClick={handleFetchContentType}>
2669+
size='small' onClick={handleFetchContentType} disabled={isResetFetch}>
26252670
</Button>
26262671
</Tooltip>
26272672
</>
@@ -2630,7 +2675,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
26302675
<Tooltip content={'Reset to system mapping'} position="top">
26312676
<Button className='icon-padding' buttonType="light" icon={onlyIcon ? "v2-ResetReverse" : ''}
26322677
version="v2" onlyIcon={true} onlyIconHoverColor={'primary'}
2633-
size='small' onClick={handleResetContentType}></Button>
2678+
size='small' onClick={handleResetContentType} disabled={isResetFetch}></Button>
26342679
</Tooltip>
26352680
</div>
26362681
),

0 commit comments

Comments
 (0)