Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert drag/drop sort to operate on groups instead #3513

Merged
merged 7 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useTreeRef } from "./provider/TreeRefProvider";
import { v4 as uuid } from "uuid";
import { findParentGroup } from "./util/findParentGroup";
import "react-complex-tree/lib/style-modern.css";
import { Group, GroupsType } from "@lib/formContext";
// import { Item } from "./Item";

export interface TreeDataProviderProps {
Expand All @@ -29,22 +30,51 @@ export interface TreeDataProviderProps {
// openSection?: (id: string) => void;
}

const findItemIndex = (items: string[], itemIndex: string | number) =>
items.indexOf(String(itemIndex));

const isOldItemPriorToNewItem = (
items: string[],
itemIndex: string | number,
targetIndex: number
) => (items.findIndex((child) => child === itemIndex) ?? Infinity) < targetIndex;

const removeItemAtIndex = (items: string[], index: number) => {
const updatedItems = [...items];
updatedItems.splice(index, 1);
return updatedItems;
};

const insertItemAtIndex = (items: string[], item: string, index: number) => {
const updatedItems = [...items];
updatedItems.splice(index, 0, item);
return updatedItems;
};

const ControlledTree: ForwardRefRenderFunction<unknown, TreeDataProviderProps> = (
{ children },
ref
) => {
// export const TreeView = () => {
const { getTreeData, addGroup, setId, updateGroupName, updateGroup, updateElementTitle } =
useGroupStore((s) => {
return {
getTreeData: s.getTreeData,
addGroup: s.addGroup,
setId: s.setId,
updateGroupName: s.updateGroupName,
updateElementTitle: s.updateElementTitle,
updateGroup: s.updateGroup,
};
});
const {
getTreeData,
getGroups,
addGroup,
setId,
updateGroupName,
replaceGroups,
updateElementTitle,
} = useGroupStore((s) => {
return {
getTreeData: s.getTreeData,
getGroups: s.getGroups,
replaceGroups: s.replaceGroups,
addGroup: s.addGroup,
setId: s.setId,
updateGroupName: s.updateGroupName,
updateElementTitle: s.updateElementTitle,
};
});

const { tree, environment } = useTreeRef();
const [focusedItem, setFocusedItem] = useState<TreeItemIndex | undefined>();
Expand Down Expand Up @@ -109,6 +139,13 @@ const ControlledTree: ForwardRefRenderFunction<unknown, TreeDataProviderProps> =
}}
canDropAt={(items, target) => {
const folderItemsCount = items.filter((item) => item.isFolder).length;
const nonFolderItemsCount = items.filter((item) => !item.isFolder).length;

// can't drag mixed item types
if (folderItemsCount > 0 && nonFolderItemsCount > 0) {
return false;
}

// if any of the selected items is a folder, disallow dropping on a folder
if (folderItemsCount >= 1) {
const { parentItem } = target as DraggingPositionBetweenItems;
Expand All @@ -117,6 +154,13 @@ const ControlledTree: ForwardRefRenderFunction<unknown, TreeDataProviderProps> =
}
}

// if any of the items is not a folder, disallow dropping on root
if (nonFolderItemsCount >= 1) {
if (target.depth === 0) {
return false;
}
}

return true;
}}
onRenameItem={(item, name) => {
Expand All @@ -132,80 +176,161 @@ const ControlledTree: ForwardRefRenderFunction<unknown, TreeDataProviderProps> =
setSelectedItems([item.index]);
}}
onDrop={async (items: TreeItem[], target: DraggingPosition) => {
// Current state of the tree in Groups format
let currentGroups = getGroups() as GroupsType;

// Target parent and index
const { parentItem: targetParent, childIndex: targetIndex } =
target as DraggingPositionBetweenItems;

let newGroups: GroupsType;
const selectedItems: string[] = [];

// Dragging/dropping root-level items
if (targetParent === "root") {
let elements = Object.keys(currentGroups);

let itemsPriorToInsertion = 0;

items.forEach((item, index) => {
// Original location
const originIndex = findItemIndex(elements, item.index);

// Adjust index if dragging down
itemsPriorToInsertion += isOldItemPriorToNewItem(elements, item.index, targetIndex)
? 1
: 0;

// Remove from old position
elements = removeItemAtIndex(elements, originIndex);

// Insert at new position
elements = insertItemAtIndex(
elements,
String(item.index),
targetIndex - itemsPriorToInsertion + index
);

selectedItems.push(String(item.index));
});

// Create a new Groups object
newGroups = elements.reduce((acc: GroupsType, key) => {
const data = currentGroups[key] as Group;
acc[key] = data;
return acc;
}, {});

replaceGroups(newGroups);
setSelectedItems(selectedItems);

return;
}

// Dragging/dropping other non-root level items
const targetParentGroup = currentGroups[targetParent];
let targetGroupElements = [...targetParentGroup.elements];

let itemsPriorToInsertion = 0;

// current state of the tree
const currentItems = getTreeData();
let originParentGroup;
let originGroupElements: string[] = [];

// Ids of the items being dragged
const itemsIndices = items.map((i) => i.index);
items.forEach((item, index) => {
// Remove item from original location
const originParent = findParentGroup(getTreeData(), String(item.index));
originParentGroup = currentGroups[originParent?.index as string];

// Target parent and index
const { parentItem: targetParentIndex, childIndex: targetIndex } =
target as DraggingPositionBetweenItems;
// Dragging/dropping item within same group
if (originParentGroup == targetParentGroup) {
originGroupElements = (originParent?.children || []) as string[];
const originIndex = originGroupElements.indexOf(String(item.index));

// Loop over the items being dragged
items.forEach((item) => {
// Find the parent of the item being dragged
const originParent = findParentGroup(currentItems, String(item.index));
// Adjust index if dragging down
itemsPriorToInsertion += isOldItemPriorToNewItem(
targetGroupElements,
item.index,
targetIndex
)
? 1
: 0;

if (!originParent) {
throw Error(`Could not find parent of item "${item.index}"`);
}
// Remove item from previous location
originGroupElements = removeItemAtIndex(originGroupElements, originIndex);

if (!originParent.children) {
throw Error(
`Parent "${originParent.index}" of item "${item.index}" did not have any children`
// Insert item at new location
originGroupElements = insertItemAtIndex(
originGroupElements,
String(item.index),
targetIndex - itemsPriorToInsertion + index
);
}

if (target.targetType === "between-items" && target.parentItem === item.index) {
// Trying to drop inside itself
return;
}
// Create a new Groups object
const newGroups = { ...currentGroups };
newGroups[String(originParent?.index)] = {
name: String(originParent?.index),
elements: originGroupElements,
};

// Replace the original groups object
currentGroups = newGroups;

// Target/Origin groups are the same
targetGroupElements = originGroupElements;

selectedItems.push(String(item.index));

// Origin index of the item being dragged
const originIndex = originParent.children.indexOf(String(item.index));
// Replace the groups object and set selected items.
replaceGroups(newGroups);

if (originIndex === -1) {
throw Error(`Item "${item.index}" not found in parent "${originParent.index}"`);
return;
}

// Get the target parent
const targetParent = currentItems[targetParentIndex];
// Dragging/dropping item between groups
originGroupElements = (originParent?.children || []) as string[];
const originIndex = originGroupElements.indexOf(String(item.index));

// Adjust the index if the item is being moved to a position after itself
const isOldItemPriorToNewItem =
((targetParent.children ?? []).findIndex((child) => child === item.index) ?? Infinity) <
targetIndex;
itemsPriorToInsertion += isOldItemPriorToNewItem ? 1 : 0;
// Adjust index if dragging down
itemsPriorToInsertion += isOldItemPriorToNewItem(
targetGroupElements,
item.index,
targetIndex
)
? 1
: 0;

// Remove the item from the origin parent
originParent.children.splice(originIndex, 1);
// Remove item from previous location
originGroupElements = removeItemAtIndex(originGroupElements, originIndex);

// Update groups
updateGroup(originParent.index, originParent.children);
});
// Create a new Groups object
let newGroups = { ...currentGroups };
newGroups[String(originParent?.index)] = {
name: String(originParent?.index),
elements: originGroupElements,
};

// Get the new state of the tree
const newItems = getTreeData();
// Replace the original groups object
currentGroups = newGroups;

// Get the target parent in the new state
const targetParent = newItems[targetParentIndex];
// Insert at new position
targetGroupElements = insertItemAtIndex(
targetGroupElements,
String(item.index),
targetIndex + index
);

// Initialize children if there are none
if (!targetParent.children) {
targetParent.children = [];
}
newGroups = { ...currentGroups };
newGroups[targetParentGroup.name] = {
name: targetParentGroup.name,
elements: targetGroupElements,
};

// Insert the items into the target parent
targetParent.children.splice(targetIndex - itemsPriorToInsertion, 0, ...itemsIndices);
selectedItems.push(String(item.index));

// Update groups
updateGroup(targetParentIndex, targetParent.children);
replaceGroups(newGroups);
});

// Set selected to trigger a re-render
setSelectedItems([targetParentIndex]);
setSelectedItems(selectedItems);
}}
onFocusItem={(item) => {
setFocusedItem(item.index);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import { FormElement } from "@lib/types";
import { findNextGroup } from "../util/findNextGroup";
import { findPreviousGroup } from "../util/findPreviousGroup";
import { getGroupFromId } from "../util/getGroupFromId";
import { Group } from "@lib/formContext";
import { GroupsType } from "@lib/formContext";
import { Group, GroupsType } from "@lib/formContext";
import { TreeItem, TreeItemIndex } from "react-complex-tree";
import { autoSetNextAction } from "../util/setNextAction";
import { setGroupNextAction } from "../util/setNextAction";
Expand All @@ -33,6 +32,7 @@ export interface GroupStoreState extends GroupStoreProps {
addGroup: (id: string, name: string) => void;
deleteGroup: (id: string) => void;
replaceGroups: (groups: GroupsType) => void;
getGroups: () => GroupsType | undefined;
getTreeData: () => TreeItems;
updateGroup: (parent: TreeItemIndex, children: TreeItemIndex[] | undefined) => void;
findParentGroup: (id: string) => TreeItem | undefined;
Expand Down Expand Up @@ -108,6 +108,7 @@ const createGroupStore = (initProps?: Partial<GroupStoreProps>) => {
setChangeKey(String(new Date().getTime()));
}
},
getGroups: () => get().templateStore.getState().form.groups,
getTreeData: () => {
const formGroups = get().templateStore.getState().form.groups;
const elements = get().templateStore.getState().form.elements;
Expand Down