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) => (
+
+ );
+
+ 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()}
- />
-
-
-
- }>
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default DraggableTree;
diff --git a/src/components/DraggableTree/EditableCell.jsx b/src/components/DraggableTree/EditableCell.jsx
new file mode 100644
index 00000000..47742916
--- /dev/null
+++ b/src/components/DraggableTree/EditableCell.jsx
@@ -0,0 +1,100 @@
+import LiteralBadge from '../Badge/LiteralBadge';
+import { contentLanguageKeyMap } from '../../constants/contentLanguage';
+import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Input } from 'antd';
+
+/**
+ * @param {Object} props - Props for the EditableCell component.
+ * @param {string} props.title - Title of the column.
+ * @param {boolean} props.editable - Whether the cell is editable.
+ * @param {React.ReactNode} props.children - Child elements inside the cell.
+ * @param {string} props.dataIndex - Data index for the cell value.
+ * @param {Object} props.record - Record object for the current row.
+ * @param {function} props.handleSave - Function to save the edited cell value.
+ * @param {Object} [props.fallbackStatus] - Object containing fallback status details.
+ * @param {Object} [props.restProps] - Additional props passed to the cell.
+ */
+
+const { TextArea } = Input;
+
+const EditableCell = ({ title, editable, children, dataIndex, record, handleSave, fallbackStatus, ...restProps }) => {
+ const [editing, setEditing] = useState(false);
+ const [value, setValue] = useState();
+ const inputRef = useRef(null);
+ const { t } = useTranslation();
+
+ const toggleEdit = () => {
+ setEditing(!editing);
+ setTimeout(() => inputRef.current && inputRef.current.focus(), 0);
+ };
+
+ const save = () => {
+ handleSave({ ...record, [dataIndex]: value });
+ setEditing(false);
+ };
+
+ const handleInputChange = (e) => {
+ setValue(e.target.value);
+ };
+
+ useEffect(() => {
+ if (!record) return;
+ if (!record[dataIndex]) return;
+
+ setValue(record[dataIndex]);
+ }, [dataIndex, record]);
+
+ let isFallbackPresent = false;
+ let fallbackPromptText = '';
+ const recordKey = contentLanguageKeyMap[title?.toUpperCase()];
+
+ if (fallbackStatus && recordKey && fallbackStatus[recordKey]) {
+ isFallbackPresent = fallbackStatus[recordKey]?.tagDisplayStatus;
+ fallbackPromptText =
+ fallbackStatus[recordKey]?.fallbackLiteralKey == '?'
+ ? t('common.forms.languageLiterals.unKnownLanguagePromptText')
+ : t('common.forms.languageLiterals.knownLanguagePromptText');
+ }
+
+ const fallbackComponent = isFallbackPresent ? (
+
+ ) : (
+ <>>
+ );
+
+ if (!editable) {
+ return (
+
+ {children}
+ {fallbackComponent}
+ |
+ );
+ }
+
+ return (
+
+ {editing ? (
+
+ ) : (
+ {children}
+ )}
+ {fallbackComponent}
+ |
+ );
+};
+
+export default EditableCell;
diff --git a/src/components/DraggableTree/draggableTable.css b/src/components/DraggableTree/draggableTable.css
new file mode 100644
index 00000000..b39f4152
--- /dev/null
+++ b/src/components/DraggableTree/draggableTable.css
@@ -0,0 +1,137 @@
+.custom-table {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+}
+
+.custom-table .ant-table-wrapper {
+ flex: 1;
+}
+
+.custom-table .table-row:hover,
+.custom-table .table-row:hover td {
+ background-color: #eff2ff !important;
+}
+
+.custom-table .ant-table-container table > thead > tr th {
+ font-weight: 700;
+}
+
+.custom-table .ant-table-tbody .ant-table-cell > div:hover {
+ cursor: text;
+}
+
+.custom-table .ant-table-tbody .indent-level-0 {
+ margin-left: 0;
+}
+.custom-table .ant-table-tbody .indent-level-1 {
+ margin-left: 4px;
+}
+.custom-table .ant-table-tbody .indent-level-2 {
+ margin-left: 8px;
+}
+.custom-table .ant-table-tbody .indent-level-3 {
+ margin-left: 12px;
+}
+
+.custom-table .ant-table-tbody .ant-table-cell > div {
+ width: fit-content;
+ display: flex;
+ max-width: 80%;
+ text-wrap: wrap;
+}
+
+.custom-table .editable-row {
+ cursor: grab !important;
+}
+
+.custom-table .editable-row:active {
+ cursor: grabbing !important;
+}
+
+.custom-table .ant-table-tbody .drop-over-downward,
+.drop-over-upward {
+ transition: transform 0.2s ease, background-color 0.2s ease;
+ transform: scale(1.02);
+ background-color: rgba(24, 144, 255, 0.1);
+}
+
+.custom-table .ant-table-tbody .drop-over-upward,
+.custom-table .ant-table-tbody .drop-over-upward-in-gap {
+ transition: transform 0.2s ease, background-color 0.2s ease;
+ transform: scale(1.02);
+ background-color: rgba(24, 144, 255, 0.1);
+}
+
+.custom-table .ant-table-tbody .ant-table-cell > div .icon-container {
+ padding: 0px 8px;
+ margin-left: -12px;
+ display: grid;
+ place-content: center;
+ cursor: pointer;
+}
+
+.custom-table .ant-table-tbody .ant-table-cell > div .icon-container .anticon {
+ font-size: 16px;
+}
+
+.custom-table .ant-table-tbody .ant-table-cell .literal-badge {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ right: 12px;
+}
+
+.custom-table .ant-table-tbody .sort-handle-column {
+ width: 20px;
+}
+
+.custom-table tr.drop-over-upward td {
+ border-top: 2px dashed #1b3de6;
+}
+
+.custom-table .drop-over-upward-in-gap td {
+ border-top: 2px dashed #1b3de6;
+ border-bottom: 2px dashed #1b3de6;
+}
+
+.custom-table .drop-over-upward-in-gap td {
+ border-top: 2px dashed #1b3de6;
+ border-bottom: 2px dashed #1b3de6;
+}
+
+.custom-table .drop-over-upward-in-gap td:first-child {
+ border-left: 2px dashed #1b3de6;
+}
+
+.custom-table .drop-over-upward-in-gap td:last-child {
+ border-right: 2px dashed #1b3de6;
+}
+
+.custom-table .ant-table-tbody td .ant-input:focus,
+.custom-table .ant-table-tbody td .ant-input-focused {
+ border: 1px solid #b6c1c9;
+}
+
+.custom-table .ant-table-tbody td .ant-input-lg {
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.custom-table .outlined-button {
+ display: flex;
+ color: var(--content-action-default, #1b3de6);
+ text-align: center;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 24px;
+ flex-direction: row-reverse;
+ padding: 4px 4px;
+ width: fit-content;
+ margin-top: 20px;
+}
+
+.custom-table .outlined-button .outlined-label {
+ font-size: 12px;
+}
diff --git a/src/components/DraggableTree/draggableTree.css b/src/components/DraggableTree/draggableTree.css
deleted file mode 100644
index 51d81b45..00000000
--- a/src/components/DraggableTree/draggableTree.css
+++ /dev/null
@@ -1,218 +0,0 @@
-.draggable-tree .addmodal .ant-modal-body {
- display: flex;
- align-items: center;
-}
-
-.draggable-tree {
- display: flex;
- justify-content: space-between;
- width: 100%;
- min-height: 100px;
- overflow-x: scroll;
- overflow-y: hidden;
-}
-
-.draggable-tree .ant-tree-list {
- flex: 1;
-}
-
-.draggable-tree .tree-item {
- display: flex;
- padding: 8px;
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
- align-self: stretch;
- background: var(--background-neutrals-ground, #fff);
- border-radius: 0px 4px 4px 0px;
- border: solid 4px #eff2ff;
- border-right: none;
- overflow-y: scroll;
- color: var(--content-neutral-primary, #222732);
- flex: 1;
-}
-
-.draggable-tree > .ant-form-item,
-.draggable-tree > .ant-form-item > .ant-form-item-row,
-.draggable-tree > .ant-form-item > .ant-form-item-row > .ant-form-item-control,
-.draggable-tree > .ant-form-item > .ant-form-item-row > .ant-form-item-control > .ant-form-item-control-input,
-.draggable-tree
- > .ant-form-item
- > .ant-form-item-row
- > .ant-form-item-control
- > .ant-form-item-control-input
- > .ant-form-item-control-input-content {
- height: 100%;
-}
-
-.draggable-tree
- > .ant-form-item
- > .ant-form-item-row
- > .ant-form-item-control
- > .ant-form-item-control-input
- > .ant-form-item-control-input-content {
- display: flex;
- flex-direction: column;
-}
-
-.draggable-tree > .ant-form-item {
- flex: 1;
- max-width: 100%;
- min-width: 50%;
-}
-
-.draggable-tree .ant-tree .ant-tree-treenode-draggable .ant-tree-draggable-icon {
- min-width: 24px;
-}
-
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected {
- background-color: #fff;
-}
-
-.draggable-tree .ant-tree .ant-tree-treenode:hover,
-.draggable-tree .ant-tree .ant-tree-treenode:hover .ant-tree-node-content-wrapper,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper:hover,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper:hover .ant-tree-treenode,
-.draggable-tree .ant-tree .ant-tree-treenode:hover .ant-tree-node-content-wrapper.ant-tree-node-selected,
-.draggable-tree .ant-tree .ant-tree-treenode:hover .ant-tree-node-content-wrapper.ant-tree-node-selected,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper:hover .ant-tree-treenode.ant-tree-node-selected,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper:hover .ant-tree-treenode.ant-tree-node-selected,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected:hover,
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected:hover .ant-tree-treenode {
- background-color: #eff2ff;
- transition: 0.3s;
-}
-
-.draggable-tree .ant-tree .anticon {
- width: 20px;
- height: 20px;
- color: #222732;
- font-size: 20px;
- font-weight: 700;
-}
-
-.draggable-tree .ant-tree .ant-tree-treenode-draggable .ant-tree-draggable-icon {
- opacity: 1;
-}
-
-.draggable-tree .ant-tree.ant-tree-block-node .ant-tree-list-holder-inner .ant-tree-node-content-wrapper {
- flex: auto;
- color: var(--content-neutral-primary, #222732);
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- line-height: 16px;
-}
-
-.draggable-tree .tag-header {
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- line-height: 16px;
-}
-
-.draggable-tree .ant-tree .ant-tree-treenode {
- align-items: center;
- padding-top: 4px;
-}
-
-.draggable-tree .ant-tree-node-content-wrapper .ant-tree-node-content-wrapper-normal {
- display: flex;
- align-items: center;
- width: 100%;
-}
-
-.draggable-tree .ant-tree-switcher-noop {
- cursor: move !important;
- cursor: grab !important;
- cursor: -moz-grab !important;
- cursor: -webkit-grab !important;
-}
-
-.draggable-tree .ant-tree-draggable-icon {
- cursor: move;
- cursor: grab;
- cursor: -moz-grab;
- cursor: -webkit-grab;
-}
-
-.draggable-tree .ant-tree-switcher-noop:active {
- cursor: grabbing !important;
- cursor: -moz-grabbing !important;
- cursor: -webkit-grabbing !important;
-}
-
-.draggable-tree .ant-tree-draggable-icon :active {
- cursor: grabbing;
- cursor: -moz-grabbing;
- cursor: -webkit-grabbing;
-}
-
-.draggable-tree .ant-tree-node-content-wrapper .ant-tree-node-content-wrapper-normal .ant-tree-title {
- width: 100%;
-}
-
-.draggable-tree .draggable-tree-concept-label {
- display: flex;
- flex: 1;
- align-items: centers;
-}
-
-.custom-common-modal-container .ant-modal-footer .outlined-label {
- color: #ce1111;
-}
-
-.draggable-tree .ant-tree .ant-tree-node-content-wrapper:hover {
- background-color: #eff2ff;
-}
-
-.custom-common-modal-container-wrapper .add-new-concept-wrapper .ant-row {
- flex-direction: column;
-}
-
-.custom-common-modal-container-wrapper .add-new-concept-wrapper .ant-form-item-label {
- text-align: left;
-}
-
-.custom-common-modal-container .add-new-concept-wrapper .ant-form-item-label > label::after {
- display: none;
-}
-
-.draggable-tree > :nth-last-child(2) {
- border-right: solid 4px #eff2ff;
-}
-
-.draggable-tree > :nth-last-child(2) {
- position: relative;
-}
-
-.draggable-tree > :nth-last-child(2)::before {
- content: '';
- position: absolute;
- top: 0px;
- bottom: 0;
- right: -4px;
- width: 4px;
- height: 28px;
- background-color: #ffffff;
-}
-
-@media screen and (max-width: 480px) {
- .draggable-tree .ant-tree.ant-tree-block-node .ant-tree-list-holder-inner .ant-tree-node-content-wrapper {
- font-size: 10px;
- }
-
- .draggable-tree .ant-tree .anticon {
- font-size: 18px;
- }
-}
-
-@media screen and (max-width: 575px) {
- .draggable-tree {
- flex-direction: column;
- }
-
- .draggable-tree .tree-item {
- border-right: solid 4px #eff2ff;
- }
-}
diff --git a/src/locales/en/translationEn.json b/src/locales/en/translationEn.json
index 2995f673..12d6f257 100644
--- a/src/locales/en/translationEn.json
+++ b/src/locales/en/translationEn.json
@@ -1154,7 +1154,8 @@
"conceptName": "Name",
"placeHolder": "Enter {{language}} name",
"deleteConceptMessage": "Are you sure you want to permanently delete this concept? This action will also delete it everywhere it was used, including in events.",
- "deleteConceptHeading": "Delete concept"
+ "deleteConceptHeading": "Delete concept",
+ "delete": "delete"
}
}
}
diff --git a/src/locales/fr/transalationFr.json b/src/locales/fr/transalationFr.json
index 8580ad28..ad468c70 100644
--- a/src/locales/fr/transalationFr.json
+++ b/src/locales/fr/transalationFr.json
@@ -1153,7 +1153,8 @@
"conceptName": "Nom",
"placeHolder": "Indiquer un nom en {{language}}",
"deleteConceptHeading": "Supprimer le concept",
- "deleteConceptMessage": "Êtes-vous sûr de vouloir supprimer définitivement ce concept ? Cette action le supprimera également partout où il a été utilisé, y compris dans les événements."
+ "deleteConceptMessage": "Êtes-vous sûr de vouloir supprimer définitivement ce concept ? Cette action le supprimera également partout où il a été utilisé, y compris dans les événements.",
+ "delete": "Supprimer"
}
}
}
diff --git a/src/pages/Dashboard/AddTaxonomy/AddTaxonomy.jsx b/src/pages/Dashboard/AddTaxonomy/AddTaxonomy.jsx
index 591154d0..f8dfab4c 100644
--- a/src/pages/Dashboard/AddTaxonomy/AddTaxonomy.jsx
+++ b/src/pages/Dashboard/AddTaxonomy/AddTaxonomy.jsx
@@ -4,17 +4,18 @@ import { getStandardFieldTranslation, standardFieldsForTaxonomy } from '../../..
import { useLocation, useNavigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
import { taxonomyClassTranslations } from '../../../constants/taxonomyClass';
import { Card, Checkbox, Col, Form, Input, Row, notification } from 'antd';
+import Alert from '../../../components/Alert';
import BreadCrumbButton from '../../../components/Button/BreadCrumb/BreadCrumbButton';
import { useTranslation } from 'react-i18next';
import PrimaryButton from '../../../components/Button/Primary';
+import OutlinedButton from '../../..//components/Button/Outlined';
import './addTaxonomy.css';
import { useAddTaxonomyMutation, useLazyGetTaxonomyQuery, useUpdateTaxonomyMutation } from '../../../services/taxonomy';
import Select from '../../../components/Select';
import CardEvent from '../../../components/Card/Common/Event';
import SearchableCheckbox from '../../../components/Filter/SearchableCheckbox';
-import { DownOutlined, PlusOutlined } from '@ant-design/icons';
+import { DownOutlined } from '@ant-design/icons';
import { userRolesWithTranslation } from '../../../constants/userRoles';
-import Outlined from '../../../components/Button/Outlined';
import { useDispatch, useSelector } from 'react-redux';
import { getUserDetails } from '../../../redux/reducer/userSlice';
import { setErrorStates } from '../../../redux/reducer/ErrorSlice';
@@ -27,7 +28,8 @@ import { getCurrentCalendarDetailsFromUserDetails } from '../../../utils/getCurr
import { placeHolderCollectionCreator } from '../../../utils/MultiLingualFormItemSupportFunctions';
import CreateMultiLingualFormItems from '../../../layout/CreateMultiLingualFormItems/CreateMultiLingualFormItems';
import { contentLanguageKeyMap } from '../../../constants/contentLanguage';
-import DraggableTree from '../../../components/DraggableTree/DraggableTree';
+import DraggableTable from '../../../components/DraggableTree/DraggableTable';
+import { sanitizeData, transformLanguageKeys } from '../../../utils/draggableTableUtilFunctions';
const taxonomyClasses = taxonomyClassTranslations.map((item) => {
return { ...item, value: item.key };
@@ -59,12 +61,13 @@ const AddTaxonomy = () => {
const taxonomyId = searchParams.get('id');
setContentBackgroundColor('#F9FAFF');
+ const [transformedConceptData, setTransformedConceptData] = useState([]);
const [standardFields, setStandardFields] = useState([]);
+ const [languageLiteralBannerDisplayStatus, setLanguageLiteralBannerDisplayStatus] = useState(null);
const [dynamic, setDynamic] = useState(location.state?.dynamic ?? false);
+ const [fallbackStatus, setFallbackStatus] = useState({});
const [userAccess, setUserAccess] = useState();
- const [deleteDisplayFlag, setDeleteDisplayFlag] = useState(true);
const [conceptData, setConceptData] = useState([]);
- const [addNewPopup, setAddNewPopup] = useState(false);
const [isDirty, setIsDirty] = useState({
formState: false,
isSubmitting: false,
@@ -244,30 +247,20 @@ const AddTaxonomy = () => {
};
function modifyConceptData(conceptData) {
- return conceptData?.map(function (item) {
- let modifiedConcept;
- if (item && item.isNew) {
- modifiedConcept = {
- name: item.name,
+ return (
+ conceptData?.map((item) => {
+ // eslint-disable-next-line no-unused-vars
+ const filteredName = Object.fromEntries(Object.entries(item.name || {}).filter(([_, value]) => value !== ''));
+
+ return {
+ ...(item.id && { id: item.id }),
+ name: filteredName,
children: item.children ? modifyConceptData(item.children) : [],
};
- } else {
- modifiedConcept = {
- id: item.id,
- name: item.name,
- children: item.children ? modifyConceptData(item.children) : [],
- };
- }
-
- return modifiedConcept;
- });
+ }) || []
+ );
}
- const openAddNewConceptModal = () => {
- setAddNewPopup(true);
- setDeleteDisplayFlag(false);
- };
-
const handleValueChange = () => {
setIsDirty({
formState: form.isFieldsTouched(['userAccess', 'disambiguatingDescription', 'name', 'mappedToField', 'class']),
@@ -275,10 +268,32 @@ const AddTaxonomy = () => {
});
};
+ const handleClearAllFallbackStatus = () => {
+ const sanitizedData = sanitizeData(transformedConceptData, {});
+ const filteredConceptData = transformLanguageKeys(sanitizedData);
+ setConceptData(filteredConceptData);
+
+ setLanguageLiteralBannerDisplayStatus(false);
+ setFallbackStatus({});
+ };
+
useEffect(() => {
if (isReadOnly) navigate(`${PathName.Dashboard}/${calendarId}${PathName.Taxonomies}`, { replace: true });
}, [isReadOnly]);
+ useEffect(() => {
+ // Flatten the fallbackStatus structure to extract all tagdisplaystatus values
+ const allTagDisplayStatuses = Object.values(fallbackStatus).flatMap((value) =>
+ typeof value === 'object'
+ ? Object.values(value).map((innerValue) => innerValue.tagdisplaystatus)
+ : [value.tagdisplaystatus],
+ );
+
+ allTagDisplayStatuses.every((status) => status === false)
+ ? setLanguageLiteralBannerDisplayStatus(false)
+ : setLanguageLiteralBannerDisplayStatus(true);
+ }, [fallbackStatus]);
+
return (
<>
{
-
+
{t('dashboard.taxonomy.addNew.concepts.heading')}
-
-
-
-
-
+
+
{t('dashboard.taxonomy.addNew.concepts.description')}
+
+
+ {languageLiteralBannerDisplayStatus && (
+
+
+
+
+
+ handleClearAllFallbackStatus()}
+ />
+ }
+ />
+
+
+
+
+
+ )}
+
+
+
{
width: 'calc(100% - 100px)',
}}>
-
diff --git a/src/pages/Dashboard/AddTaxonomy/addTaxonomy.css b/src/pages/Dashboard/AddTaxonomy/addTaxonomy.css
index 50e29434..5a91c50a 100644
--- a/src/pages/Dashboard/AddTaxonomy/addTaxonomy.css
+++ b/src/pages/Dashboard/AddTaxonomy/addTaxonomy.css
@@ -23,6 +23,10 @@
line-height: 24px;
}
+.add-taxonomy-wrapper .language-literal-banner .ant-alert-message {
+ color: #1a64a7;
+}
+
.add-taxonomy-wrapper .field-description {
color: var(--content-neutral-secondary, #646d7b);
font-size: 16px;
@@ -102,20 +106,6 @@
border-radius: 4px 4px 0px 0px;
}
-.add-taxonomy-wrapper .concept-card .outlined-button {
- display: flex;
- color: var(--content-action-default, #1b3de6);
- text-align: center;
- font-size: 16px;
- font-style: normal;
- font-weight: 600;
- line-height: 24px;
- flex-direction: row-reverse;
- gap: 8px;
- padding: 8px 16px; /* Adjust the value according to your design */
- height: 40px;
-}
-
.add-taxonomy-wrapper .disabled-dropdown .ant-dropdown-trigger {
border-radius: 4px;
border: 1px solid var(--content-neutral-stroke, #b6c1c9);
@@ -202,9 +192,3 @@
font-size: 20px !important;
}
}
-
-@media screen and (max-width: 768px) {
- .add-taxonomy-wrapper .concept-card .outlined-button {
- margin-bottom: 8px;
- }
-}
diff --git a/src/utils/draggableTableUtilFunctions.js b/src/utils/draggableTableUtilFunctions.js
new file mode 100644
index 00000000..1b2f1d29
--- /dev/null
+++ b/src/utils/draggableTableUtilFunctions.js
@@ -0,0 +1,204 @@
+import { contentLanguageKeyMap } from '../constants/contentLanguage';
+
+/**
+ * Clones and updates fallbackStatus by removing the specified columnKey from the row's fallback info.
+ *
+ * @param {Object} fallbackStatus - The current fallback status object containing information about rows.
+ * @param {Object} row - The row object that includes a key to identify its fallback info.
+ * @param {string} columnKey - The key to be removed from the fallback info of the specified row.
+ * @returns {Object} - A new fallback status object with the updated data.
+ */
+export const cloneFallbackStatus = (fallbackStatus, row, columnKey) => {
+ const clonedStatus = { ...fallbackStatus };
+ const rowFallbackInfo = clonedStatus[row.key];
+
+ if (rowFallbackInfo) {
+ // eslint-disable-next-line no-unused-vars
+ const { [columnKey]: _, ...updatedFallbackInfo } = rowFallbackInfo;
+ clonedStatus[row.key] = updatedFallbackInfo;
+ }
+
+ return clonedStatus;
+};
+
+/**
+ * Recursively updates nodes in a tree structure based on the row data.
+ *
+ * @param {Array} nodes - The array of tree nodes to be updated.
+ * @param {Object} row - The row object containing data to update in the tree nodes.
+ * @returns {Array} - The updated tree structure.
+ */
+export const updateNodeData = (nodes, row) => {
+ return nodes.map((node) => {
+ if (node.key === row.key) {
+ return { ...node, ...row };
+ }
+ if (node.children && node.children.length > 0) {
+ return { ...node, children: updateNodeData(node.children, row) };
+ }
+ return node;
+ });
+};
+
+/**
+ * Removes properties from nodes based on the fallbackStatus conditions.
+ *
+ * @param {Array} data - The array of tree nodes to sanitize.
+ * @param {Object} fallbackStatus - The fallback status object.
+ * @returns {Array} - The sanitized tree structure.
+ */
+export const sanitizeData = (data, fallbackStatus) => {
+ return data.map((item) => {
+ // Create a shallow copy of the item to avoid mutating the original object
+ const updatedItem = { ...item };
+ const fallbackInfo = fallbackStatus[updatedItem.key];
+
+ if (fallbackInfo) {
+ Object.entries(fallbackInfo).forEach(([key, value]) => {
+ if (value.tagDisplayStatus === true && key in updatedItem) {
+ delete updatedItem[key];
+ }
+ });
+ }
+
+ if (updatedItem.children && updatedItem.children.length > 0) {
+ // Recursively sanitize the children and update the children property
+ updatedItem.children = sanitizeData(updatedItem.children, fallbackStatus);
+ }
+
+ return updatedItem;
+ });
+};
+
+/**
+ * Transforms nodes to consolidate language keys into a `name` object and removes them from the node.
+ *
+ * @param {Array} data - The array of tree nodes to transform.
+ * @returns {Array} - The transformed tree structure with consolidated language keys for name field.
+ */
+export const transformLanguageKeys = (data) => {
+ return data.map((item) => {
+ const name = {};
+
+ Object.values(contentLanguageKeyMap).forEach((langKey) => {
+ if (langKey in item) {
+ name[langKey] = item[langKey];
+ delete item[langKey];
+ }
+ });
+
+ if (Object.keys(name).length > 0) {
+ item.name = name;
+ }
+
+ if (item.children && item.children.length > 0) {
+ item.children = transformLanguageKeys(item.children);
+ }
+
+ return item;
+ });
+};
+
+/**
+ * Creates a deep clone of an object using JSON serialization.
+ *
+ * @param {Object} obj - The object to clone.
+ * @returns {Object} - The deep-cloned object.
+ */
+export function deepClone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+/**
+ * Creates a deep copy of an object or array.
+ *
+ * @param {Object|Array} obj - The object or array to deep copy.
+ * @returns {Object|Array} - The deep-copied object or array.
+ */
+export 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;
+};
+
+/**
+ * Moves a concept (node) from one part of the tree/table to another.
+ *
+ * @param {string} dragKey - The key of the node being dragged.
+ * @param {string} dropKey - The key of the node where the dragged node is dropped.
+ * @param {Array} data - The array of tree nodes to modify.
+ * @param {boolean} [dropToGap=false] - Whether to drop the node into a gap (as a sibling) or as a child.
+ * @returns {Array} - The modified tree structure with the node moved.
+ */
+export function moveConcept(dragKey, dropKey, data, dropToGap = false) {
+ let clonedData = deepClone(data);
+
+ let dragParent = null;
+ let dragItem = null;
+ let dropParent = null;
+ let dropIndex = -1;
+
+ function findItemAndParent(key, items, parent = null) {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+
+ if (item.key === key) {
+ return { item, parent, index: i };
+ }
+
+ if (item.children) {
+ const result = findItemAndParent(key, item.children, item);
+ if (result) return result;
+ }
+ }
+ return null;
+ }
+
+ const dragResult = findItemAndParent(dragKey, clonedData);
+ if (dragResult) {
+ dragItem = dragResult.item;
+ dragParent = dragResult.parent;
+ }
+
+ const dropResult = findItemAndParent(dropKey, clonedData);
+ if (dropResult) {
+ dropParent = dropResult.parent;
+ dropIndex = dropResult.index;
+ }
+
+ if (dragItem && dropResult) {
+ // Remove drag item from its original position
+ if (dragParent) {
+ dragParent.children = dragParent.children.filter((item) => item.key !== dragKey);
+ } else {
+ clonedData = clonedData.filter((item) => item.key !== dragKey);
+ }
+
+ if (dropToGap) {
+ // Add drag item as a child of the drop item
+ if (!dropResult.item.children) {
+ dropResult.item.children = [];
+ }
+ dropResult.item.children.push(dragItem);
+ } else {
+ // Add drag item as a sibling at the drop index
+ if (dropParent) {
+ dropParent.children.splice(dropIndex, 0, dragItem);
+ } else {
+ clonedData.splice(dropIndex, 0, dragItem);
+ }
+ }
+ }
+
+ return clonedData;
+}