diff --git a/api/src/models/FieldMapper.ts b/api/src/models/FieldMapper.ts index b011a708b..b4662188b 100644 --- a/api/src/models/FieldMapper.ts +++ b/api/src/models/FieldMapper.ts @@ -1,6 +1,14 @@ import { JSONFile } from "lowdb/node"; import LowWithLodash from "../utils/lowdb-lodash.utils.js"; +interface Advanced { + validationRegex: string, + Mandatory:boolean, + Multiple:boolean, + Unique:boolean, + NonLocalizable:boolean +} + interface FieldMapper { field_mapper: { id: string; @@ -13,6 +21,7 @@ interface FieldMapper { isDeleted: boolean; backupFieldType: string; refrenceTo: { uid: string; title: string }; + advanced:Advanced }[]; } diff --git a/api/src/services/org.service.ts b/api/src/services/org.service.ts index f72a9998b..0eed96e9b 100644 --- a/api/src/services/org.service.ts +++ b/api/src/services/org.service.ts @@ -48,15 +48,10 @@ const getAllStacks = async (req: Request): Promise => { status: err.response.status, }; } - + let locale = await getStackLocal(token_payload, res.data.stacks); return { data: { - stacks: - res.data.stacks?.map((stack: any) => ({ - name: stack.name, - api_key: stack.api_key, - master_locale: stack.master_locale, - })) || [], + stacks: locale, }, status: res.status, }; @@ -280,6 +275,62 @@ const getStackStatus = async (req: Request) => { ); } }; +//get all locals of particular stack +const getStackLocal = async (token_payload: any, data: any) => { + const srcFun = "getStackLocal"; + return new Promise(async (resolve, reject) => { + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + let stacks = []; + for (let stack of data) { + const [err, res] = await safePromise( + https({ + method: "GET", + url: `${config.CS_API[ + token_payload?.region as keyof typeof config.CS_API + ]!}/locales`, + headers: { + api_key: stack.api_key, + authtoken, + }, + }) + ); + if (err) { + logger.error( + getLogMessage( + srcFun, + HTTP_TEXTS.CS_ERROR, + token_payload, + err.response.data + ) + ); + + return { + data: err.response.data, + status: err.response.status, + }; + } + let localesArr:any =[] + res?.data?.locales.map((lang:any)=>{ + return localesArr.push({ + code:lang.code, + name:lang.name + }) + }) + + let obj = { + name: stack.name, + api_key: stack.api_key, + master_locale: stack.master_locale, + locales: localesArr + }; + stacks.push(obj); + } + resolve(stacks); + }); +}; export const orgService = { getAllStacks, diff --git a/api/src/validators/auth.validator.ts b/api/src/validators/auth.validator.ts index 3ddc61d5e..8ba097fe6 100644 --- a/api/src/validators/auth.validator.ts +++ b/api/src/validators/auth.validator.ts @@ -50,6 +50,6 @@ export default checkSchema({ errorMessage: VALIDATION_ERRORS.STRING_REQUIRED.replace("$", "2FA Token"), bail: true, }, - trim: true, + trim: false, }, }); diff --git a/ui/src/cmsData/login.json b/ui/src/cmsData/login.json index afbe99fa2..0c6bd9df7 100644 --- a/ui/src/cmsData/login.json +++ b/ui/src/cmsData/login.json @@ -3,7 +3,7 @@ "locale": "en-us", "uid": "blt058427c4131df78a", "_in_progress": false, - "copyrightText": "© 2023-2024 Contentstack Inc. All rights reserved.", + "copyrightText": "Contentstack Inc. All rights reserved.", "created_at": "2024-02-22T11:20:53.435Z", "created_by": "bltb317ebe35429ebe6", "heading": "Stacks Not Suites", diff --git a/ui/src/components/AccountPage/index.tsx b/ui/src/components/AccountPage/index.tsx index a5a4d4479..5cb8a7460 100644 --- a/ui/src/components/AccountPage/index.tsx +++ b/ui/src/components/AccountPage/index.tsx @@ -10,6 +10,9 @@ import './index.scss'; const AccountPage = (props: AccountObj): JSX.Element => { const { heading, subtitle, copyrightText } = props.data; + const currentYear = new Date().getFullYear(); + const previousYear = currentYear - 1; + return ( // eslint-disable-next-line react/no-unknown-property
@@ -32,8 +35,8 @@ const AccountPage = (props: AccountObj): JSX.Element => {
-
{props.children}
-

{copyrightText}

+
{props?.children}
+

{`© ${previousYear}-${currentYear} ${copyrightText}`}

); diff --git a/ui/src/components/AdvancePropertise/index.tsx b/ui/src/components/AdvancePropertise/index.tsx index 87dd78ec7..6248e3e6b 100644 --- a/ui/src/components/AdvancePropertise/index.tsx +++ b/ui/src/components/AdvancePropertise/index.tsx @@ -1,23 +1,61 @@ +import { useState } from 'react'; + import { ModalBody, ModalHeader, FieldLabel, TextInput, - ToggleSwitch + ToggleSwitch, + Tooltip } from '@contentstack/venus-components'; import './index.scss'; + export interface SchemaProps { fieldtype: string; value: any; rowId: string; + updateFieldSettings:(rowId:string, value:any, checkBoxChanged:boolean)=> void; + isLocalised:boolean; closeModal: () => void; + data:any } -const AdvancePropertise = (props: SchemaProps) => { +const AdvancePropertise = (props: SchemaProps) => { + + + const [toggleStates, setToggleStates] = useState({ + validationRegex: props?.value?.ValidationRegex, + mandatory: props?.value?.Mandatory, + multiple: props?.value?.Multiple, + unique: props?.value?.Unique, + nonLocalizable: props?.value?.NonLocalizable + }); + + const handleToggleChange = (field: string, value: boolean, checkBoxChanged: boolean) => { + + setToggleStates(prevStates => ({ + ...prevStates, + [field]: value + })); + + props?.updateFieldSettings(props?.rowId, {[field?.charAt(0)?.toUpperCase() + field?.slice(1)]: value}, checkBoxChanged); +}; + + const handleOnChange = ( field: string, event: any, checkBoxChanged: boolean) => { + setToggleStates(prevStates => ({ + ...prevStates, + [field]: event?.target?.value + })); + + props?.updateFieldSettings(props?.rowId, {[field?.charAt(0)?.toUpperCase() + field?.slice(1)]: event?.target?.value}, checkBoxChanged); + + } + + return ( <> - + Validation (Regex) @@ -25,8 +63,10 @@ const AdvancePropertise = (props: SchemaProps) => { handleOnChange("validationRegex",e, true)} /> Options @@ -36,26 +76,35 @@ const AdvancePropertise = (props: SchemaProps) => { label="Mandatory" labelColor="primary" labelPosition="right" - checked={props?.value?.mandatory} + checked={toggleStates?.mandatory} + onChange={(e:any) => handleToggleChange("mandatory" , e?.target?.checked, true)} /> handleToggleChange("multiple" ,e?.target?.checked, true)} /> handleToggleChange("unique" , e?.target?.checked,true)} /> + handleToggleChange("nonLocalizable" , e?.target?.checked,true)} /> + +

If enabled, editing this field is restricted in localized entries. The field will use the value of the master-language entry in all localized entries. diff --git a/ui/src/components/Common/AddStack/addStack.tsx b/ui/src/components/Common/AddStack/addStack.tsx index 4c0a466a3..9d948f472 100644 --- a/ui/src/components/Common/AddStack/addStack.tsx +++ b/ui/src/components/Common/AddStack/addStack.tsx @@ -28,6 +28,9 @@ import { AddStackCMSData, defaultAddStackCMSData } from './addStack.interface'; // Styles import './addStack.scss'; +import { getAllLocales } from '../../../services/api/user.service'; +import { validateObject } from '../../../utilities/functions'; +import { IDropDown } from '../../../context/app/app.interface'; export interface Stack { name: string; description: string; @@ -37,11 +40,11 @@ export interface Stack { const AddStack = (props: any): JSX.Element => { const [isProcessing, setIsProcessing] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [allLocales, setAllLocales] = useState([]); const [addStackCMSData, setAddStackCMSData] = useState(defaultAddStackCMSData); - const onSubmit = async (formData: any) => { setIsProcessing(true); - const resp = await props.onSubmit({ + const resp = await props?.onSubmit({ name: formData?.name || props?.defaultValues?.name, description: formData?.description || props?.defaultValues?.description, locale: formData?.locale?.value || props?.defaultValues?.locale @@ -52,7 +55,7 @@ const AddStack = (props: any): JSX.Element => { notificationContent: { text: 'Stack created successfully' }, type: 'success' }); - props.closeModal(); + props?.closeModal(); } else { Notification({ notificationContent: { text: 'Failed to create the stack' }, type: 'error' }); } @@ -77,6 +80,25 @@ const AddStack = (props: any): JSX.Element => { console.error(err); setIsLoading(false); }); + + //fetch all locales + getAllLocales(props?.selectedOrganisation).then((response:any) => { + const rawMappedLocalesMapped = validateObject(response?.data) && response?.data?.locales + ? Object?.keys(response?.data?.locales)?.map((key) => ({ + uid: key, + label: response?.data?.locales[key], + value: key, + master_locale: key, + locales:[], + created_at: key + })) + : []; + setAllLocales(rawMappedLocalesMapped); + }) + .catch((err: any) => { + console.error(err); + }); + //org id will always be there }, []); return ( <> @@ -202,7 +224,7 @@ const AddStack = (props: any): JSX.Element => { }} name="locale" width="300px" - options={props?.locales} + options={allLocales} maxMenuHeight={200} isClearable={true} version={'v2'} diff --git a/ui/src/components/ContentMapper/contentMapper.interface.ts b/ui/src/components/ContentMapper/contentMapper.interface.ts index be0a68242..dbdabf861 100644 --- a/ui/src/components/ContentMapper/contentMapper.interface.ts +++ b/ui/src/components/ContentMapper/contentMapper.interface.ts @@ -9,6 +9,13 @@ interface mapDataType { contentTypes: ContentType[]; projectId: string; } +interface Advanced { + ValidationRegex: string, + mandatory:boolean, + multiple:boolean, + unique:boolean, + nonLocalizable:boolean +} export interface ContentMapperType { content_types_heading?: string; cta: CTA; @@ -24,7 +31,7 @@ export interface ContentstackFields { export interface FieldTypes { label: string; - value: string; + value: any; } export interface TableTypes { sortBy: any; @@ -47,6 +54,7 @@ export interface ContentType { export interface FieldMapType { ContentstackFieldType: string; + child?: FieldMapType[] | undefined; backupFieldType: string; contentstackField: string; contentstackFieldUid: string; @@ -55,7 +63,8 @@ export interface FieldMapType { otherCmsType: string; uid: string; id: string; - _invalid?: boolean; + _canSelect?: boolean; + advanced:Advanced } export interface ItemStatus { @@ -99,3 +108,11 @@ export interface optionsType { export interface ExstingContentTypeMatch { [key: string]: string; } + +export interface UidMap { + [key: string]: boolean; +} + +export interface ContentTypeMap { + [key: string]: string; +} diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 4435f6fd4..3259d7689 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -12,7 +12,10 @@ import { Tooltip, Notification, cbModal, - InstructionText + InstructionText, + ModalHeader, + ModalBody, + ModalFooter } from '@contentstack/venus-components'; import { jsonToHtml } from '@contentstack/json-rte-serializer'; import HTMLReactParser from 'html-react-parser'; @@ -47,7 +50,9 @@ import { ExistingFieldType, ContentTypeList, ContentTypesSchema, - optionsType + optionsType, + UidMap, + ContentTypeMap } from './contentMapper.interface'; import { ItemStatusMapProp } from '@contentstack/venus-components/build/components/Table/types'; import { ModalObj } from '../Modal/modal.interface'; @@ -81,10 +86,11 @@ const Fields: Mapping = { CheckBox: 'Select' }; -interface ContentTypeMap { - [key: string]: string; +interface ModalProps { + e:React.MouseEvent; + newIndex:number; + closeModal: () => void; } - const ContentMapper = () => { /** ALL CONTEXT HERE */ const { @@ -135,11 +141,25 @@ const ContentMapper = () => { const [otherCmsUid, setotherCmsUid] = useState(contentTypes[0]?.otherCmsUid); const [isContentTypeMapped, setisContentTypeMapped] = useState(false); const [isContentTypeSaved, setisContentTypeSaved] = useState(false); + const [advancePropertise, setadvancePropertise] = useState({ + validationRegex:'', + Mandatory: false, + Multiple: false, + Unique: false, + NonLocalizable: false + }); + const [isLocalised, setisLocalised] = useState(newMigrationData?.destination_stack?.selectedStack?.locales?.length > 1 ? true : false); + + const [active, setActive] = useState(null ?? 0); const [searchContentType, setSearchContentType] = useState(''); + const [rowIds, setRowIds] = useState({}) + const [selectedEntries, setSelectedEntries] = useState([]); + + /** ALL HOOKS Here */ const { projectId = '' } = useParams(); const navigate = useNavigate(); @@ -167,8 +187,8 @@ const ContentMapper = () => { // Make title and url field non editable useEffect(() => { tableData?.forEach((field) => { - if (field?.otherCmsField === 'title' || field?.otherCmsField === 'url') { - field._invalid = true; + if (field?.otherCmsField !== 'title' && field?.otherCmsField !== 'url') { + field._canSelect = true; } }); }) @@ -197,6 +217,15 @@ const ContentMapper = () => { } }, [tableData, isContentTypeSaved]); + // To make all the fields checked + useEffect(() => { + const selectedId = tableData.reduce((acc, item) => { + acc[item?.id] = true; + return acc; + }, {}); + setRowIds(selectedId) + }, [tableData]) + // Method to fetch content types const fetchContentTypes = async (searchText: string) => { const { data } = await getContentTypes(projectId || '', 0, 10, searchContentType || ''); //org id will always present @@ -226,8 +255,6 @@ const ContentMapper = () => { // Method to search content types const handleSearch = async (searchCT: string) => { - console.log("searchCT", searchCT); - setSearchContentType(searchCT) const { data } = await getContentTypes(projectId, 0, 5, searchCT || ''); //org id will always present @@ -318,33 +345,94 @@ const ContentMapper = () => { fetchFields(contentTypes?.[i]?.id, searchText || ''); setotherCmsUid(contentTypes?.[i]?.otherCmsUid); setSelectedContentType(contentTypes?.[i]); - }; + }; //function to handle previous content type navigation const handlePrevClick = (e: React.MouseEvent) => { const newIndex = currentIndex > 0 ? currentIndex - 1 : 0; - setCurrentIndex(newIndex); - openContentType(e, newIndex); - document.querySelectorAll('.ct-list li').forEach((ctLi, ind) => { - if (newIndex === ind) { - ctLi?.classList?.add('active-ct'); - } - }); - }; + if(isDropDownChanged){ + handleSaveContentTypeModal(e, newIndex) - // function to handle next content type navigation - const handleNextClick = (e: React.MouseEvent) => { - if (currentIndex < contentTypes?.length - 1) { - const newIndex = currentIndex + 1; + }else{ setCurrentIndex(newIndex); openContentType(e, newIndex); document.querySelectorAll('.ct-list li').forEach((ctLi, ind) => { if (newIndex === ind) { ctLi?.classList?.add('active-ct'); } - }); + }) + + } + }; + + // function to handle next content type navigation + const handleNextClick = (e: React.MouseEvent) => { + if (currentIndex < contentTypes?.length - 1) { + const newIndex = currentIndex + 1; + + if(isDropDownChanged){ + handleSaveContentTypeModal(e, newIndex) + + }else{ + setCurrentIndex(newIndex); + openContentType(e, newIndex); + document.querySelectorAll('.ct-list li').forEach((ctLi, ind) => { + if (newIndex === ind) { + ctLi?.classList?.add('active-ct'); + } + }) + + } + } }; + const SaveContentType = (props:ModalProps) => { + + return ( + <> + + +

Hey there! You have unsaved changes on this page.

+
+ + + + + + + + + ); + } + const handleSaveContentTypeModal = (e:any, newIndex:number) => { + return cbModal({ + component: (props: ModalObj) => ( + + ), + modalProps: { + shouldCloseOnOverlayClick: true, + size: 'small' + } + }); + } // Function to get exisiting content types list const fetchExistingContentTypes = async () => { @@ -353,7 +441,26 @@ const ContentMapper = () => { setContentTypesList(data?.contentTypes); } }; + + const updateFieldSettings = (rowId:string, updatedSettings:any, checkBoxChanged:boolean) => { + setisDropDownCHanged(checkBoxChanged); + //setadvancePropertise(...updatedSettings); + + const newTableData = tableData?.map(row => { + if (row?.uid === rowId) { + setadvancePropertise({...row?.advanced, ...updatedSettings}); + + return { ...row, advanced: { ...row?.advanced, ...updatedSettings } }; + } + return row; + }); + + + setTableData(newTableData); + }; + + const handleOnClick = (title: string) => { return cbModal({ component: (props: ModalObj) => ( @@ -384,13 +491,16 @@ const ContentMapper = () => { ); }; - interface UidMap { - [key: string]: boolean; + + // Function to handle selected fields + const handleSelectedEntries = (singleSelectedRowIds: any, selectedData: any) => { + const selectedObj: any = {} + singleSelectedRowIds.forEach((uid: any) => { + selectedObj[uid] = true; + }) + setRowIds(selectedObj) + setSelectedEntries(selectedData) } - const rowIds = tableData.reduce((acc, item) => { - acc[item?.id] = true; - return acc; - }, {}); // Method for change select value const handleValueChange = (value: FieldTypes, rowIndex: string) => { @@ -412,15 +522,20 @@ const ContentMapper = () => { const handleAdvancedSetting = ( fieldtype: string, - fieldvalue: ExistingFieldType, - rowId: string + fieldvalue: any, + rowId: string, + data: any ) => { + return cbModal({ component: (props: ModalObj) => ( ), @@ -435,7 +550,7 @@ const ContentMapper = () => { const data = { name: newMigrationData?.destination_stack?.selectedStack?.label, description: 'test migration stack', - master_locale: newMigrationData?.destination_stack?.selectedStack?.locale + master_locale: newMigrationData?.destination_stack?.selectedStack?.master_locale }; const res = await createTestStack( newMigrationData?.destination_stack?.selectedOrg?.value, @@ -474,27 +589,37 @@ const ContentMapper = () => { maxWidth="290px" isClearable={false} options={option} - // isDisabled={data?.otherCmsField === 'title' || data?.otherCmsField === 'url'} + isDisabled={data?.ContentstackFieldType === "group" || data?.otherCmsField === 'title' || data?.otherCmsField === 'url'} /> - - handleAdvancedSetting(data?.ContentstackFieldType, exstingField, data?.uid) - } - /> + + + handleAdvancedSetting(data?.ContentstackFieldType, data?.advanced, data?.uid, data) + } + /> + + ); }; - const handleFieldChange = (selectedValue: FieldTypes, rowIndex: string) => { + const handleFieldChange = (selectedValue: FieldTypes, rowIndex: string) => { setisDropDownCHanged(true); setexsitingField((prevOptions) => ({ ...prevOptions, [rowIndex]: { label: selectedValue?.label, value: selectedValue?.value } })); + setadvancePropertise({ + validationRegex: selectedValue?.value?.format, + Mandatory: selectedValue?.value?.mandatory, + Multiple: selectedValue?.value?.multiple, + Unique: selectedValue?.value?.unique, + NonLocalizable: selectedValue?.value?.non_localizable + }); if (isDropDownChanged && isContentTypeSaved) { setSelectedOptions((prevSelected) => { @@ -505,11 +630,23 @@ const ContentMapper = () => { const updatedRows = tableData.map((row) => { if (row?.uid === rowIndex) { - return { ...row, contentstackField: selectedValue?.label }; + + return { + ...row, + contentstackField: selectedValue?.label, + advanced:{ + validationRegex: selectedValue?.value?.format, + Mandatory: selectedValue?.value?.mandatory, + Multiple: selectedValue?.value?.multiple, + Unique: selectedValue?.value?.unique, + NonLocalizable: selectedValue?.value?.non_localizable + } + }; } return row; }); - setTableData(updatedRows); + + setTableData(updatedRows as FieldMapType[]); }; const SelectAccessorOfColumn = (data: FieldMapType) => { @@ -615,8 +752,7 @@ const ContentMapper = () => { const adjustedOptions = OptionsForRow.map((option: optionsType) => ({ ...option, isDisabled: selectedOptions?.includes(option?.label ?? '') - })); - + })); return (
@@ -634,8 +770,15 @@ const ContentMapper = () => { version="v2" icon="Setting" size="small" - onClick={() => - handleAdvancedSetting(data?.ContentstackFieldType, exstingField, data?.uid) + onClick={() =>{ + const value ={ + ValidationRegex: data?.advanced?.ValidationRegex, + Mandatory: data?.advanced?.mandatory, + Multiple: data?.advanced?.multiple, + Unique: data?.advanced?.unique, + NonLocalizable: data?.advanced?.nonLocalizable + } + handleAdvancedSetting(data?.ContentstackFieldType, advancePropertise, data?.uid, data)} } />
@@ -652,6 +795,7 @@ const ContentMapper = () => { selectedContentType?.otherCmsUid && OtherContentType?.label ) { + setcontentTypeMapped((prevSelected) => ({ ...prevSelected, [otherCmsTitle]: OtherContentType?.label @@ -677,7 +821,7 @@ const ContentMapper = () => { updateAt: new Date(), contentstackTitle: selectedContentType?.contentstackTitle, contentstackUid: selectedContentType?.contnetStackUid, - fieldMapping: tableData + fieldMapping: selectedEntries } }; @@ -797,7 +941,7 @@ const ContentMapper = () => { const adjustedOption = options.map((option: any) => ({ ...option, isDisabled: contentTypeMapped && Object.values(contentTypeMapped).includes(option?.label) - })); + })); return (
@@ -867,10 +1011,11 @@ const ContentMapper = () => { { ), showExportCta: true }} - rowDisableProp={{ key: '_invalid', value: true }} + getSelectedRow={handleSelectedEntries} + rowSelectCheckboxProp={{ key: '_canSelect', value: true }} />
diff --git a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx index b3ffac3b3..661e866c5 100644 --- a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx +++ b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx @@ -3,9 +3,8 @@ import { Icon, Select, cbModal } from '@contentstack/venus-components'; import { AppContext } from '../../../context/app/app.context'; import { DEFAULT_DROPDOWN, IDropDown, INewMigration } from '../../../context/app/app.interface'; -import { isEmptyString, validateArray, validateObject } from '../../../utilities/functions'; +import { isEmptyString, validateArray } from '../../../utilities/functions'; import { createStacksInOrg, getAllStacksInOrg } from '../../../services/api/stacks.service'; -import { getAllLocales } from '../../../services/api/user.service'; import { StackResponse } from '../../../services/api/service.interface'; import AddStack, { Stack } from '../../../components/Common/AddStack/addStack'; import { updateDestinationStack } from '../../../services/api/migration.service'; @@ -39,7 +38,8 @@ const LoadStacks = (props: LoadFileFormatProps) => { label: 'Loading stacks...', value: 'loading', default: false, - locale: '', + master_locale:'', + locales:[], created_at: '' } ]; @@ -75,7 +75,8 @@ const LoadStacks = (props: LoadFileFormatProps) => { const newCreatedStack: IDropDown = { label: resp?.data?.stack?.name, value: resp?.data?.stack?.api_key, - locale: resp?.data?.stack?.master_locale, + master_locale: resp?.data?.stack?.master_locale, + locales: resp?.data?.stack?.locales, created_at: resp?.data?.stack?.created_at, uid: resp?.data?.stack?.api_key }; @@ -141,27 +142,13 @@ const LoadStacks = (props: LoadFileFormatProps) => { newMigrationData?.destination_stack?.selectedOrg?.value ); //org id will always be there - - //fetch all locales - const response = await getAllLocales(selectedOrganisation?.value); //org id will always be there - const rawMappedLocalesMapped = - validateObject(response?.data) && response?.data?.locales - ? Object?.keys(response?.data?.locales)?.map((key) => ({ - uid: key, - label: response?.data?.locales[key], - value: key, - locale: key, - created_at: key - })) - : []; - setAllLocales(rawMappedLocalesMapped); - const stackArray = validateArray(stackData?.data?.stacks) ? stackData?.data?.stacks?.map((stack: StackResponse) => ({ label: stack?.name, value: stack?.api_key, uid: stack?.api_key, - locale: stack?.master_locale, + master_locale: stack?.master_locale, + locales: stack?.locales, created_at: stack?.created_at })) : []; @@ -175,18 +162,18 @@ const LoadStacks = (props: LoadFileFormatProps) => { //Set selected Stack const selectedStackData = validateArray(stackArray) - ? stackArray.find( - (stack: IDropDown) => - stack.value === newMigrationData?.destination_stack?.selectedStack?.value - ) - : DEFAULT_DROPDOWN; - + ? stackArray.find( + (stack: IDropDown) => + stack?.value === newMigrationData?.destination_stack?.selectedStack?.value + ) + : DEFAULT_DROPDOWN; + setSelectedStack(selectedStackData); const newMigrationDataObj: INewMigration = { ...newMigrationData, destination_stack: { - ...newMigrationData.destination_stack, + ...newMigrationData?.destination_stack, selectedStack: selectedStackData } }; @@ -198,7 +185,6 @@ const LoadStacks = (props: LoadFileFormatProps) => { useEffect(() => { fetchData(); }, []); - const handleCreateNewStack = () => { cbModal({ component: (props: LoadFileFormatProps) => ( @@ -209,6 +195,7 @@ const LoadStacks = (props: LoadFileFormatProps) => { }} onSubmit={handleOnSave} defaultValues={defaultStack} + selectedOrganisation= {selectedOrganisation?.value} {...props} /> ), @@ -247,7 +234,7 @@ const LoadStacks = (props: LoadFileFormatProps) => {
- {selectedStack?.locale} + {selectedStack?.master_locale}
diff --git a/ui/src/components/DestinationStack/Summary/StacksSummary.tsx b/ui/src/components/DestinationStack/Summary/StacksSummary.tsx index 2728a0361..ebb20d70d 100644 --- a/ui/src/components/DestinationStack/Summary/StacksSummary.tsx +++ b/ui/src/components/DestinationStack/Summary/StacksSummary.tsx @@ -25,7 +25,8 @@ const StacksSummary = (props: StacksSummaryProps): JSX.Element => { label: 'Loading stacks...', value: 'loading', default: false, - locale: '', + master_locale: '', + locales:[], created_at: '' } ]; @@ -57,7 +58,7 @@ const StacksSummary = (props: StacksSummaryProps): JSX.Element => {
- {selectedStack?.locale} + {selectedStack?.master_locale}
diff --git a/ui/src/components/DestinationStack/index.tsx b/ui/src/components/DestinationStack/index.tsx index 720b86cb7..f08cae353 100644 --- a/ui/src/components/DestinationStack/index.tsx +++ b/ui/src/components/DestinationStack/index.tsx @@ -83,7 +83,8 @@ const DestinationStackComponent = ({ let selectedStackData: IDropDown = { value: destination_stack, label: '', - locale: '', + master_locale: '', + locales:[], created_at: '' }; @@ -103,7 +104,8 @@ const DestinationStackComponent = ({ selectedStackData = { label: stack?.name, value: stack?.api_key, - locale: stack?.master_locale, + master_locale: stack?.master_locale, + locales: stack?.locales, created_at: stack?.created_at }; } diff --git a/ui/src/components/LegacyCms/Summary/summary.scss b/ui/src/components/LegacyCms/Summary/summary.scss index 20a1bbe4c..05e619158 100644 --- a/ui/src/components/LegacyCms/Summary/summary.scss +++ b/ui/src/components/LegacyCms/Summary/summary.scss @@ -26,6 +26,9 @@ .Checkbox-wrapper .Checkbox { display: flex; } +.Checkbox-wrapper .Checkbox .Checkbox__label { + margin-top: -2px; +} .Checkbox input:disabled:checked ~ .Checkbox__label, .Checkbox input:disabled:indeterminate ~ .Checkbox__label { color: #212121; diff --git a/ui/src/components/MainHeader/index.scss b/ui/src/components/MainHeader/index.scss index 36ba78e2e..c4903139d 100644 --- a/ui/src/components/MainHeader/index.scss +++ b/ui/src/components/MainHeader/index.scss @@ -17,6 +17,49 @@ // height: 2rem; margin-right: 1rem; position: relative; - width: 2rem; + width: 3rem; + a { + align-items: center; + display: flex; + height: 2.5rem; + justify-content: center; + padding: .5rem; + text-decoration: none; + } + a:focus { + border-radius: 5px; + box-shadow: 0 0 1px $color-brand-primary-base, 0 0 0 4px #bbb4f4; + } } + .organisationWrapper:focus { + box-shadow: 0 0 1px $color-brand-primary-base, 0 0 0 4px #bbb4f4; + } + .Dropdown__header__value { + overflow: visible; + line-height: 1.2; + } + .Dropdown__header__label { + margin-bottom: 0; + } + .Dropdown.Dropdown--hover .Dropdown__menu--primary { + top: 2.5rem; + } + .organisationWrapper { + .Dropdown__header__value { + color: #475161; + max-width: 11.1875rem; + width: auto; + overflow: hidden; + } + .Dropdown__menu--primary .Dropdown__menu__list__item, .Dropdown__menu--secondary .Dropdown__menu__list__item, .Dropdown__menu--tertiary .Dropdown__menu__list__item { + max-width: 14.5rem; + } + } +} +.Dropdown__header__label { + line-height: $line-height-reset; } +.Dropdown__header__value { + color: $color-black-222!important; + line-height: $line-height-reset; +} \ No newline at end of file diff --git a/ui/src/components/MainHeader/index.tsx b/ui/src/components/MainHeader/index.tsx index cd3aa7dc0..c3e6077aa 100644 --- a/ui/src/components/MainHeader/index.tsx +++ b/ui/src/components/MainHeader/index.tsx @@ -1,7 +1,7 @@ // Libraries import { useContext, useEffect, useState } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { Dropdown } from '@contentstack/venus-components'; +import { useNavigate, useLocation, Link } from 'react-router-dom'; +import { Dropdown, Tooltip } from '@contentstack/venus-components'; // Service import { getCMSDataFromFile } from '../../cmsData/cmsSelector'; @@ -100,21 +100,20 @@ const MainHeader = () => {
{logo?.image?.url ? (
- - Contentstack Logo - + + + Contentstack Logo + +
) : ( '' )} -
-
{organizationLabel}
+
{ return newMigrationData?.destination_stack?.selectedStack?.label; case 'Selected locale': - return newMigrationData?.destination_stack?.selectedStack?.locale; + return newMigrationData?.destination_stack?.selectedStack?.master_locale; } }; diff --git a/ui/src/components/Modal/index.tsx b/ui/src/components/Modal/index.tsx index 7a064c133..eeb3d5870 100644 --- a/ui/src/components/Modal/index.tsx +++ b/ui/src/components/Modal/index.tsx @@ -43,14 +43,15 @@ const Modal = (props: ProjectModalProps) => { const res = await createProject(selectedOrg?.uid || '', values); - return res.error ? false : res; + return res?.error ? false : res; }; const nameValidation = (value: string) => { - if (!value || value === '') { + + if (value === '') { setInputValue(false); return 'Please enter project name.'; - } else if (value && value.length > 200) { + } else if (value && value?.length > 200) { setInputValue(false); return 'Project Name should not be more than 200 chars'; } else { @@ -60,7 +61,7 @@ const Modal = (props: ProjectModalProps) => { }; const descValidation = (value: string) => { - if (value && value.length > 255) { + if (value && value?.length > 255) { setInputValue(false); return 'Description should not be more than 255 chars'; } else { @@ -124,11 +125,11 @@ const Modal = (props: ProjectModalProps) => { placeholder={namePlaceholder} data-testid="title-input" name="name" - error={(meta.error || meta.submitError) && meta.touched} + error={(meta?.error || meta?.submitError) && meta?.touched} /> - {meta.error && meta.touched && ( + {meta?.error && meta?.touched && ( - {meta.error} + {meta?.error} )} @@ -154,15 +155,15 @@ const Modal = (props: ProjectModalProps) => { placeholder={descriptionPlaceholder} version="v2" data-testid="description-input" - error={(meta.error || meta.submitError) && meta.touched} + error={(meta?.error || meta?.submitError) && meta?.touched} /> - {meta.error && meta.touched && ( + {meta?.error && meta?.touched && ( - {meta.error} + {meta?.error} )} diff --git a/ui/src/components/ProjectsHeader/index.tsx b/ui/src/components/ProjectsHeader/index.tsx index 3d0b5df86..868cf0fd8 100644 --- a/ui/src/components/ProjectsHeader/index.tsx +++ b/ui/src/components/ProjectsHeader/index.tsx @@ -10,6 +10,7 @@ const ProjectsHeader = ({ setSearchText, searchPlaceholder, cta, + allProject, handleModal }: ProjectsHeaderType) => { let interval: ReturnType; @@ -21,6 +22,7 @@ const ProjectsHeader = ({ } const SearchProject = ( <> + {allProject && allProject?.length > 0 && (
+ )} ); @@ -57,7 +60,7 @@ const ProjectsHeader = ({ return ( <> - + 0 && pageActions} /> ); }; diff --git a/ui/src/components/ProjectsHeader/projectsHeader.interface.ts b/ui/src/components/ProjectsHeader/projectsHeader.interface.ts index 7555fae49..8341fa11b 100644 --- a/ui/src/components/ProjectsHeader/projectsHeader.interface.ts +++ b/ui/src/components/ProjectsHeader/projectsHeader.interface.ts @@ -1,4 +1,5 @@ import { CTA } from '../../types/common.interface'; +import { ProjectsObj } from '../../pages/Projects/projects.interface' export interface ProjectsHeaderType { cta?: CTA; @@ -7,4 +8,5 @@ export interface ProjectsHeaderType { setSearchText: (value: string) => void; searchPlaceholder: string; handleModal?: () => void; + allProject?: ProjectsObj[] | null; } diff --git a/ui/src/components/SchemaModal/index.scss b/ui/src/components/SchemaModal/index.scss index 2d608fc69..27d53d8e2 100644 --- a/ui/src/components/SchemaModal/index.scss +++ b/ui/src/components/SchemaModal/index.scss @@ -1,16 +1,20 @@ @import '../../scss/variables'; +.schema { + min-height: 290px; +} .entries-outline { + margin-left: 0.8rem; ul { padding: 0.625rem; padding-bottom: 0.3125rem; padding-top: 0; transition: opacity 0.2s linear; li { - color: #475161; + color: $color-font-base; cursor: pointer; - font-size: 1rem; - font-weight: 500; + font-size: $size-font-large; + font-weight: $font-weight-medium; padding: 0; white-space: nowrap; .iconsholder { @@ -27,16 +31,56 @@ path { stroke: $color-brand-primary-base; } + &.chevron { + path { + fill: $color-brand-primary-base; + stroke: $color-brand-primary-base; + } + } } } } + .icons { + * { + pointer-events: none; + stroke: $color-font-active; + } + &.nested { + margin-left: -0.9375rem; + } + } + &.active { + .icons { + svg { + path { + stroke: $color-brand-primary-base; + } + &.chevron { + path { + fill: $color-brand-primary-base; + stroke: $color-brand-primary-base; + } + } + } + } + & > .title { + color: $color-brand-primary-base; + font-weight: $font-weight-semi-bold; + } + } } .title { - color: #475161; - font-weight: 400; + color: $color-font-base; + font-size: $size-font-large; + font-weight: $font-weight-regular; + line-height: $line-height-reset!important; opacity: 1; + overflow: hidden; + padding: 0.625rem 0; + text-overflow: ellipsis; text-transform: capitalize; transition: opacity 0.5s linear, width 300ms linear; + white-space: nowrap; width: 12.5rem; } .icons { @@ -45,9 +89,34 @@ justify-content: flex-start; padding: 0.625rem 0; svg { - margin-right: 0.625rem; + margin-right: 0.375rem; + &.field-icon { + max-width: 12px; + } } } + .chevron { + display: inline-block; + transition: all 0.3s linear; + &.close { + transform: rotate(-90deg); + } + } + & > ul { + padding-left: 0.125rem; + & > li { + & > ul { + padding-left: 1.5625rem; + } + } + } + } + &.close { + height: 0 !important; + opacity: 0 !important; + overflow: hidden; + padding: 0 0.625rem; + transition: opacity 0.8s linear; } } } diff --git a/ui/src/components/SchemaModal/index.tsx b/ui/src/components/SchemaModal/index.tsx index 843358574..bfc28be9b 100644 --- a/ui/src/components/SchemaModal/index.tsx +++ b/ui/src/components/SchemaModal/index.tsx @@ -9,6 +9,7 @@ import { FieldMapType } from '../ContentMapper/contentMapper.interface'; // Styles import './index.scss'; +// Function for get icons const getTopLevelIcons = (field: FieldMapType) => { const icons: Icons = { title: 'StarSmall', @@ -61,7 +62,7 @@ const getTopLevelIcons = (field: FieldMapType) => { return icons['rte']; } - if (field?.ContentstackFieldType === 'JSON Rich Text Editor') { + if (field?.ContentstackFieldType === 'JSON Rich Text Editor' || field?.ContentstackFieldType === 'json') { return icons['jsonRte']; } @@ -77,7 +78,7 @@ const getTopLevelIcons = (field: FieldMapType) => { return icons['reference']; } - if (!field.ContentstackFieldType) { + if (!field?.ContentstackFieldType) { return icons['blocks']; } @@ -85,31 +86,156 @@ const getTopLevelIcons = (field: FieldMapType) => { }; const TreeView = ({ schema = [] }: schemaType) => { - const [list, setList] = useState([]); + const [nestedList, setNestedList] = useState([]); useEffect(() => { - setList(schema); + let groupId = '' + const data: FieldMapType[] = [] + schema?.forEach((field) => { + if (field?.ContentstackFieldType === "group") { + groupId = field?.uid + data?.push({...field, child: []}) + } + else{ + if(field?.uid?.startsWith(groupId+'.')){ + const obj = data[data?.length - 1] + if(Object.prototype.hasOwnProperty.call(obj, 'child')){ + obj?.child?.push(field) + } + else{ + obj.child = [field] + } + } + else{ + data.push({...field, child: []}) + } + } + }); + setNestedList(data); }, [schema]); + // Check if schema is nested + const hasNestedValue = (field: FieldMapType) => field && field?.child && field?.child?.length > 0; + + // Remove Group name from its child + const getChildFieldName = (text?: string, groupName?: string) => { + if (text?.startsWith(groupName+' > ')) { + return text?.replace(groupName+' > ', '') + } + }; + + const handleClick = (event: React.MouseEvent) => { + if (document?.querySelector('.iconsholder.active')) { + document?.querySelector('.iconsholder.active') + ?.classList.remove('active'); + } + if (event?.target instanceof HTMLElement) { + if (event?.target?.classList.contains('icons')) { + if (event?.target?.parentElement?.parentElement?.querySelector('ul')) { + event?.target?.parentElement?.parentElement + ?.querySelector('ul') + ?.classList.toggle('close'); + } + + if (event?.target?.querySelector('.chevron')) { + event?.target?.querySelector('.chevron')?.classList.toggle('close'); + } + } + + event?.target?.parentElement?.classList.add('active'); + } + }; + + const generateNestedOutline = (item: FieldMapType, index: number) => { + return ( +
    0 ? '' : 'close'} + > + {item?.child?.map((field: FieldMapType, nestedIndex: number) => { + let fieldname = ''; + if (field?.uid) { + fieldname = field?.uid?.replace(/\.+/g, '_'); + } + return ( +
  • +
    ) => { + e.preventDefault(); + e.stopPropagation(); + handleClick(e); + }} + className={`iconsholder`} + > + + {hasNestedValue(field) && ( + + )} + + + + {getChildFieldName(field?.otherCmsField, item?.otherCmsField)} + +
    + + {hasNestedValue(field) && + generateNestedOutline(field, nestedIndex)} +
  • + ) + })} +
+ ); + }; + return (
-
-
- {list?.length > 0 && ( -
    - {list?.map((item: FieldMapType) => ( -
  • -
    - - - - {item?.otherCmsField} -
    -
  • - ))} -
- )} -
+
+ {nestedList?.length > 0 && ( +
    + {nestedList?.map((item: FieldMapType, index: number) => { + let outlineName = ""; + if (item?.uid) { + outlineName = item?.uid?.replace(/\.+/g, "_"); + } + const hasNested = hasNestedValue(item); + + return ( +
  • +
    ) => { + e.preventDefault(); + e.stopPropagation(); + handleClick(e); + }} + > + { + document + ?.querySelector('.PageLayout__leftSidebar') + ?.classList.add('hovered'); + }} + > + {hasNested && ( + + )} + + + {item?.otherCmsField} +
    + {hasNested && generateNestedOutline(item, index)} +
  • + )})} +
+ )}
); diff --git a/ui/src/context/app/app.interface.ts b/ui/src/context/app/app.interface.ts index 919d5ef7e..d17692ba0 100644 --- a/ui/src/context/app/app.interface.ts +++ b/ui/src/context/app/app.interface.ts @@ -136,7 +136,10 @@ interface FieldTypes { label: string; value: string; } - +interface locales { + code:string; + name:string +} export interface ILegacyCms { selectedCms: ICMSType; selectedFileFormat: ICardType; @@ -177,7 +180,8 @@ export interface IDropDown { label: string; value: string; default?: boolean; - locale: string; + master_locale: string; + locales:locales[]; created_at: string; } export interface ITestMigration { @@ -205,7 +209,8 @@ export const DEFAULT_DROPDOWN: IDropDown = { value: '', default: false, uid: '', - locale: '', + master_locale: '', + locales:[], created_at: '' }; diff --git a/ui/src/pages/Login/index.tsx b/ui/src/pages/Login/index.tsx index 9588c5b6e..0e0ca279b 100644 --- a/ui/src/pages/Login/index.tsx +++ b/ui/src/pages/Login/index.tsx @@ -202,6 +202,8 @@ const Login: FC = () => { className="mb-2" version="v2" htmlFor="tfa_token" + aria-label="tfa_token" + > {twoFactorAuthentication?.security_code?.title} @@ -213,6 +215,7 @@ const Login: FC = () => { error={meta?.error && meta?.touched} width="large" testId="cs-tfa-token-input-field" + id="tfa_token" /> {meta?.error && meta?.touched && ( = () => { className="mb-2" required={true} version="v2" - htmlFor="email" + htmlFor={login?.email} > {login?.email} @@ -302,7 +305,7 @@ const Login: FC = () => { } })); }} - name="email" + name={login?.email} width="large" id="email" version="v2" @@ -310,6 +313,7 @@ const Login: FC = () => { data-testid="cs-login-email-input-field" placeholder={login?.placeholder?.email} error={(meta?.error || meta?.submitError) && meta?.touched} + aria-label={login?.email} /> {meta.error && meta.touched && ( = () => { buttonType="primary" type="submit" icon="v2-Login" - tabIndex={0} + tabindex={0} > {login?.cta?.title} diff --git a/ui/src/pages/Projects/index.tsx b/ui/src/pages/Projects/index.tsx index 213c3ed5d..af100a145 100644 --- a/ui/src/pages/Projects/index.tsx +++ b/ui/src/pages/Projects/index.tsx @@ -147,6 +147,7 @@ const Projects = () => { setSearchText={setSearchText} cta={cta} handleModal={openModal} + allProject={projects} /> ) diff --git a/ui/src/pages/RegionalLogin/index.tsx b/ui/src/pages/RegionalLogin/index.tsx index 25faebafe..3bf266e6f 100644 --- a/ui/src/pages/RegionalLogin/index.tsx +++ b/ui/src/pages/RegionalLogin/index.tsx @@ -102,7 +102,6 @@ const RegionalLogin = () => { region?.service_title as string ) } - aria-label={`${region?.cta?.title} with ${region?.service_title} ${region?.region_title}`} > {region?.cta?.title} diff --git a/ui/src/scss/App.scss b/ui/src/scss/App.scss index e7ffb519d..1e24a7b4b 100644 --- a/ui/src/scss/App.scss +++ b/ui/src/scss/App.scss @@ -339,3 +339,11 @@ h2 { } } } +.ReactModal__Overlay .ReactModal__Content .ReactModal__Content__header h3 { + color: $color-black-2121; + font-weight: bold; +} +.ReactModal__Content__footer .Button{ + font-size: 1rem; + line-height: 100%; +} \ No newline at end of file diff --git a/ui/src/scss/_variables.scss b/ui/src/scss/_variables.scss index 673ed1479..1d8442b82 100644 --- a/ui/src/scss/_variables.scss +++ b/ui/src/scss/_variables.scss @@ -27,6 +27,8 @@ $color-brand-warning-medium: #eb5646; $color-font-disabled: rgba(160, 174, 192, 0.5); $color-font-gray: #637392; $color-black-2121: #212121; +$color-black-222: #222222; + $font-family-primary: 'Inter', sans-serif; $font-family-secondary: 'Montserrat', sans-serif; @@ -43,8 +45,6 @@ $size-font-4-xl: 1.875rem; /* 30px[H3] */ $size-font-5-xl: 1.75rem; /* 28px[H2] */ $size-font-6-xl: 2.5rem; /* 40px[H1] */ -$font-base: 16; - $font-weight-regular: 400; $font-weight-medium: 500; $font-weight-semi-bold: 600; diff --git a/ui/src/services/api/service.interface.ts b/ui/src/services/api/service.interface.ts index bb72271c1..3a3fa636f 100644 --- a/ui/src/services/api/service.interface.ts +++ b/ui/src/services/api/service.interface.ts @@ -12,6 +12,7 @@ export interface StackResponse { name: string; api_key: string; master_locale: string; + locales:[]; created_at: string; }