Fix menu reordering in navigation (#1871)
This commit is contained in:
parent
5850644742
commit
b419fd9975
7 changed files with 1344 additions and 225 deletions
|
@ -12,12 +12,11 @@ import { Backlink } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { maybe } from "../../../misc";
|
|
||||||
import { MenuDetails_menu } from "../../types/MenuDetails";
|
import { MenuDetails_menu } from "../../types/MenuDetails";
|
||||||
import { MenuItemType } from "../MenuItemDialog";
|
import { MenuItemType } from "../MenuItemDialog";
|
||||||
import MenuItems, { TreeOperation } from "../MenuItems";
|
import MenuItems, { TreeOperation } from "../MenuItems";
|
||||||
import MenuProperties from "../MenuProperties";
|
import MenuProperties from "../MenuProperties";
|
||||||
import { computeTree } from "./tree";
|
import { computeRelativeTree } from "./tree";
|
||||||
|
|
||||||
export interface MenuDetailsFormData {
|
export interface MenuDetailsFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -55,17 +54,20 @@ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const initialForm: MenuDetailsFormData = {
|
const initialForm: MenuDetailsFormData = {
|
||||||
name: maybe(() => menu.name, "")
|
name: menu?.name ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const [treeOperations, setTreeOperations] = React.useState<TreeOperation[]>(
|
const [treeOperations, setTreeOperations] = React.useState<TreeOperation[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const removeSimulatedMoves = (operations: TreeOperation[]) =>
|
||||||
|
operations.filter(operation => !operation.simulatedMove);
|
||||||
|
|
||||||
const handleSubmit = async (data: MenuDetailsFormData) => {
|
const handleSubmit = async (data: MenuDetailsFormData) => {
|
||||||
const result = await onSubmit({
|
const result = await onSubmit({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
operations: treeOperations
|
operations: removeSimulatedMoves(treeOperations)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
@ -75,10 +77,8 @@ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (operation: TreeOperation) => {
|
const handleChange = (operations: TreeOperation[]) => {
|
||||||
if (!!operation) {
|
setTreeOperations([...treeOperations, ...operations]);
|
||||||
setTreeOperations([...treeOperations, operation]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -110,17 +110,25 @@ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
<MenuItems
|
<MenuItems
|
||||||
canUndo={treeOperations.length > 0}
|
canUndo={treeOperations.length > 0}
|
||||||
items={maybe(() =>
|
items={
|
||||||
computeTree(menu.items, [...treeOperations])
|
menu?.items
|
||||||
)}
|
? computeRelativeTree(menu.items, treeOperations)
|
||||||
|
: []
|
||||||
|
}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onItemAdd={onItemAdd}
|
onItemAdd={onItemAdd}
|
||||||
onItemClick={onItemClick}
|
onItemClick={onItemClick}
|
||||||
onItemEdit={onItemEdit}
|
onItemEdit={onItemEdit}
|
||||||
onUndo={() =>
|
onUndo={() =>
|
||||||
setTreeOperations(
|
setTreeOperations(operations => {
|
||||||
treeOperations.slice(0, treeOperations.length - 1)
|
if (operations.length > 1) {
|
||||||
)
|
// Undo of a simulated move needs removal of 2 moves instead of one
|
||||||
|
if (operations[operations.length - 2].simulatedMove) {
|
||||||
|
return operations.slice(0, operations.length - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return operations.slice(0, operations.length - 1);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -45,23 +45,28 @@ function removeNode(
|
||||||
return newTree;
|
return newTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNode(
|
function insertNode({
|
||||||
tree: MenuDetails_menu_items[],
|
tree,
|
||||||
path: number[],
|
path,
|
||||||
node: MenuDetails_menu_items,
|
node,
|
||||||
position: number
|
position
|
||||||
): MenuDetails_menu_items[] {
|
}: {
|
||||||
|
tree: MenuDetails_menu_items[];
|
||||||
|
path: number[];
|
||||||
|
node: MenuDetails_menu_items;
|
||||||
|
position: number;
|
||||||
|
}): MenuDetails_menu_items[] {
|
||||||
if (path.length === 0) {
|
if (path.length === 0) {
|
||||||
return [...tree.slice(0, position), node, ...tree.slice(position)];
|
return [...tree.slice(0, position), node, ...tree.slice(position)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path[0] in tree) {
|
if (path[0] in tree) {
|
||||||
tree[path[0]].children = insertNode(
|
tree[path[0]].children = insertNode({
|
||||||
tree[path[0]].children,
|
tree: tree[path[0]].children,
|
||||||
path.slice(1),
|
path: path.slice(1),
|
||||||
node,
|
node,
|
||||||
position
|
position
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
@ -89,45 +94,48 @@ function removeNodeAndChildren(
|
||||||
return removeNode(tree, sourcePath);
|
return removeNode(tree, sourcePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function permuteNode(
|
function permuteRelativeNode(
|
||||||
tree: MenuDetails_menu_items[],
|
tree: MenuDetails_menu_items[],
|
||||||
permutation: TreeOperation
|
permutation: TreeOperation
|
||||||
): MenuDetails_menu_items[] {
|
): MenuDetails_menu_items[] {
|
||||||
const sourcePath = findNode(tree, permutation.id);
|
const sourcePath = findNode(tree, permutation.id);
|
||||||
const node = getNode(tree, sourcePath);
|
const node = getNode(tree, sourcePath);
|
||||||
|
|
||||||
|
const hasParent = !!permutation.parentId;
|
||||||
|
|
||||||
const treeAfterRemoval = removeNode(tree, sourcePath);
|
const treeAfterRemoval = removeNode(tree, sourcePath);
|
||||||
|
|
||||||
const targetPath = permutation.parentId
|
const targetPath = hasParent
|
||||||
? findNode(treeAfterRemoval, permutation.parentId)
|
? findNode(treeAfterRemoval, permutation.parentId)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const treeAfterInsertion = insertNode(
|
const position = sourcePath[sourcePath.length - 1];
|
||||||
treeAfterRemoval,
|
|
||||||
targetPath,
|
const treeAfterInsertion = insertNode({
|
||||||
|
tree: treeAfterRemoval,
|
||||||
|
path: targetPath,
|
||||||
node,
|
node,
|
||||||
permutation.sortOrder
|
position: position + permutation.sortOrder
|
||||||
);
|
});
|
||||||
|
|
||||||
return treeAfterInsertion;
|
return treeAfterInsertion;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeOperation(
|
function executeRelativeOperation(
|
||||||
tree: MenuDetails_menu_items[],
|
tree: MenuDetails_menu_items[],
|
||||||
operation: TreeOperation
|
operation: TreeOperation
|
||||||
): MenuDetails_menu_items[] {
|
): MenuDetails_menu_items[] {
|
||||||
return operation.type === "move"
|
return operation.type === "move"
|
||||||
? permuteNode(tree, operation)
|
? permuteRelativeNode(tree, operation)
|
||||||
: removeNodeAndChildren(tree, operation);
|
: removeNodeAndChildren(tree, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeTree(
|
export function computeRelativeTree(
|
||||||
tree: MenuDetails_menu_items[],
|
tree: MenuDetails_menu_items[],
|
||||||
operations: TreeOperation[]
|
operations: TreeOperation[]
|
||||||
) {
|
) {
|
||||||
const newTree = operations.reduce(
|
const newTree = operations.reduce(
|
||||||
(acc, operation) => executeOperation(acc, operation),
|
(acc, operation) => executeRelativeOperation(acc, operation),
|
||||||
// FIXME: 😡
|
|
||||||
JSON.parse(JSON.stringify(tree))
|
JSON.parse(JSON.stringify(tree))
|
||||||
);
|
);
|
||||||
return newTree;
|
return newTree;
|
||||||
|
|
|
@ -21,7 +21,7 @@ const NODE_MARGIN = 40;
|
||||||
export interface MenuItemsProps {
|
export interface MenuItemsProps {
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
items: MenuDetails_menu_items[];
|
items: MenuDetails_menu_items[];
|
||||||
onChange: (operation: TreeOperation) => void;
|
onChange: (operations: TreeOperation[]) => void;
|
||||||
onItemAdd: () => void;
|
onItemAdd: () => void;
|
||||||
onItemClick: (id: string, type: MenuItemType) => void;
|
onItemClick: (id: string, type: MenuItemType) => void;
|
||||||
onItemEdit: (id: string) => void;
|
onItemEdit: (id: string) => void;
|
||||||
|
@ -181,10 +181,12 @@ const Node: React.FC<NodeRendererProps> = props => {
|
||||||
className={classes.deleteButton}
|
className={classes.deleteButton}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
node.onChange({
|
node.onChange([
|
||||||
id: node.id as any,
|
{
|
||||||
|
id: node.id,
|
||||||
type: "remove"
|
type: "remove"
|
||||||
})
|
}
|
||||||
|
])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
|
@ -245,6 +247,7 @@ const MenuItems: React.FC<MenuItemsProps> = props => {
|
||||||
marginLeft: NODE_MARGIN * (path.length - 1)
|
marginLeft: NODE_MARGIN * (path.length - 1)
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
maxDepth={5}
|
||||||
isVirtualized={false}
|
isVirtualized={false}
|
||||||
rowHeight={NODE_HEIGHT}
|
rowHeight={NODE_HEIGHT}
|
||||||
treeData={items.map(item =>
|
treeData={items.map(item =>
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Properly computes diffs # 1`] = `
|
exports[`Properly computes diffs # 1`] = `
|
||||||
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "1glasses",
|
"id": "1glasses",
|
||||||
"parentId": "0jewelry",
|
"parentId": "0jewelry",
|
||||||
"sortOrder": 0,
|
"sortOrder": 0,
|
||||||
"type": "move",
|
"type": "move",
|
||||||
}
|
},
|
||||||
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Properly computes diffs # 2`] = `
|
exports[`Properly computes diffs # 2`] = `
|
||||||
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "1glasses",
|
"id": "1glasses",
|
||||||
"parentId": "2accessories",
|
"parentId": "2accessories",
|
||||||
"sortOrder": 0,
|
"sortOrder": -1,
|
||||||
"type": "move",
|
"type": "move",
|
||||||
}
|
},
|
||||||
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Properly computes diffs # 3`] = `
|
exports[`Properly computes diffs # 3`] = `
|
||||||
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "2accessories",
|
"id": "2accessories",
|
||||||
"parentId": "4apparel",
|
"parentId": "4apparel",
|
||||||
"sortOrder": 0,
|
"sortOrder": 0,
|
||||||
"type": "move",
|
"type": "move",
|
||||||
}
|
},
|
||||||
|
]
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface TreeOperation {
|
||||||
type: TreeOperationType;
|
type: TreeOperationType;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
simulatedMove?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unknownTypeError = Error("Unknown type");
|
export const unknownTypeError = Error("Unknown type");
|
||||||
|
@ -64,11 +65,11 @@ export function getItemId(item: MenuDetails_menu_items): string {
|
||||||
export function getDiff(
|
export function getDiff(
|
||||||
originalTree: TreeItem[],
|
originalTree: TreeItem[],
|
||||||
newTree: TreeItem[]
|
newTree: TreeItem[]
|
||||||
): TreeOperation {
|
): TreeOperation[] {
|
||||||
const originalMap = treeToMap(originalTree, "root");
|
const originalMap = treeToMap(originalTree, "root");
|
||||||
const newMap = treeToMap(newTree, "root");
|
const newMap = treeToMap(newTree, "root");
|
||||||
|
|
||||||
const diff: TreeOperation[] = Object.keys(newMap).map(key => {
|
const diff: TreeOperation[] = Object.keys(newMap).flatMap(key => {
|
||||||
const originalNode = originalMap[key];
|
const originalNode = originalMap[key];
|
||||||
const newNode = newMap[key];
|
const newNode = newMap[key];
|
||||||
|
|
||||||
|
@ -76,23 +77,56 @@ export function getDiff(
|
||||||
|
|
||||||
if (patch.length > 0) {
|
if (patch.length > 0) {
|
||||||
const addedNode = patch.find(operation => operation.type === "add");
|
const addedNode = patch.find(operation => operation.type === "add");
|
||||||
|
const removedNode = patch.find(operation => operation.type === "remove");
|
||||||
|
|
||||||
if (!!addedNode) {
|
if (!!addedNode) {
|
||||||
|
const changedParent = originalNode.length !== newNode.length;
|
||||||
|
const sortOrder = removedNode
|
||||||
|
? addedNode.newPos - removedNode.oldPos
|
||||||
|
: addedNode.newPos;
|
||||||
|
|
||||||
|
// This exists because backend doesn't recongize the position of the new node
|
||||||
|
// when it's moved from child to parent and/or up
|
||||||
|
// We have to make an additional move so that backend can sort the new tree correctly
|
||||||
|
// because without it the new node goes to the end of the parent node array by default
|
||||||
|
// SimulatedMove is removed before submit
|
||||||
|
if (changedParent && sortOrder !== originalNode.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: addedNode.items[0],
|
||||||
|
parentId: key === "root" ? undefined : key,
|
||||||
|
sortOrder: newNode.length - 1,
|
||||||
|
type: "move" as TreeOperationType,
|
||||||
|
simulatedMove: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: addedNode.items[0],
|
||||||
|
parentId: key === "root" ? undefined : key,
|
||||||
|
sortOrder:
|
||||||
|
sortOrder - newNode.length < 0
|
||||||
|
? sortOrder - newNode.length + 1
|
||||||
|
: sortOrder - newNode.length - 1,
|
||||||
|
type: "move" as TreeOperationType
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: addedNode.items[0],
|
id: addedNode.items[0],
|
||||||
parentId: key === "root" ? undefined : key,
|
parentId: key === "root" ? undefined : key,
|
||||||
sortOrder: addedNode.newPos,
|
sortOrder,
|
||||||
type: "move" as TreeOperationType
|
type: "move" as TreeOperationType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return diff.find(d => !!d);
|
return diff.filter(d => !!d);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeData(
|
export function getNodeData(
|
||||||
item: MenuDetails_menu_items,
|
item: MenuDetails_menu_items,
|
||||||
onChange: (operation: TreeOperation) => void,
|
onChange: (operations: TreeOperation[]) => void,
|
||||||
onClick: (id: string, type: MenuItemType) => void,
|
onClick: (id: string, type: MenuItemType) => void,
|
||||||
onEdit: (id: string) => void
|
onEdit: (id: string) => void
|
||||||
): TreeItem {
|
): TreeItem {
|
||||||
|
|
|
@ -103589,14 +103589,23 @@ exports[`Storyshots Views / Navigation / Menu details loading 1`] = `
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MenuItems-container-id"
|
class="MenuItems-container-id"
|
||||||
style="min-height:56px;padding:0 24px;padding-top:20px"
|
style="min-height:-56px;padding:;padding-top:"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="Skeleton-skeleton-id"
|
class="rst__tree MenuItems-root-id"
|
||||||
data-test-id="skeleton"
|
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
</span>
|
<div
|
||||||
|
class="MuiPaper-root-id MenuItems-row-id MuiPaper-elevation0-id MuiPaper-rounded-id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||||
|
>
|
||||||
|
Add new menu item to begin creating menu
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MuiCardActions-root-id MenuItems-actions-id MuiCardActions-spacing-id"
|
class="MuiCardActions-root-id MenuItems-actions-id MuiCardActions-spacing-id"
|
||||||
|
|
Loading…
Reference in a new issue