diff --git a/.github/workflows/run-cypress-tests-develop.yml b/.github/workflows/run-cypress-tests-develop.yml new file mode 100644 index 00000000..4fc76f97 --- /dev/null +++ b/.github/workflows/run-cypress-tests-develop.yml @@ -0,0 +1,20 @@ +name: Trigger Cypress Tests on Develop Branch + +on: + pull_request: + branches: + - develop + workflow_dispatch: + +jobs: + trigger-end-to-end-tests: + uses: ./.github/workflows/run-cypress-tests.yml + with: + environment: 'staging' + grep: '' + grepTags: '@general' + secrets: + DOCKER_PAT: ${{ secrets.DOCKER_PAT }} + CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} + S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/run-cypress-tests-main.yml b/.github/workflows/run-cypress-tests-main.yml new file mode 100644 index 00000000..0813cd56 --- /dev/null +++ b/.github/workflows/run-cypress-tests-main.yml @@ -0,0 +1,20 @@ +name: Trigger Cypress Tests on Main Branch + +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + trigger-end-to-end-tests: + uses: ./.github/workflows/run-cypress-tests.yml + with: + environment: 'production' + grep: '' + grepTags: '@essential' + secrets: + DOCKER_PAT: ${{ secrets.DOCKER_PAT }} + CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} + S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/run-cypress-tests.yml b/.github/workflows/run-cypress-tests.yml index f1f8f46c..03e7e5f7 100644 --- a/.github/workflows/run-cypress-tests.yml +++ b/.github/workflows/run-cypress-tests.yml @@ -3,10 +3,10 @@ name: Trigger End-to-end Tests Workflow on: workflow_dispatch: inputs: - baseUrl: - description: 'Base URL to run tests against' + environment: + description: 'Environment to run tests against (production/staging)' + default: staging required: true - default: 'https://staging.cms.footlight.io/' grep: description: 'Grep pattern for selecting tests' required: false @@ -15,26 +15,208 @@ on: description: 'Grep tags for selecting tests' required: false default: '@essential' - numContainers: - description: 'Number of containers to use for browsers' + + workflow_call: + inputs: + environment: + description: 'Environment to run tests against' + required: true + type: string + grep: + description: 'Grep pattern for selecting tests' + required: false + type: string + grepTags: + description: 'Grep tags for selecting tests' required: false - default: '2' + type: string + default: '@essential' + + secrets: + DOCKER_PAT: + required: true + CYPRESS_PASSWORD: + required: true + S3_ACCESS_KEY_ID: + required: true + S3_SECRET_ACCESS_KEY: + required: true + jobs: - trigger-tests: + built-and-run-cypress: runs-on: ubuntu-latest steps: - - name: Trigger End-to-end Tests Workflow - run: | - curl -X POST https://api.github.com/repos/kmdvs/cypress-gha-tests/actions/workflows/e2eTests.yml/dispatches \ - -H "Authorization: token ${{secrets.DOCKER_PAT}}" \ - -H "Accept: application/vnd.github.v3+json" \ - -d '{ - "ref": "main", - "inputs": { - "baseUrl": "${{ github.event.inputs.baseUrl }}", - "grep": "${{ github.event.inputs.grep }}", - "grepTags": "${{ github.event.inputs.grepTags }}", - "numContainers": "${{ github.event.inputs.numContainers }}" - } - }' + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Docker network + run: docker network create footlight-network.test + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} + aws-region: ca-central-1 + + - name: Run mongodb with latest dump + env: + BUCKET_NAME: footlight-dump + run: | + docker run -d --name test.mongo --network footlight-network.test -p 27017:27017 mongo:latest + + latest_file=$(aws s3 ls s3://$BUCKET_NAME/ --recursive | sort | tail -n 1 | awk '{print $4}') + aws s3 cp s3://$BUCKET_NAME/$latest_file ./latest_file.zip + unzip latest_file.zip -d ./latest_file + + docker cp ./latest_file test.mongo:/dump + docker exec test.mongo mongorestore --db footlight-calendar /dump/$latest_file/footlight-calendar + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ vars.USERNAME_DEV }} + password: ${{ secrets.DOCKER_PAT }} + + - name: Create env file + run: | + echo "APP_PORT=8080" >> .env + echo "DATABASE_URL=mongodb://test.mongo:27017/footlight-calendar" >> .env + echo "AWS_S3_ACCESS_KEY_ID=${{ secrets.S3_ACCESS_KEY_ID }}" >> .env + echo "AWS_S3_SECRET_ACCESS_KEY=${{ secrets.S3_SECRET_ACCESS_KEY}}" >> .env + echo "DEFAULT_TIMEZONE=Canada/Eastern" >> .env + echo "AWS_S3_BUCKET=${{vars.AWS_S3_BUCKET}}" >> .env + echo "AWS_S3_REGION=${{vars.AWS_S3_REGION}}" >> .env + + - name: Pull and Run CMS Backend Docker Image + run: | + if [ "${{ inputs.environment }}" == "production" ]; then + IMAGE="ghcr.io/culturecreates/footlight-calendar-api/footlight-admin-api:master" + elif [ "${{ inputs.environment }}" == "staging" ]; then + IMAGE="ghcr.io/culturecreates/footlight-calendar-api/footlight-admin-api:develop" + fi + + docker pull $IMAGE + + docker run -d \ + --restart always \ + --name test.footlight.api \ + --network footlight-network.test \ + -p 8080:8080 \ + $IMAGE + + docker cp ./.env test.footlight.api:/usr/src/app + + - name: Build and run Footlight Container + run: | + sed -i 's|^REACT_APP_API_URL=.*|REACT_APP_API_URL="http://test.footlight.api:8080"|' .env.staging + docker build -t footlight . + docker run -d --name test.footlight.app --network footlight-network.test -p 3000:3000 footlight + + - name: Wait for Footlight to be ready + run: | + for i in {1..5}; do + if curl -s http://localhost:3000; then + echo "Footlight is up and running!" + exit 0 + fi + echo "Waiting for Footlight to be ready..." + sleep 10 + done + echo "Footlight did not start in time!" + exit 1 + + - name: Pull cypress docker image + run: docker pull ghcr.io/kmdvs/cms-cypress_regression_tests:main + + - name: Write grep and grepTags to cypress.env.json + run: | + mkdir -p cypress + cd cypress + mkdir -p logs screenshots videos + + echo '{ + "grep": "${{ inputs.grep }}", + "grepTags": "${{ inputs.grepTags }}" + }' > cypress.env.json + + - name: Verify contents of cypress.env.json + run: | + cat cypress/cypress.env.json + + - name: Run Cypress tests + run: | + + base_url="http://test.footlight.app:3000/" + + # grep_value="${{ inputs.grep }}" + # echo "Original grep_value: '$grep_value'" + # grep_value_clean=$(echo "$grep_value" | tr -d '\n\r') + # echo "Cleaned grep_value: '$grep_value_clean'" + + # if [ -z "$grep_value_clean" ]; then + # grep_value_json='""' + # else + # grep_value_json=$(printf '%s' "$grep_value_clean" | sed 's/"/\\"/g; s/.*/"&"/') + # fi + + # echo "JSON grep_value: '$grep_value_json'" + + # # Properly format the --env argument for Cypress + # env_json="{\"grepTags\":\"${{ inputs.grepTags }}\",\"grep\":${grep_value_json}}" + # echo "Formatted env JSON: $env_json" + + # # Simulate the Cypress --env argument + # env_arg=$(printf '%s' "$env_json") + # echo "Simulated --env argument for Cypress: $env_arg" + + # # Validate JSON formatting + # echo "$env_json" | jq . # Validate JSON formatting + + + # Run Cypress tests with the formatted --env argument + docker run \ + --network footlight-network.test \ + -e DEBUG="" \ + -e XDG_RUNTIME_DIR=/tmp/runtime \ + -e CYPRESS_BASE_URL=$base_url \ + -e CYPRESS_ADMIN_EN_EMAIL="${{ vars.CYPRESS_ADMIN_EN_EMAIL }}" \ + -e CYPRESS_ADMIN_FR_EMAIL="${{ vars.CYPRESS_ADMIN_FR_EMAIL }}" \ + -e CYPRESS_GUEST_EN_EMAIL="${{ vars.CYPRESS_GUEST_EN_EMAIL }}" \ + -e CYPRESS_GUEST_FR_EMAIL="${{ vars.CYPRESS_GUEST_FR_EMAIL }}" \ + -e CYPRESS_ADMIN_EN_PASSWORD=${{ secrets.CYPRESS_PASSWORD }} \ + -e CYPRESS_ADMIN_FR_PASSWORD=${{ secrets.CYPRESS_PASSWORD }} \ + -e CYPRESS_GUEST_EN_PASSWORD=${{ secrets.CYPRESS_PASSWORD }} \ + -e CYPRESS_GUEST_FR_PASSWORD=${{ secrets.CYPRESS_PASSWORD }} \ + -e HEADLESS="true" \ + -v ${GITHUB_WORKSPACE}/cypress/screenshots:/e2e/cypress/screenshots \ + -v ${GITHUB_WORKSPACE}/cypress/videos:/e2e/cypress/videos \ + -v ${GITHUB_WORKSPACE}/cypress/logs:/e2e/cypress/logs \ + -v ${GITHUB_WORKSPACE}/cypress/cypress.env.json:/e2e/cypress/cypress.env.json \ + ghcr.io/kmdvs/cms-cypress_regression_tests:main \ + npx cypress run --browser firefox > ${GITHUB_WORKSPACE}/cypress/logs/debug-firefox.log 2>&1 + + - name: Upload Cypress Debug Logs + uses: actions/upload-artifact@v3 + if: always() + with: + name: cypress-debug-logs + path: cypress/logs/debug-firefox.log + + - name: Upload Cypress Screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: cypress-screenshots-firefox + path: cypress/screenshots + if-no-files-found: ignore + + - name: Upload Cypress Videos + uses: actions/upload-artifact@v4 + if: always() + with: + name: cypress-videos-firefox + path: cypress/videos + if-no-files-found: ignore diff --git a/Dockerfile b/Dockerfile index 7b6e093f..53580e6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ COPY . . EXPOSE 3000 -ENTRYPOINT ["sh","./footlight-app-start.sh"] +ENTRYPOINT ["npm","run", "start:staging"] diff --git a/src/components/Card/MandatoryField/MandatoryField.jsx b/src/components/Card/MandatoryField/MandatoryField.jsx index 9091ed1a..ed7ed0d0 100644 --- a/src/components/Card/MandatoryField/MandatoryField.jsx +++ b/src/components/Card/MandatoryField/MandatoryField.jsx @@ -66,6 +66,12 @@ function MandatoryField(props) { setAvailableFields(field?.filter((f) => !f?.isRequiredField && !f?.preFilled && !f?.isAdminOnlyField)); }, [tabKey, field]); + const createLabel = (category, fieldName) => { + if (Array.isArray(category)) return `${category.slice(1).reverse().join(' - ')} - ${fieldName}`; + + return category === 'Contact' ? `${category} - ${fieldName}` : fieldName; + }; + return ( @@ -79,10 +85,13 @@ function MandatoryField(props) { removeFromFields(index)} icon={} @@ -101,10 +110,13 @@ function MandatoryField(props) { addToFields(field)} icon={} /> diff --git a/src/components/DraggableTree/DraggableRow.jsx b/src/components/DraggableTree/DraggableRow.jsx new file mode 100644 index 00000000..f4935262 --- /dev/null +++ b/src/components/DraggableTree/DraggableRow.jsx @@ -0,0 +1,66 @@ +import { useRef, useState } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; + +/** + * @param {Object} props - Props for the DraggableBodyRow component. + * @param {string|number} props['data-row-key'] - Unique key for the row. + * @param {function} props.moveRow - Function to handle row movement. + * @param {string} [props.className] - Additional class names for styling. + * @param {number} props.numberOfParents - Number of parent elements to determine nesting level. + * @param {Object} [props.style] - Inline styles for the row. + * @param {Object} [props.restProps] - Additional props passed to the row. + */ + +const type = 'DraggableBodyRow'; + +const DraggableBodyRow = ({ 'data-row-key': dataRowKey, moveRow, className, numberOfParents, style, ...restProps }) => { + const ref = useRef(null); + const [isDroppingToGap, setIsDroppingToGap] = useState(false); + const [{ isOver, dropClassName }, drop] = useDrop({ + accept: type, + hover: (_, monitor) => { + setIsDroppingToGap(monitor.getDifferenceFromInitialOffset()?.x > 40); + }, + collect: (monitor) => { + const { dataRowKey: dragIndex } = monitor.getItem() || {}; + + if (dragIndex === dataRowKey) { + return {}; + } + return { + isOver: monitor.isOver(), + dropClassName: `${isDroppingToGap ? ' drop-over-upward-in-gap' : ' drop-over-upward'}`, + }; + }, + drop: (item, monitor) => { + const dropToGap = monitor.getDifferenceFromInitialOffset()?.x > 40; + moveRow(item.dataRowKey, dataRowKey, dropToGap); + }, + }); + + const [, drag] = useDrag({ + type, + item: { + dataRowKey, + }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + drop(drag(ref)); + + return ( + + ); +}; + +export default DraggableBodyRow; diff --git a/src/components/DraggableTree/DraggableTable.jsx b/src/components/DraggableTree/DraggableTable.jsx new file mode 100644 index 00000000..6239551d --- /dev/null +++ b/src/components/DraggableTree/DraggableTable.jsx @@ -0,0 +1,256 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { Table } from 'antd'; +import { useOutletContext } from 'react-router-dom'; +import { capitalizeFirstLetter } from '../../utils/stringManipulations'; +import { contentLanguageKeyMap } from '../../constants/contentLanguage'; +import './draggableTable.css'; +import { PlusOutlined, MinusOutlined } from '@ant-design/icons'; +import Outlined from '../Button/Outlined'; +import { useTranslation } from 'react-i18next'; +import { languageFallbackStatusCreator } from '../../utils/languageFallbackStatusCreator'; +import { Dropdown, Menu } from 'antd'; +import { MoreOutlined } from '@ant-design/icons'; +import { Confirm } from '../Modal/Confirm/Confirm'; +import { + cloneFallbackStatus, + deepCopy, + moveConcept, + sanitizeData, + transformLanguageKeys, + updateNodeData, +} from '../../utils/draggableTableUtilFunctions'; +import EditableCell from './EditableCell'; +import DraggableBodyRow from './DraggableRow'; + +const DraggableTable = ({ data, setData, fallbackStatus, setFallbackStatus, transformedData, setTransformedData }) => { + const [currentCalendarData] = useOutletContext(); + const calendarContentLanguage = currentCalendarData?.contentLanguage; + const { t } = useTranslation(); + + const [transformationComplete, setTransformationComplete] = useState(false); + + const handleSave = (row, data = transformedData, columnKey) => { + const fallbackStatusCloned = cloneFallbackStatus(fallbackStatus, row, columnKey); + const updatedData = updateNodeData(data, row); + const sanitizedData = sanitizeData(updatedData, fallbackStatusCloned); + const transformedData = transformLanguageKeys(sanitizedData); + + setData(transformedData); + }; + + const handleDelete = (key) => { + Confirm({ + title: t('dashboard.taxonomy.addNew.concepts.deleteConceptHeading'), + onAction: () => { + const updatedData = deleteNodeFromData(transformedData, key); + const sanitizedData = sanitizeData(updatedData, fallbackStatus); + const filteredConceptData = transformLanguageKeys(sanitizedData); + setData(filteredConceptData); + }, + content: t('dashboard.taxonomy.addNew.concepts.deleteConceptMessage'), + okText: t('dashboard.settings.addUser.delete'), + cancelText: t('dashboard.events.deleteEvent.cancel'), + }); + }; + + const deleteNodeFromData = (data, key) => { + const deleteData = (items) => { + for (let i = 0; i < items.length; i++) { + if (items[i].key === key) { + items.splice(i, 1); + return; + } + if (items[i].children) { + deleteData(items[i].children); + } + } + }; + + // Create a deep copy of the data to avoid mutating the original array + const newData = deepCopy(data); + deleteData(newData); + return newData; + }; + + const columns = calendarContentLanguage.map((language) => ({ + title: capitalizeFirstLetter(language), + dataIndex: contentLanguageKeyMap[language], + key: contentLanguageKeyMap[language], + editable: true, + })); + + const moveRow = useCallback( + (dragIndex, hoverIndex, dropToGap) => { + setData(moveConcept(dragIndex, hoverIndex, data, dropToGap)); + }, + [data], + ); + + const transformData = (data, parentCount = 0) => { + if (!data) return data; + const { name, children, ...rest } = data; + const languageFallbacks = languageFallbackStatusCreator({ + calendarContentLanguage, + languageFallbacks: currentCalendarData?.languageFallbacks, + fieldData: name, + isFieldsDirty: {}, + }); + const fallbackKeys = Object.keys(languageFallbacks); + let extractedFallbackValues = {}; + fallbackKeys.forEach((lanKey) => { + if (languageFallbacks[lanKey]?.tagDisplayStatus) + extractedFallbackValues[lanKey] = languageFallbacks[lanKey].fallbackLiteralValue; + }); + + const transformed = { + ...rest, + ...name, + ...extractedFallbackValues, + numberOfParents: parentCount, + }; + + setFallbackStatus((prev) => ({ ...prev, [transformed.key]: languageFallbacks })); + + if (children && Array.isArray(children)) { + transformed.children = children.map((child) => transformData(child, parentCount + 1)); + } + + return transformed; + }; + + useEffect(() => { + if (!calendarContentLanguage || !data) return; + setTransformedData(data.map((item) => transformData(item, 0))); + setTransformationComplete(true); + }, [data, calendarContentLanguage]); + + const handleAdd = () => { + const newKey = `new_${Date.now()}`; + const newRow = { + key: newKey, + isNew: true, + ...columns.reduce((acc, col) => { + acc[col.dataIndex] = `Concept ${col.title}`; + return acc; + }, {}), + }; + + const newConceptData = [...transformedData, newRow]; + setTransformedData(newConceptData); + const sanitizedData = sanitizeData(newConceptData, fallbackStatus); + const filteredConceptData = transformLanguageKeys(sanitizedData); + setData(filteredConceptData); + }; + + const components = { + body: { + row: DraggableBodyRow, + cell: EditableCell, + }, + }; + + const menu = (record) => ( + + handleDelete(record?.key)}> + {t('dashboard.taxonomy.addNew.concepts.delete')} + + + ); + + const modifiedColumns = [ + ...columns.map((col) => ({ + ...col, + ellipsis: true, + onCell: (record) => { + return { + record, + editable: col.editable, + dataIndex: col.dataIndex, + title: col.title, + handleSave: (row, data) => handleSave(row, data, col.dataIndex), + fallbackStatus: fallbackStatus[record.key], + }; + }, + })), + { + title: '', + dataIndex: 'actions', + width: 30, + key: 'actions', + render: (_, record) => ( +
+ + + +
+ ), + }, + ]; + + return ( + transformationComplete && ( +
+ + + + + { + if (!record.children || record.children.length === 0) return null; + return expanded ? ( +
{ + e.stopPropagation(); + return onExpand(record, e); + }}> + +
+ ) : ( +
{ + e.stopPropagation(); + return onExpand(record, e); + }}> + +
+ ); + }, + }} + rowClassName="editable-row" + onRow={(record, index) => { + const attr = { + index, + moveRow, + fallbackStatus, + numberOfParents: record.numberOfParents, + }; + return attr; + }} + /> + + + ) + ); +}; + +export default DraggableTable; diff --git a/src/components/DraggableTree/DraggableTree.jsx b/src/components/DraggableTree/DraggableTree.jsx deleted file mode 100644 index 8e8f26c7..00000000 --- a/src/components/DraggableTree/DraggableTree.jsx +++ /dev/null @@ -1,466 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Form, Tree, Input } from 'antd'; -import { useTranslation } from 'react-i18next'; -import CustomModal from '../Modal/Common/CustomModal'; -import PrimaryButton from '../Button/Primary'; -import { EditOutlined } from '@ant-design/icons'; -import TextButton from '../Button/Text'; -import { useOutletContext } from 'react-router-dom'; -import { contentLanguage, contentLanguageKeyMap } from '../../constants/contentLanguage'; -import Outlined from '../Button/Outlined'; -import './draggableTree.css'; -import { Confirm } from '../Modal/Confirm/Confirm'; -import FormItem from 'antd/es/form/FormItem'; -import { capitalizeFirstLetter } from '../../utils/stringManipulations'; -import { contentLanguageBilingual } from '../../utils/bilingual'; -import { useSelector } from 'react-redux'; -import { getUserDetails } from '../../redux/reducer/userSlice'; -import CreateMultiLingualFormItems from '../../layout/CreateMultiLingualFormItems/CreateMultiLingualFormItems'; -import { placeHolderCollectionCreator } from '../../utils/MultiLingualFormItemSupportFunctions'; - -const DraggableTree = ({ - data, - setData, - addNewPopup, - setAddNewPopup, - deleteDisplayFlag, - setDeleteDisplayFlag, - setEmptyConceptName, - form, -}) => { - const { TextArea } = Input; - - const [currentCalendarData] = useOutletContext(); - const calendarContentLanguage = currentCalendarData?.contentLanguage; - - const { user } = useSelector(getUserDetails); - const { t } = useTranslation(); - const [treeDataCollection, setTreeDataCollection] = useState({}); - const [forEditing, setForEditing] = useState(); - const [selectedNode, setSetSelectedNode] = useState(); - const [expandedKeys, setExpandedKeys] = useState(); - const [newConceptName, setNewConceptName] = useState(); - - const generateFormattedData = (data, language) => { - const treeData = data.map((item) => { - let conceptNameCollection = {}; - calendarContentLanguage.forEach((lang) => { - const conceptNameInCurrentLanguage = item?.name[contentLanguageKeyMap[lang]]; - if (conceptNameInCurrentLanguage) { - conceptNameCollection[contentLanguageKeyMap[lang]] = conceptNameInCurrentLanguage; - } - }); - const requiredLanguageKey = contentLanguageKeyMap[language]; - const card = { - key: item.key, - name: contentLanguageBilingual({ - requiredLanguageKey, - data: item?.name, - interfaceLanguage: user.interfaceLanguage, - calendarContentLanguage, - }), - title: ( -
- - {contentLanguageBilingual({ - requiredLanguageKey, - data: item?.name, - interfaceLanguage: user.interfaceLanguage, - calendarContentLanguage, - })} - - { - e.stopPropagation(); - setNewConceptName(conceptNameCollection); - setSetSelectedNode(item); - editConceptHandler(item); - }}> - - -
- ), - children: item.children ? generateFormattedData(item.children, language) : undefined, - }; - return card; - }); - return treeData; - }; - - const combineBothTreeData = (dataSets) => { - const combinedData = []; - const dataSetKeyCollection = Object.keys(dataSets); - const firstTree = dataSets[dataSetKeyCollection[0]]; - - for (let index = 0; index < dataSets[dataSetKeyCollection[0]]?.length; index++) { - let combinedNames = {}; - let combinedElement = { - id: firstTree[index]?.key, - key: firstTree[index]?.key, - name: {}, - children: [], - }; - - dataSetKeyCollection.forEach((conceptLanguageKey) => { - combinedNames[contentLanguageKeyMap[conceptLanguageKey]] = dataSets[conceptLanguageKey]?.[index]?.name; - }); - - combinedElement = { ...combinedElement, name: combinedNames }; - - if (firstTree[index]?.children?.length > 0) { - let childDataSets = {}; - dataSetKeyCollection.forEach((conceptLanguageKey) => { - childDataSets[conceptLanguageKey] = dataSets[conceptLanguageKey]?.[index]?.children; - }); - combinedElement.children = combineBothTreeData(childDataSets); - } - - const savedElement = findItem(combinedElement.key); - if (savedElement?.isNew) { - combinedElement.isNew = savedElement.isNew; - } - combinedData.push(combinedElement); - } - return combinedData; - }; - - const onDrop = ({ info }) => { - const dropKey = info.node.key; - const dragKey = info.dragNode.key; - const dropPos = info.node.pos.split('-'); - const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); - let modifiedDataCollection = {}; - - const loop = (data, key, callback) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - loop(data[i].children, key, callback); - } - } - }; - - calendarContentLanguage.forEach((language) => { - let dragObj; - loop(treeDataCollection[language], dragKey, (item, index, arr) => { - arr.splice(index, 1); - dragObj = item; - }); - - if (!info.dropToGap) { - loop(treeDataCollection[language], dropKey, (item) => { - item.children = item.children || []; - item.children.unshift(dragObj); - }); - } else if ((info.node.children || []).length > 0 && info.node.expanded && dropPosition === 1) { - loop(treeDataCollection[language], dropKey, (item) => { - item.children = item.children || []; - item.children.unshift(dragObj); - }); - } else { - let ar = []; - let i; - loop(treeDataCollection[language], dropKey, (_item, index, arr) => { - ar = arr; - i = index; - }); - if (dropPosition === -1) { - ar.splice(i, 0, dragObj); - } else { - ar.splice(i + 1, 0, dragObj); - } - } - modifiedDataCollection[language] = [...treeDataCollection[language]]; - }); - - setData(combineBothTreeData(modifiedDataCollection)); - }; - - const findItem = (key) => { - const helper = (items) => { - for (let i = 0; i < items.length; i++) { - if (items[i].key === key) { - return items[i]; - } - if (items[i].children) { - const foundItem = helper(items[i].children); - if (foundItem) { - return foundItem; - } - } - } - return null; - }; - - return helper(data); - }; - - const editConceptHandler = (node) => { - if (node) { - let conceptNameCollection = {}; - calendarContentLanguage.forEach((language) => { - conceptNameCollection[contentLanguageKeyMap[language]] = node?.name[contentLanguageKeyMap[language]]; - }); - form.setFieldValue('conceptName', conceptNameCollection); - setAddNewPopup(true); - setDeleteDisplayFlag(true); - setForEditing(true); - } - }; - - const handleAddChildModalClose = () => { - setEmptyConceptName(); - let conceptNameCollection = {}; - calendarContentLanguage.forEach((language) => { - conceptNameCollection[contentLanguageKeyMap[language]] = ''; - }); - form.setFieldValue('conceptName', conceptNameCollection); - setSetSelectedNode(); - setAddNewPopup(false); - }; - - const handleAddChild = () => { - const conceptNameCollection = form.getFieldValue('conceptName') || {}; - - if (forEditing) { - const updatedNode = { - ...selectedNode, - name: conceptNameCollection, - }; - const updatedData = updateNodeInData(data, selectedNode?.key, updatedNode); - setData(updatedData); - setForEditing(false); - } else { - const newChildNode = { - key: Date.now().toString(), - id: Date.now().toString(), - name: conceptNameCollection, - children: [], - isNew: true, - }; - - if (selectedNode) { - const updatedData = updateNodeInData(data, selectedNode.key, { - ...selectedNode, - children: [...(selectedNode.children || []), newChildNode], - }); - setData(updatedData); - } else { - const updatedData = [...data, newChildNode]; - setData(updatedData); - } - } - setEmptyConceptName(); - handleAddChildModalClose(); - setSetSelectedNode(); - }; - - const updateNodeInData = (data, key, updatedNode) => { - const updateData = (items) => { - return items.map((item) => { - if (item.key === key) { - return updatedNode; - } - if (item.children) { - return { - ...item, - children: updateData(item.children), - }; - } - return item; - }); - }; - - const newData = updateData([...data]); - return newData; - }; - - const handleDelete = () => { - setAddNewPopup(false); - Confirm({ - title: t('dashboard.taxonomy.addNew.concepts.deleteConceptHeading'), - onAction: () => { - if (forEditing && selectedNode) { - const updatedData = deleteNodeFromData(data, selectedNode.key); - setData(updatedData); - setForEditing(false); - setEmptyConceptName(); - handleAddChildModalClose(); - } else { - setDeleteDisplayFlag(false); - setData(data); - } - }, - content: t('dashboard.taxonomy.addNew.concepts.deleteConceptMessage'), - okText: t('dashboard.settings.addUser.delete'), - cancelText: t('dashboard.events.deleteEvent.cancel'), - }); - }; - - const deepCopy = (obj) => { - if (obj === null || typeof obj !== 'object') { - return obj; - } - if (Array.isArray(obj)) { - return obj.map(deepCopy); - } - const copiedObj = {}; - for (let key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - copiedObj[key] = deepCopy(obj[key]); - } - } - return copiedObj; - }; - - const deleteNodeFromData = (data, key) => { - const deleteData = (items) => { - for (let i = 0; i < items.length; i++) { - if (items[i].key === key) { - items.splice(i, 1); - return; - } - if (items[i].children) { - deleteData(items[i].children); - } - } - }; - - // Create a deep copy of the data to avoid mutating the original array - const newData = deepCopy(data); - deleteData(newData); - return newData; - }; - - useEffect(() => { - if (!calendarContentLanguage) return; - let t = {}; - calendarContentLanguage.forEach((language) => { - t[language] = generateFormattedData(data, language); - }); - setTreeDataCollection(t); - }, [data, calendarContentLanguage]); - - return ( -
- {calendarContentLanguage.map((language) => { - return ( - - - {t(`common.tab${capitalizeFirstLetter(language)}`)} - - -
- - onDrop({ - info, - treeData: treeDataCollection[language], - treeLanguage: contentLanguage.ENGLISH, - }) - } - onExpand={(key) => { - setExpandedKeys(key); - }} - treeData={treeDataCollection[language]} - /> -
-
- ); - })} - -
- { - setForEditing(false); - }} - centered - title={ - - {!forEditing ? t('dashboard.taxonomy.addNew.concepts.add') : t('dashboard.taxonomy.addNew.concepts.edit')} - - } - onCancel={() => handleAddChildModalClose()} - footer={ -
- {deleteDisplayFlag && ( -
- handleDelete()} - style={{ - border: '2px solid var(--content-alert-error, #f43131)', - background: 'var(--background-neutrals-transparent, rgba(255, 255, 255, 0))', - color: '#CE1111', - }} - /> -
- )} -
- handleAddChildModalClose()} - /> - -
-
- }> -
- - -