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()} - /> - -
-
- }> -
- - -