diff --git a/api/src/config/index.ts b/api/src/config/index.ts index c74d8cb45..5290be089 100644 --- a/api/src/config/index.ts +++ b/api/src/config/index.ts @@ -42,7 +42,7 @@ export type ConfigType = { * Configuration object for the application. */ export const config: ConfigType = { - APP_TOKEN_EXP: "1d", + APP_TOKEN_EXP: "2d", PORT: process.env.PORT!, APP_ENV: process.env.NODE_ENV!, APP_TOKEN_KEY: process.env.APP_TOKEN_KEY!, diff --git a/api/src/controllers/org.controller.ts b/api/src/controllers/org.controller.ts index 52577058a..b97c769db 100644 --- a/api/src/controllers/org.controller.ts +++ b/api/src/controllers/org.controller.ts @@ -62,11 +62,24 @@ const getStackLocale = async (req: Request, res: Response) => { res.status(resp.status).json(resp.data); }; +/* Retrieves the org details. +* +* @param req - The request object. +* @param res - The response object. +* @returns A Promise that resolves to the org details response. +*/ +const getOrgDetails = async (req: Request, res: Response) => { + const resp = await orgService.getOrgDetails(req); + res.status(resp.status).json(resp.data); +}; + + export const orgController = { getAllStacks, createStack, getLocales, getStackStatus, - getStackLocale + getStackLocale, + getOrgDetails, }; diff --git a/api/src/controllers/projects.contentMapper.controller.ts b/api/src/controllers/projects.contentMapper.controller.ts index 956e3638d..fd1000aec 100644 --- a/api/src/controllers/projects.contentMapper.controller.ts +++ b/api/src/controllers/projects.contentMapper.controller.ts @@ -48,6 +48,22 @@ const getExistingContentTypes = async ( const resp = await contentMapperService.getExistingContentTypes(req); res.status(201).json(resp); }; + +/** + * Retrieves the existing global fields. + * + * @param {Request} req - The request object. + * @param {Response} res - The response object. + * @returns {Promise} - A promise that resolves when the operation is complete. + */ +const getExistingGlobalFields = async ( + req: Request, + res: Response +): Promise => { + const resp = await contentMapperService.getExistingGlobalFields(req); + res.status(201).json(resp); +}; + /** * Updates the content type fields. * @@ -131,5 +147,6 @@ export const contentMapperController = { // removeMapping, getSingleContentTypes, removeContentMapper, - updateContentMapper + updateContentMapper, + getExistingGlobalFields }; diff --git a/api/src/models/project-lowdb.ts b/api/src/models/project-lowdb.ts index 99075ccd3..4de76bac0 100644 --- a/api/src/models/project-lowdb.ts +++ b/api/src/models/project-lowdb.ts @@ -70,7 +70,7 @@ interface Project { isNewStack: boolean; newStackId: string; stackDetails: []; - mapperKeys: []; + mapperKeys: {}; extract_path: string; } diff --git a/api/src/routes/contentMapper.routes.ts b/api/src/routes/contentMapper.routes.ts index a50282d66..6b8fed79f 100644 --- a/api/src/routes/contentMapper.routes.ts +++ b/api/src/routes/contentMapper.routes.ts @@ -40,6 +40,15 @@ router.get( asyncRouter(contentMapperController.getExistingContentTypes) ); +/** + * Get Existing GlobalFields List + * @route GET /:projectId + */ +router.get( + "/globalFields/:projectId", + asyncRouter(contentMapperController.getExistingGlobalFields) +); + /** * Update FieldMapping or contentType * @route PUT /contentTypes/:orgId/:projectId/:contentTypeId diff --git a/api/src/routes/org.routes.ts b/api/src/routes/org.routes.ts index 8450be110..2b0eb222f 100644 --- a/api/src/routes/org.routes.ts +++ b/api/src/routes/org.routes.ts @@ -51,4 +51,13 @@ router.post( router.get("/get_stack_locales", asyncRouter(orgController.getStackLocale)); +/** + * GET all contentstack org details route. + * @param req - Express request object. + * @param res - Express response object. + */ +router.get("/get_org_details", asyncRouter(orgController.getOrgDetails)); + + + export default router; diff --git a/api/src/services/contentMapper.service.ts b/api/src/services/contentMapper.service.ts index e138ccc93..2146509c8 100644 --- a/api/src/services/contentMapper.service.ts +++ b/api/src/services/contentMapper.service.ts @@ -268,7 +268,6 @@ const getExistingContentTypes = async (req: Request) => { data: err.response.data, status: err.response.status, }; - const contentTypes = res.data.content_types.map((singleCT: any) => { return { title: singleCT.title, @@ -280,6 +279,57 @@ const getExistingContentTypes = async (req: Request) => { //Add logic to get Project from DB return { contentTypes }; }; + +/** + * Retrieves existing global fields for a given project. + * @param req - The request object containing the project ID and token payload. + * @returns An object containing the retrieved content types. + */ +const getExistingGlobalFields = async (req: Request) => { + const projectId = req?.params?.projectId; + + const { token_payload } = req.body; + + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + await ProjectModelLowdb.read(); + const project = ProjectModelLowdb.chain + .get("projects") + .find({ id: projectId }) + .value(); + const stackId = project?.destination_stack_id; + const [err, res] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + token_payload?.region as keyof typeof config.CS_API + ]!}/global_fields`, + headers: { + api_key: stackId, + authtoken: authtoken, + }, + }) + ); + + if (err) + return { + data: err.response.data, + status: err.response.status, + }; + const globalFields = res.data.global_fields.map((global: any) => { + return { + title: global.title, + uid: global.uid, + schema: global.schema, + }; + }); + + //Add logic to get Project from DB + return { globalFields }; +}; + /** * Updates the content type based on the provided request. * @param req - The request object containing the necessary parameters and data. @@ -923,8 +973,6 @@ const removeContentMapper = async (req: Request) => { * @throws ExceptionFunction if an error occurs during the update. */ const updateContentMapper = async (req: Request) => { - console.info("updateContentMapper", req.params, req.body); - const { orgId, projectId } = req.params; const { token_payload, content_mapper } = req.body; const srcFunc = "updateContentMapper"; @@ -944,7 +992,6 @@ const updateContentMapper = async (req: Request) => { try { ProjectModelLowdb.update((data: any) => { - // console.info("data ===============", data, content_mapper) data.projects[projectIndex].mapperKeys = content_mapper; data.projects[projectIndex].updated_at = new Date().toISOString(); }); @@ -989,5 +1036,6 @@ export const contentMapperService = { removeContentMapper, removeMapping, getSingleContentTypes, - updateContentMapper + updateContentMapper, + getExistingGlobalFields }; diff --git a/api/src/services/org.service.ts b/api/src/services/org.service.ts index 4a4eac760..eb7eb390b 100644 --- a/api/src/services/org.service.ts +++ b/api/src/services/org.service.ts @@ -368,10 +368,68 @@ const getStackLocale = async (req: Request) => { } }; +/** + * Retrieves the plan details of a org. + * @param req - The request object containing the orgId, token_payload. + * @returns An object containing the org details. + * @throws ExceptionFunction if an error occurs while getting the org details. + */ +const getOrgDetails = async (req: Request) => { + const { orgId } = req.params; + const { token_payload } = req.body; + const srcFunc = "getOrgDetails"; + + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + + try { + const [stackErr, stackRes] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + token_payload?.region as keyof typeof config.CS_API + ]!}/organizations/${orgId}?include_plan=true`, + headers: { + authtoken, + }, + }) + ); + + if (stackErr) + return { + data: { + message: HTTP_TEXTS.DESTINATION_STACK_ERROR, + }, + status: stackErr.response.status, + }; + + return { + status: HTTP_CODES.OK, + data: stackRes.data, + }; + } catch (error: any) { + logger.error( + getLogMessage( + srcFunc, + `Error occurred while getting locales a stack.`, + token_payload, + error + ) + ); + throw new ExceptionFunction( + error?.message || HTTP_TEXTS.INTERNAL_ERROR, + error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR + ); + } +}; + export const orgService = { getAllStacks, getLocales, createStack, getStackStatus, getStackLocale, + getOrgDetails, }; diff --git a/api/src/services/projects.service.ts b/api/src/services/projects.service.ts index 58c91176e..30e832765 100644 --- a/api/src/services/projects.service.ts +++ b/api/src/services/projects.service.ts @@ -125,7 +125,7 @@ const createProject = async (req: Request) => { created_at: '', isNewStack: false }, - mapperKeys: [] + mapperKeys: {} }; try { diff --git a/ui/package-lock.json b/ui/package-lock.json index fdd23193f..94eb9b77f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -20,7 +20,9 @@ "axios": "^1.5.1", "bootstrap": "5.1.3", "chokidar": "^3.6.0", + "final-form": "^4.20.10", "html-react-parser": "^4.2.9", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.9", @@ -10883,7 +10885,6 @@ "version": "4.20.10", "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", - "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -16449,6 +16450,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/ui/package.json b/ui/package.json index 51f70482f..55c788507 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,9 @@ "axios": "^1.5.1", "bootstrap": "5.1.3", "chokidar": "^3.6.0", + "final-form": "^4.20.10", "html-react-parser": "^4.2.9", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.9", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index db1aa1166..ff0518493 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,3 +1,4 @@ +// Libraries import { Suspense, useEffect } from 'react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; @@ -9,11 +10,12 @@ import { persistor, store } from './store'; import AppRouter from './components/Common/router'; import ErrorBoundary from './components/ErrorBoundary'; import AppLayout from './components/layout/AppLayout'; +import { useNetworkCheck } from './components/NetworkProvider'; + // Styles import '@contentstack/venus-components/build/main.css'; import './scss/App.scss'; -import { useNetworkCheck } from './components/NetworkProvider'; function App() { const isOnline = useNetworkCheck(); @@ -25,9 +27,6 @@ function App() { if (!isOnline) { // Hide the modal by setting display to none selectModal.style.display = 'none'; - } else { - // Show the modal by setting display to block - selectModal.style.display = 'block'; } } }, [isOnline]); diff --git a/ui/src/cmsData/projects.json b/ui/src/cmsData/projects.json index 29ecbefd6..7bfa72e99 100644 --- a/ui/src/cmsData/projects.json +++ b/ui/src/cmsData/projects.json @@ -75,7 +75,7 @@ "type": "doc", "_version": 12 }, - "heading": "Welcome to Content Migrations", + "heading": "Welcome to Content Migration!", "help_text": { "type": "doc", "attrs": {}, diff --git a/ui/src/components/AdvancePropertise/index.scss b/ui/src/components/AdvancePropertise/index.scss index bb0876613..403f278f4 100644 --- a/ui/src/components/AdvancePropertise/index.scss +++ b/ui/src/components/AdvancePropertise/index.scss @@ -189,4 +189,4 @@ font-size: 14px; color: $color-stepper-title; -} \ No newline at end of file +} diff --git a/ui/src/components/ContentMapper/contentMapper.interface.ts b/ui/src/components/ContentMapper/contentMapper.interface.ts index 18a399abb..41271898e 100644 --- a/ui/src/components/ContentMapper/contentMapper.interface.ts +++ b/ui/src/components/ContentMapper/contentMapper.interface.ts @@ -93,6 +93,7 @@ export interface FieldMetadata { allow_json_rte?: boolean; } export interface ContentTypesSchema { + display_type: string; data_type?: 'text' | 'number' | 'isodate' | 'json' | 'file' | 'reference' | 'group' | 'boolean' | 'link'; display_name: string; enum?: any; diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index d1efa7464..099312958 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -21,6 +21,7 @@ import { getContentTypes, getFieldMapping, getExistingContentTypes, + getExistingGlobalFields, updateContentType, resetToInitialMapping, fetchExistingContentType, @@ -226,6 +227,8 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re }= {} } = migrationData; + // const contentTypesList = awau + const [tableData, setTableData] = useState([]); const [loading, setLoading] = useState(false); const [isLoading, setIsLoading] = useState(newMigrationData?.isprojectMapped); @@ -237,17 +240,23 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re const [contentTypes, setContentTypes] = useState([]); const [otherCmsTitle, setOtherCmsTitle] = useState(contentTypes[0]?.otherCmsTitle); const [contentTypeUid, setContentTypeUid] = useState(''); - const [contentTypesList, setContentTypesList] = useState([]); + + const [existingContentTypes, setExistingContentTypes] = useState([]); + const [existingGlobalFields, setExistingGlobalFields] = useState([]) + const [isContentType, setIsContentType] = useState(contentTypes?.[0]?.type === "content_type"); + const [contentModels, setContentModels] = useState([]); + + const [selectedContentType, setSelectedContentType] = useState(); const [existingField, setExistingField] = useState({}); const [selectedOptions, setSelectedOptions] = useState([]); const [isDropDownChanged, setIsDropDownChanged] = useState(false); const [contentTypeMapped, setContentTypeMapped] = useState( - newMigrationData?.content_mapping?.content_type_mapping?.[0] || {} + newMigrationData?.content_mapping?.content_type_mapping || {} ); const [otherContentType, setOtherContentType] = useState({ - label: newMigrationData?.content_mapping?.content_type_mapping?.[0]?.[otherCmsTitle] || 'Select content type from existing stack', - value: newMigrationData?.content_mapping?.content_type_mapping?.[0]?.[otherCmsTitle] || 'Select content type from existing stack', + label: contentTypeMapped?.[otherCmsTitle] || `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack`, + value: contentTypeMapped?.[otherCmsTitle] || `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack`, }); const [otherCmsUid, setOtherCmsUid] = useState(contentTypes[0]?.otherCmsUid); const [isContentTypeMapped, setIsContentTypeMapped] = useState(false); @@ -284,6 +293,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re const deletedExstingField : ExistingFieldType= existingField; const isNewStack = newMigrationData?.stackDetails?.isNewStack; const [isFieldDeleted, setIsFieldDeleted] = useState(false); + const [isContentDeleted, setIsContentDeleted] = useState(false); /** ALL HOOKS Here */ const { projectId = '' } = useParams(); @@ -309,8 +319,9 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re console.error(err); }); - fetchExistingContentTypes(); fetchContentTypes(searchText || ''); + fetchExistingContentTypes(); + fetchExistingGlobalFields(); }, []); // Make title and url field non editable @@ -323,23 +334,49 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re },[tableData]); useEffect(() => { - if(otherCmsTitle) { - newMigrationData?.content_mapping?.content_type_mapping?.forEach((ctMap) => { - if (ctMap?.[otherCmsTitle] !== undefined) { + const mappedContentType = contentModels && contentModels?.find((item)=>item?.title === contentTypeMapped?.[otherCmsTitle]); + + if (contentTypeMapped && otherCmsTitle ) { + + if (mappedContentType?.uid) { + setOtherContentType({ + id: mappedContentType?.uid, + label: contentTypeMapped?.[otherCmsTitle], + value: contentTypeMapped?.[otherCmsTitle], + }); + setIsContentDeleted(false); + } else { + + setOtherContentType({ + label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack`, + value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack` + }); + + } + + } + + }, [contentTypeMapped, otherCmsTitle, contentModels]); + + + + useEffect(()=>{ + if(isContentDeleted){ + setContentTypeMapped((prevState: ContentTypeMap) => { + const { [otherCmsTitle]: removed, ...newState } = prevState; - setOtherContentType({ - label: ctMap?.[otherCmsTitle] ?? 'Select content type from existing stack', - value: ctMap?.[otherCmsTitle] ?? 'Select content type from existing stack' - }) - } - }) + return newState; + }); + + setIsFieldDeleted(false); } - }, [otherCmsTitle]); + + + },[isContentDeleted, contentModels, otherCmsTitle]) + useEffect(() => { - const checkKey = newMigrationData?.content_mapping?.content_type_mapping?.find(ctMap => ctMap[otherCmsTitle] === otherContentType?.label); - - if (checkKey?.[otherCmsTitle] !== undefined) { + if (contentTypeMapped[otherCmsTitle] === otherContentType?.label) { tableData?.forEach((row) => { contentTypeSchema?.forEach((schema) => { @@ -361,13 +398,6 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re } } - // else if(existingField[row?.uid]){ - // updatedExstingField[row?.uid] = { - // label: `${schema?.display_name} > ${childSchema?.display_name}`, - // value: childSchema - // } - // } - } }) } @@ -406,6 +436,15 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re setRowIds(selectedId); }, [tableData]); + // To fetch existing content types or global fields as per the type + useEffect(() => { + if(isContentType) { + setContentModels(existingContentTypes); + } else { + setContentModels(existingGlobalFields); + } + }, [existingContentTypes, existingGlobalFields, isContentType]) + // To close the filter panel on outside click useEffect(() => { document.addEventListener('click', handleClickOutside, true); @@ -467,6 +506,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re setContentTypeUid(data?.contentTypes?.[0]?.id); fetchFields(data?.contentTypes?.[0]?.id, searchText || ''); setOtherCmsUid(data?.contentTypes?.[0]?.otherCmsUid); + setIsContentType(data?.contentTypes?.[0]?.type === "content_type"); }; // Method to search content types @@ -517,7 +557,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re // Fetch table data const fetchData = async ({ searchText }: TableTypes) => { setSearchText(searchText) - fetchFields(contentTypeUid, searchText); + contentTypeUid && fetchFields(contentTypeUid, searchText); }; // Method for Load more table data @@ -581,25 +621,45 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re const otherTitle = contentTypes?.[i]?.otherCmsTitle; setOtherCmsTitle(otherTitle); - setOtherContentType({ - label: newMigrationData?.content_mapping?.content_type_mapping?.[i]?.[otherTitle] || 'Select content type from existing stack', - value: newMigrationData?.content_mapping?.content_type_mapping?.[i]?.[otherTitle] || 'Select content type from existing stack' - }); + // setOtherContentType({ + // label: contentTypeMapped?.[otherTitle] || 'Select content type from existing stack', + // value: contentTypeMapped?.[otherTitle] || 'Select content type from existing stack' + // }); setContentTypeUid(contentTypes?.[i]?.id ?? ''); fetchFields(contentTypes?.[i]?.id ?? '', searchText || ''); setOtherCmsUid(contentTypes?.[i]?.otherCmsUid); setSelectedContentType(contentTypes?.[i]); + setIsContentType(contentTypes?.[i]?.type === "content_type"); } // Function to get exisiting content types list const fetchExistingContentTypes = async () => { const { data, status } = await getExistingContentTypes(projectId); if (status === 201) { - setContentTypesList(data?.contentTypes); + setExistingContentTypes(data?.contentTypes); + const mappedContentType = data?.contentTypes && data?.contentTypes?.find((item:ContentTypeList)=>item?.title === contentTypeMapped?.[otherCmsTitle]); + + if (mappedContentType?.uid) { + setOtherContentType({ + id: mappedContentType?.uid, + label: contentTypeMapped?.[otherCmsTitle], + value: contentTypeMapped?.[otherCmsTitle], + }); + setIsContentDeleted(false); + } } }; + // Function to get exisiting global fields list + const fetchExistingGlobalFields = async () => { + const { data, status } = await getExistingGlobalFields(projectId); + + if (status === 201) { + setExistingGlobalFields(data?.globalFields); + } + } + const updateFieldSettings = (rowId: string, updatedSettings: Advanced, checkBoxChanged: boolean) => { setIsDropDownChanged(checkBoxChanged); @@ -778,6 +838,16 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re }); setTableData(updatedRows); setSelectedEntries(updatedRows); + + const dropdownChangeState: INewMigration = { + ...newMigrationData, + content_mapping: { + ...newMigrationData?.content_mapping, + isDropDownChanged: true, + otherCmsTitle: otherCmsTitle + } + } + dispatch(updateNewMigrationData((dropdownChangeState))); }; const handleDropDownChange = (value: FieldTypes) => { @@ -894,26 +964,18 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re } setIsFieldDeleted(true); - // console.log(deletedExstingField); - - delete existingField[item?.uid] + const index = selectedOptions?.indexOf(existingField[item?.uid]?.value?.label); - - const index = selectedOptions?.indexOf(`${item.contentstackField}`); - //console.log(index); if(index > -1){ - selectedOptions.slice(index,1 ) + selectedOptions.splice(index,1 ); } - + delete existingField[item?.uid] } } - else{ - - + else { setIsFieldDeleted(false); } - setExistingField((prevOptions: ExistingFieldType) => ({ @@ -970,6 +1032,17 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re setTableData(updatedRows); setSelectedEntries(updatedRows); + + const dropdownChangeState: INewMigration = { + ...newMigrationData, + content_mapping: { + ...newMigrationData?.content_mapping, + isDropDownChanged: true, + otherCmsTitle: otherCmsTitle + } + } + dispatch(updateNewMigrationData((dropdownChangeState))); + }; //function to generate group schema structure of source cms @@ -1031,6 +1104,8 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re return value?.data_type === 'json'; case 'enum': return 'enum' in value; + case 'display_type': + return value?.display_type === 'dropdown'; case 'allow_rich_text': return value?.field_metadata?.allow_rich_text === true; case 'Group': @@ -1155,15 +1230,15 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re 'link': 'link', 'reference': 'reference', 'dropdown': 'enum', - 'Droplist': 'enum', + 'Droplist': 'display_type', 'radio': 'enum' }; const OptionsForRow: OptionsType[] = []; - // If OtherContentType label and contentTypesList are present, set the contentTypeSchema - if (otherContentType?.label && contentTypesList) { - const ContentType: ContentTypeList | undefined = contentTypesList?.find( + // If OtherContentType label and contentModels are present, set the contentTypeSchema + if (otherContentType?.label && contentModels) { + const ContentType: ContentTypeList | undefined = contentModels?.find( ({ title }) => title === otherContentType?.label ); setContentTypeSchema(ContentType?.schema); @@ -1244,6 +1319,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re ...updatedExstingField, [data?.uid]: { label: newLabel, value: newvalue } }; + existingField[data?.uid] = { label: newLabel, value: newvalue } } const newValue: string = OptionsForRow[0]?.value?.display_name; @@ -1370,11 +1446,11 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re ...newMigrationData, content_mapping: { ...newMigrationData?.content_mapping, - content_type_mapping: [ + content_type_mapping: { - ...newMigrationData?.content_mapping?.content_type_mapping ?? [], - {[otherCmsTitle]: otherContentType?.label} - ] + ...newMigrationData?.content_mapping?.content_type_mapping ?? {}, + [otherCmsTitle]: otherContentType?.label + } } }; @@ -1416,13 +1492,13 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re setIsContentTypeMapped(true); setIsContentTypeSaved(true); - // const newMigrationDataObj: INewMigration = { - // ...newMigrationData, - // content_mapping: { ...newMigrationData?.content_mapping, isDropDownChanged: false } - // }; + const newMigrationDataObj: INewMigration = { + ...newMigrationData, + content_mapping: { ...newMigrationData?.content_mapping, isDropDownChanged: false } + }; - // dispatch(updateNewMigrationData((newMigrationDataObj))); + dispatch(updateNewMigrationData((newMigrationDataObj))); const savedCT = filteredContentTypes?.map(ct => ct?.id === data?.data?.updatedContentType?.id ? { ...ct, status: data?.data?.updatedContentType?.status } : ct @@ -1430,7 +1506,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re setFilteredContentTypes(savedCT); setContentTypes(savedCT); - await updateContentMapper(orgId, projectID, [...newMigrationData.content_mapping.content_type_mapping,{[otherCmsTitle]: otherContentType?.label}]); + await updateContentMapper(orgId, projectID, {...contentTypeMapped, [otherCmsTitle]: otherContentType?.label}); } else { Notification({ @@ -1447,6 +1523,14 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re const handleDropdownState = () => { setIsDropDownChanged(false); + const dropdownChangeState: INewMigration = { + ...newMigrationData, + content_mapping: { + ...newMigrationData?.content_mapping, + isDropDownChanged: false + } + } + dispatch(updateNewMigrationData((dropdownChangeState ))); } useImperativeHandle(ref, () => ({ @@ -1478,6 +1562,15 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re fieldMapping: updatedRows } }; + let newstate = {} ; + setContentTypeMapped((prevState: ContentTypeMap) => { + const newState = { ...prevState }; + + delete newState[otherCmsTitle]; + newstate = newState; + + return newState; + }); if (orgId && selectedContentType) { const { status } = await resetToInitialMapping( orgId, @@ -1485,10 +1578,13 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re selectedContentType?.id ?? '', dataCs ); + setExistingField({}); setContentTypeSchema([]); - setContentTypeMapped({}); + if (status == 200) { + await updateContentMapper(orgId, projectID, {...newstate} ); + Notification({ notificationContent: { text: 'Content type reset successfully' }, notificationProps: { @@ -1503,6 +1599,50 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re // Function to fetch single content type const handleFetchContentType = async () => { + const { data , status} = await fetchExistingContentType(projectId,'') ; + if(data?.contentTypes?.length <= 0){ + Notification({ + notificationContent: { text: "No content found in the stack" }, + notificationProps: { + position: 'bottom-center', + hideProgressBar: false + }, + type: 'error' + }); + } + const contentTypesArr: ContentTypeList[] = contentModels; + const index = contentModels.findIndex(ct => ct?.uid === data?.uid); + + if(index != -1) { + contentTypesArr[index] = data; + } + + setContentModels(data?.contentTypes); + + + const content_type = data?.contentTypes?.find((item:any)=>item?.title === otherContentType?.label); + const contentTypeKey = Object.keys(contentTypeMapped).find(key => contentTypeMapped[key] === otherContentType?.label); + + + if(! content_type && contentTypeKey){ + const updatedState = { ...contentTypeMapped }; + delete updatedState[contentTypeKey]; + + setContentTypeMapped((prevState: ContentTypeMap) => { + const newState = { ...prevState }; + + delete newState[contentTypeKey] + + return newState; + }); + await updateContentMapper(selectedOrganisation?.value, projectId, {... updatedState} ); + setOtherContentType({ + label: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack`, + value: `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack` + + }); + } + if (otherContentType?.label === "Select Content Type") { Notification({ notificationContent: { text: "Please Select a Content Type to fetch." }, @@ -1515,19 +1655,19 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re } else { const { data , status} = await fetchExistingContentType(projectId, otherContentType?.id ?? ''); - const index = contentTypesList.findIndex(ct => ct?.uid === data?.uid); + const index = contentModels.findIndex(ct => ct?.uid === data?.uid); - const contentTypesArr: ContentTypeList[] = contentTypesList; + const contentTypesArr: ContentTypeList[] = contentModels; if(index != -1) { contentTypesArr[index] = data; } - setContentTypesList(contentTypesArr); + //setContentTypesList(contentTypesArr); setContentTypeSchema(data?.schema); if (status == 201) { Notification({ - notificationContent: { text: 'Content type fetched successfully' }, + notificationContent: { text: 'Content type data fetched successfully' }, notificationProps: { position: 'bottom-center', hideProgressBar: false @@ -1535,6 +1675,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re type: 'success' }); } + } } @@ -1550,7 +1691,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re } ]; - const isOtherContentType = contentTypesList?.some((ct) => ct?.title === otherContentType?.label); + const isOtherContentType = contentModels && contentModels?.some((ct) => ct?.title === otherContentType?.label); if (!isNewStack) { columns?.push({ @@ -1571,7 +1712,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re }); } - const options = contentTypesList?.map((item) => ({ + const options = contentModels?.map((item) => ({ label: item?.title, value: item?.title, id: item?.uid, @@ -1796,7 +1937,7 @@ const ContentMapper = forwardRef(({projectData}: ContentMapperComponentProps, re options={adjustedOption} width="440px" maxWidth="440px" - placeholder={otherContentType && 'Select content type from existing stack'} + placeholder={otherContentType && `Select ${isContentType ? 'Content Type' : 'Global Field'} from Existing Stack`} version="v2" /> diff --git a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx index 7d6055696..adebdcc36 100644 --- a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx +++ b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx @@ -76,13 +76,15 @@ const LoadStacks = (props: LoadFileFormatProps) => { useEffect(() => { newMigrationDataRef.current = newMigrationData; }, [newMigrationData]); - useEffect(()=>{ + + useEffect(() => { if(!isEmptyString(newMigrationData?.destination_stack?.selectedStack?.value)){ setSelectedStack(newMigrationData?.destination_stack?.selectedStack); } - setAllStack(newMigrationData?.destination_stack?.stackArray) + // setAllStack(newMigrationData?.destination_stack?.stackArray) },[newMigrationData?.destination_stack?.selectedStack]) + //Handle new stack details const handleOnSave = async (data: Stack) => { @@ -272,10 +274,10 @@ const LoadStacks = (props: LoadFileFormatProps) => { // placeholder='Select a stack' placeholder={placeholder} - isClearable={newMigrationData?.destination_stack?.stackArray?.length > 0 && !emptyStackValue ? true : false } + isClearable={newMigrationData?.destination_stack?.stackArray?.length > 0 && !emptyStackValue} // hideSelectedOptions={true} - isDisabled={props?.stepComponentProps?.isSummary || false} - error={isLoading ? false : isError ? true : false } + // isDisabled={props?.stepComponentProps?.isSummary || false} + error={isLoading ? false : !!isError } width="600px" hasAddOption={true} // menuIsOpen diff --git a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx index ee8354109..6cfab9018 100644 --- a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx +++ b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx @@ -60,9 +60,6 @@ const LoadFileFormat = (props: LoadFileFormatProps) => { isFileFormatCheckboxChecked: isCheckedBoxChecked } })); - await fileformatConfirmation(selectedOrganisation?.value, projectId, { - fileformat_confirmation: true - }); //call for Step Change props.handleStepChange(props?.currentStep); @@ -138,16 +135,21 @@ const LoadFileFormat = (props: LoadFileFormatProps) => { return (
- } - /> + + {isError &&

{error}

}
diff --git a/ui/src/components/LegacyCms/Actions/LoadPrefix.tsx b/ui/src/components/LegacyCms/Actions/LoadPrefix.tsx index b01125a3c..deb0d31f3 100644 --- a/ui/src/components/LegacyCms/Actions/LoadPrefix.tsx +++ b/ui/src/components/LegacyCms/Actions/LoadPrefix.tsx @@ -118,6 +118,7 @@ const LoadPreFix = (props: LoadSelectCmsProps) => { placeholder={'Enter Affix'} version="v2" error={isError} + aria-label='affix' /> {isError &&

{errorMessage}

} diff --git a/ui/src/components/LegacyCms/index.tsx b/ui/src/components/LegacyCms/index.tsx index 8f2ad27dc..d727f469c 100644 --- a/ui/src/components/LegacyCms/index.tsx +++ b/ui/src/components/LegacyCms/index.tsx @@ -181,7 +181,7 @@ const LegacyCMSComponent = forwardRef(({ legacyCMSData, isCompleted, handleOnAll awsData: legacyCMSData?.awsDetails, isLocalPath: legacyCMSData?.is_localPath }, - isValidated: legacyCMSData?.is_fileValid || newMigrationData?.legacy_cms?.uploadedFile?.isValidated, + isValidated: legacyCMSData?.is_fileValid , }, //need to add backend data once endpoint exposed. affix: legacyCMSData?.affix || '', isFileFormatCheckboxChecked: true, //need to add backend data once endpoint exposed. diff --git a/ui/src/components/MainHeader/index.scss b/ui/src/components/MainHeader/index.scss index 14a769e90..f3c6149c1 100644 --- a/ui/src/components/MainHeader/index.scss +++ b/ui/src/components/MainHeader/index.scss @@ -3,6 +3,7 @@ .mainheader { [class*="Profile_card"] { [class*="Dropdown__menu__list"] { + max-height: 203px; padding: 0; } @@ -79,11 +80,12 @@ } .Dropdown__header__value { - color: #475161; + color: $color-font-base; + font-weight: $font-weight-regular; + height: 22px; max-width: 11.1875rem; width: auto; - color: $color-black-222 !important; - line-height: $line-height-reset; + line-height: $line-height-default; } .Dropdown__menu--primary .Dropdown__menu__list__item, @@ -93,8 +95,27 @@ } .Dropdown__header__label { - line-height: $line-height-reset; - margin-bottom: 8px; + font-weight: $font-weight-bold; + line-height: 1.3; + margin-bottom: 4px; } } + .user-short-name { + background-color: $color-font-white; + border: 2px solid $color-base-white-10; + border-radius: 50%; + color: $color-font-base; + font-weight: $font-weight-bold; + height: $px-32; + width: $px-32; + &:hover { + border-color: $color-brand-primary-base; + } + } + .Dropdown-wrapper .Dropdown__menu--primary { + left: auto; + right: 0; + top: 3rem; + width: 300px; + } } \ No newline at end of file diff --git a/ui/src/components/Modal/index.tsx b/ui/src/components/Modal/index.tsx index a2c0aa628..f5e369f1c 100644 --- a/ui/src/components/Modal/index.tsx +++ b/ui/src/components/Modal/index.tsx @@ -57,7 +57,7 @@ const Modal = (props: ProjectModalProps) => { const nameValidation = (value: string) => { if (!value) { setInputValue(false); - return; + return 'Project name is required.'; } else if (!/^[^\s].*$/.test(value)) { setInputValue(false); return 'Please enter project name.'; @@ -141,7 +141,7 @@ const Modal = (props: ProjectModalProps) => { maxLength="200" error={(meta?.error || meta?.submitError) && meta?.touched} /> - {meta?.error && ( + {meta.touched && meta.error && ( { } const HorizontalStepper = forwardRef( (props: stepperProps, ref: React.ForwardedRef) => { + + const { stepId } = useParams<{ stepId: string }>(); + const stepIndex = parseInt(stepId || '', 10) - 1; + const { steps, className, emptyStateMsg, hideTabView, testId } = props; - const [showStep, setShowStep] = useState(0); + const [showStep, setShowStep] = useState(stepIndex); const [stepsCompleted, setStepsCompleted] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); - const { stepId } = useParams<{ stepId: string }>(); - const navigate = useNavigate(); const { projectId = '' } = useParams(); @@ -156,7 +158,8 @@ const HorizontalStepper = forwardRef( notificationContent: { text: `Please select a stack to proceed further` }, type: 'warning' }) - } else if (newMigrationData?.destination_stack?.selectedStack?.value !== newMigrationData?.stackDetails?.value) { + } + else if (newMigrationData?.destination_stack?.selectedStack?.value !== newMigrationData?.stackDetails?.value) { return Notification({ notificationContent: { text: `Please save the stack to proceed further` }, type: 'warning' diff --git a/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.scss b/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.scss index f578a711d..6e0dc9670 100644 --- a/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.scss +++ b/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.scss @@ -128,5 +128,5 @@ padding-left: .25em; } .required-text{ - margin-left: -10px; + margin-left: -5px; } \ No newline at end of file diff --git a/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.tsx b/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.tsx index d50a592f6..27eab15db 100644 --- a/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.tsx +++ b/ui/src/components/Stepper/VerticalStepper/AutoVerticalStepper.tsx @@ -84,7 +84,7 @@ const AutoVerticalStepper = React.forwardRef<
- {isRequired || data?.isRequired && (required) } + {(isRequired || data?.isRequired) && (required) } {data?.ifReadonly && (read only)}
{data.titleNote ? data.titleNote : ''} diff --git a/ui/src/components/TestMigration/index.tsx b/ui/src/components/TestMigration/index.tsx index ed0189939..a4bdf8b76 100644 --- a/ui/src/components/TestMigration/index.tsx +++ b/ui/src/components/TestMigration/index.tsx @@ -79,7 +79,7 @@ const TestMigration = () => {
Execution Logs
- +
diff --git a/ui/src/components/layout/AppLayout/index.tsx b/ui/src/components/layout/AppLayout/index.tsx index 9fe955566..295985da0 100644 --- a/ui/src/components/layout/AppLayout/index.tsx +++ b/ui/src/components/layout/AppLayout/index.tsx @@ -1,13 +1,15 @@ // Libraries import { FC, ReactNode, useEffect } from 'react'; import { Params, useLocation, useParams } from 'react-router'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getUserDetails } from '../../../store/slice/authSlice'; // Component import MainHeader from '../../MainHeader'; import SideBar from '../../SideBar'; +import { RootState } from '../../../store'; +import useAuthCheck from '../../../hooks/authentication'; type IProps = { children?: ReactNode; @@ -17,12 +19,16 @@ const AppLayout: FC = ({ children }) => { const location = useLocation(); const dispatch = useDispatch(); + const authentication = useSelector((state:RootState)=>state?.authentication?.isAuthenticated); + const projectId = location?.pathname?.split('/')?.[2]; useEffect(()=>{ dispatch(getUserDetails()); - },[dispatch]) + },[dispatch]); + + useAuthCheck(); return ( <> diff --git a/ui/src/context/app/app.interface.ts b/ui/src/context/app/app.interface.ts index c723ba361..7f82fc8e5 100644 --- a/ui/src/context/app/app.interface.ts +++ b/ui/src/context/app/app.interface.ts @@ -5,6 +5,7 @@ import { import { ICardType, defaultCardType } from '../../components/Common/Card/card.interface'; import { CTA } from '../../types/common.interface'; import { IFilterType } from '../../components/Common/Modal/FilterModal/filterModal.interface'; +import { ContentTypeList } from '../../components/ContentMapper/contentMapper.interface'; export interface ICTA { title: string; href: string; @@ -167,12 +168,13 @@ export interface IDestinationStack { stackArray: IDropDown[]; } export interface IContentMapper { - content_type_mapping: ContentTypeMap[]; + content_type_mapping: ContentTypeMap; isDropDownChanged?: boolean; otherCmsTitle?: string; + contentTypeList:ContentTypeList[] } export interface INewMigration { - mapperKeys: ContentTypeMap[]; + mapperKeys: ContentTypeMap; legacy_cms: ILegacyCms; destination_stack: IDestinationStack; content_mapping: IContentMapper; @@ -305,9 +307,10 @@ export const DEFAULT_DESTINATION_STACK: IDestinationStack = { }; export const DEFAULT_CONTENT_MAPPER: IContentMapper = { - content_type_mapping: [], + content_type_mapping: {}, isDropDownChanged: false, - otherCmsTitle: '' + otherCmsTitle: '', + contentTypeList: [], }; export const DEFAULT_TEST_MIGRATION: ITestMigration = { @@ -316,7 +319,7 @@ export const DEFAULT_TEST_MIGRATION: ITestMigration = { }; export const DEFAULT_NEW_MIGRATION: INewMigration = { - mapperKeys: [], + mapperKeys: {}, legacy_cms: DEFAULT_LEGACY_CMS, destination_stack: DEFAULT_DESTINATION_STACK, content_mapping: DEFAULT_CONTENT_MAPPER, diff --git a/ui/src/hooks/authentication.tsx b/ui/src/hooks/authentication.tsx new file mode 100644 index 000000000..7f7b701ef --- /dev/null +++ b/ui/src/hooks/authentication.tsx @@ -0,0 +1,33 @@ +import { useSelector, useDispatch } from 'react-redux'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import {jwtDecode} from 'jwt-decode'; +import { reInitiliseState } from '../store/slice/authSlice'; + +const useAuthCheck = () => { + const authToken = useSelector((state:any) => state.authentication.authToken); + const selectedOrganisation = useSelector((state:any) => state.authentication.selectedOrganisation); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + if (authToken) { + try { + const { exp } = jwtDecode(authToken); + const currentTime = Date.now() / 1000; + + if (exp && exp < currentTime) { + + dispatch(reInitiliseState()); + navigate('/'); + } + } catch (error) { + console.error('Error decoding token:', error); + dispatch(reInitiliseState()); + navigate('/'); + } + } + }, [authToken, selectedOrganisation, dispatch, navigate]); +}; + +export default useAuthCheck; diff --git a/ui/src/pages/Login/index.tsx b/ui/src/pages/Login/index.tsx index ccf434f26..b5e684ae0 100644 --- a/ui/src/pages/Login/index.tsx +++ b/ui/src/pages/Login/index.tsx @@ -1,7 +1,7 @@ // Libraries import { FC,useEffect, useState } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { setAuthToken } from '../../store/slice/authSlice'; import { @@ -157,7 +157,14 @@ const Login: FC = () => { if (response?.status === 200 && response?.data?.message === LOGIN_SUCCESSFUL_MESSAGE) { setIsLoading(false); setDataInLocalStorage('app_token', response?.data?.app_token); - dispatch(setAuthToken(response?.data?.token)); + const authenticationObj = { + + authToken: response?.data?.app_token, + isAuthenticated: true + } + + dispatch(setAuthToken(authenticationObj)); + setLoginStates((prev) => ({ ...prev, submitted: true })); navigate(`/projects`, { replace: true }); } diff --git a/ui/src/pages/Migration/index.tsx b/ui/src/pages/Migration/index.tsx index 63597e17a..c755d00ad 100644 --- a/ui/src/pages/Migration/index.tsx +++ b/ui/src/pages/Migration/index.tsx @@ -2,14 +2,14 @@ import { useEffect, useState, useRef } from 'react'; import { Params, useNavigate, useParams } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; -import { Notification } from '@contentstack/venus-components'; // Redux files import { RootState } from '../../store'; import { updateMigrationData, updateNewMigrationData } from '../../store/slice/migrationDataSlice'; // Services -import { getMigrationData, updateCurrentStepData, updateLegacyCMSData, updateDestinationStack, createTestStack, updateAffixData, fileformatConfirmation, updateFileFormatData, affixConfirmation, updateStackDetails } from '../../services/api/migration.service'; +import { getMigrationData, updateCurrentStepData, updateLegacyCMSData, updateDestinationStack, createTestStack, updateAffixData, fileformatConfirmation, updateFileFormatData, affixConfirmation, updateStackDetails, getOrgDetails } from '../../services/api/migration.service'; +import { getAllStacksInOrg } from '../../services/api/stacks.service'; import { getCMSDataFromFile } from '../../cmsData/cmsSelector'; // Utilities @@ -34,6 +34,9 @@ import DestinationStackComponent from '../../components/DestinationStack'; import ContentMapper from '../../components/ContentMapper'; import TestMigration from '../../components/TestMigration'; import MigrationExecution from '../../components/MigrationExecution'; +import { cbModal, Notification } from '@contentstack/venus-components'; +import SaveChangesModal from '../../components/Common/SaveChangesModal'; +import { ModalObj } from '../../components/Modal/modal.interface'; type StepperComponentRef = { handleStepChange: (step: number) => void; @@ -48,6 +51,7 @@ const Migration = () => { const [curreentStepIndex, setCurrentStepIndex] = useState(0); const [isCompleted, setIsCompleted] = useState(false); const [isProjectMapper, setIsProjectMapper] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const params: Params = useParams(); const { projectId = '' } = useParams(); @@ -362,37 +366,111 @@ const Migration = () => { }; const handleOnClickContentMapper = async (event: MouseEvent) => { - setIsLoading(true); - - event.preventDefault(); + setIsModalOpen(true); - const data = { - name: newMigrationData?.destination_stack?.selectedStack?.label, - description: 'test migration stack', - master_locale: newMigrationData?.destination_stack?.selectedStack?.master_locale - }; + //get org plan details + const orgDetails = await getOrgDetails(selectedOrganisation?.value); + const stacks_details_key = Object.keys(orgDetails?.data?.organization?.plan?.features).find(key => orgDetails?.data?.organization?.plan?.features[key].uid === 'stacks') || ''; - const res = await createTestStack( - newMigrationData?.destination_stack?.selectedOrg?.value, - projectId, - data - ); + const max_stack_limit = orgDetails?.data?.organization?.plan?.features[stacks_details_key]?.max_limit; - const newMigrationDataObj: INewMigration = { - ...newMigrationData, - test_migration: { stack_link: res?.data?.data?.url, stack_api_key: res?.data?.data?.data?.stack?.api_key } - }; + const stackData = await getAllStacksInOrg(selectedOrganisation?.value, ''); // org id will always be there + + const stack_count = stackData?.data?.stacks?.length; - dispatch(updateNewMigrationData((newMigrationDataObj))); - if (res?.status) { + if (stack_count >= max_stack_limit) { setIsLoading(false); + Notification({ + notificationContent: { text: 'You have reached the maximum limit of stacks for your organization' }, + type: 'warning' + }); + return; + } - const url = `/projects/${projectId}/migration/steps/4`; - navigate(url, { replace: true }); + if(newMigrationData?.content_mapping?.isDropDownChanged){ + return cbModal({ + component: (props: ModalObj) => ( + { + setIsLoading(true); + const data = { + name: newMigrationData?.destination_stack?.selectedStack?.label, + description: 'test migration stack', + master_locale: newMigrationData?.destination_stack?.selectedStack?.master_locale + }; + + const res = await createTestStack( + newMigrationData?.destination_stack?.selectedOrg?.value, + projectId, + data + ); + + if (res?.status) { + setIsLoading(false); + const newMigrationDataObj: INewMigration = { + ...newMigrationData, + content_mapping: { ...newMigrationData?.content_mapping, isDropDownChanged: false }, + + test_migration: { stack_link: res?.data?.data?.url, stack_api_key: res?.data?.data?.data?.stack?.api_key } + }; + + dispatch(updateNewMigrationData((newMigrationDataObj))); + + const url = `/projects/${projectId}/migration/steps/4`; + navigate(url, { replace: true }); + + await updateCurrentStepData(selectedOrganisation.value, projectId); + handleStepChange(3); + }}} + dropdownStateChange={changeDropdownState} + /> + ), + modalProps: { + size: 'xsmall', + shouldCloseOnOverlayClick: false + } + }); + + } + else{ + event.preventDefault(); + setIsLoading(true); + const data = { + name: newMigrationData?.destination_stack?.selectedStack?.label, + description: 'test migration stack', + master_locale: newMigrationData?.destination_stack?.selectedStack?.master_locale + }; + + const res = await createTestStack( + newMigrationData?.destination_stack?.selectedOrg?.value, + projectId, + data + ); + + const newMigrationDataObj: INewMigration = { + ...newMigrationData, + test_migration: { stack_link: res?.data?.data?.url, stack_api_key: res?.data?.data?.data?.stack?.api_key } + }; + + dispatch(updateNewMigrationData((newMigrationDataObj))); + if (res?.status) { + setIsLoading(false); + + const url = `/projects/${projectId}/migration/steps/4`; + navigate(url, { replace: true }); + + await updateCurrentStepData(selectedOrganisation.value, projectId); + handleStepChange(3); + } - await updateCurrentStepData(selectedOrganisation.value, projectId); - handleStepChange(3); } + + + } const handleOnClickTestMigration = async () => { @@ -405,13 +483,6 @@ const Migration = () => { handleStepChange(4); } - const handleOnClickFunctions = [ - handleOnClickLegacyCms, - handleOnClickDestinationStack, - handleOnClickContentMapper, - handleOnClickTestMigration - ]; - const changeDropdownState = () =>{ const newMigrationDataObj: INewMigration = { ...newMigrationData, @@ -420,6 +491,15 @@ const Migration = () => { dispatch(updateNewMigrationData((newMigrationDataObj))); } + + const handleOnClickFunctions = [ + handleOnClickLegacyCms, + handleOnClickDestinationStack, + handleOnClickContentMapper, + handleOnClickTestMigration + ]; + + return ( diff --git a/ui/src/pages/Projects/index.scss b/ui/src/pages/Projects/index.scss index a69ea0f50..0fecb227f 100644 --- a/ui/src/pages/Projects/index.scss +++ b/ui/src/pages/Projects/index.scss @@ -45,6 +45,7 @@ color: $color-font-base; a { color: $color-brand-primary-base; + text-decoration: underline; } } } @@ -55,4 +56,7 @@ .EmptyState__title { padding-top: 15px; } +} +.create-project-cta { + line-height: $line-height-default; } \ No newline at end of file diff --git a/ui/src/pages/Projects/index.tsx b/ui/src/pages/Projects/index.tsx index 8e2896964..0b0319712 100644 --- a/ui/src/pages/Projects/index.tsx +++ b/ui/src/pages/Projects/index.tsx @@ -1,6 +1,6 @@ // Libraries import { useEffect, useState } from 'react'; -import { PageLayout, EmptyState, Button, Icon, cbModal } from '@contentstack/venus-components'; +import { PageLayout, EmptyState, Button, Icon, cbModal, StackCardSkeleton} from '@contentstack/venus-components'; import { jsonToHtml } from '@contentstack/json-rte-serializer'; import HTMLReactParser from 'html-react-parser'; import { useLocation } from 'react-router-dom'; @@ -67,6 +67,7 @@ const Projects = () => { },[]); const fetchProjects = async () => { + setLoadStatus(true); if (selectedOrganisation?.value) { const { data, status } = await getAllProjects(selectedOrganisation?.value || ''); //org id will always present if (status === 200) { @@ -171,7 +172,7 @@ const Projects = () => { {loadStatus ? (
{[...Array(20)].map((e, i) => ( - + ))}
) : ( @@ -182,7 +183,7 @@ const Projects = () => { )) )} - {projects && projects?.length === 0 && !searchText && ( + {!loadStatus && projects?.length === 0 && !searchText && allProjects?.length === 0 && ( { } }; +export const getExistingGlobalFields = async (projectId: string) => { + try { + return await getCall(`${API_VERSION}/mapper/globalFields/${projectId}`, options); + } catch (error) { + if (error instanceof Error) { + throw new Error(`${error.message}`); + } else { + throw new Error('Unknown error'); + } + } +}; + export const updateContentType = async ( orgId: string, projectId: string, @@ -269,4 +281,14 @@ export const updateStackDetails = async(orgId: string, projectId: string, data: return error; } -} \ No newline at end of file +} + +export const getOrgDetails = async(orgId: string) => { + try { + return await getCall(`${API_VERSION}/org/${orgId}/get_org_details`, options); + } catch (error) { + return error; + } +} + +// const { orgId } = req.params; \ No newline at end of file diff --git a/ui/src/store/slice/authSlice.tsx b/ui/src/store/slice/authSlice.tsx index 4abe1409c..c57c1fc38 100644 --- a/ui/src/store/slice/authSlice.tsx +++ b/ui/src/store/slice/authSlice.tsx @@ -76,8 +76,8 @@ const authSlice = createSlice({ initialState, reducers:{ setAuthToken : (state, action)=>{ - state.authToken = action?.payload; - state.isAuthenticated = !!action?.payload; + state.authToken = action?.payload?.authToken; + state.isAuthenticated = action?.payload?.isAuthenticated; }, setUser : (state, action) => { state.user = action?.payload; diff --git a/upload-api/.env b/upload-api/.env index 5f3a84b6c..2c50b6496 100644 --- a/upload-api/.env +++ b/upload-api/.env @@ -1,2 +1,2 @@ PORT=4002 -NODE_BACKEND_API =http://localhost:5000 \ No newline at end of file +NODE_BACKEND_API =http://localhost:5001 \ No newline at end of file diff --git a/upload-api/src/config/index.ts b/upload-api/src/config/index.ts index 39e83b803..2ffc1d757 100644 --- a/upload-api/src/config/index.ts +++ b/upload-api/src/config/index.ts @@ -12,5 +12,5 @@ export default { bucketName: 'migartion-test', buketKey: 'project/package 45.zip' }, - localPath: '/Users/umesh.more/Documents/ui-migration/migration-v2-node-server/upload-api/extracted_files/package 45.zip' + localPath: '/Users/rohit/Desktop/package 45.zip' };