@griddo/ax 11.4.23 → 11.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/components/Fields/ComponentContainer/ComponentContainer.test.tsx +10 -52
  3. package/src/components/ActionMenu/index.tsx +3 -2
  4. package/src/components/Fields/CheckField/style.tsx +16 -16
  5. package/src/components/Fields/ComponentArray/MixableComponentArray/index.tsx +65 -49
  6. package/src/components/Fields/ComponentArray/SameComponentArray/index.tsx +89 -69
  7. package/src/components/Fields/ComponentContainer/index.tsx +11 -11
  8. package/src/components/Fields/ComponentContainer/style.tsx +12 -4
  9. package/src/components/Fields/ReferenceField/ItemList/Item/index.tsx +19 -9
  10. package/src/components/Fields/ReferenceField/ItemList/Item/style.tsx +24 -13
  11. package/src/components/Fields/ReferenceField/ItemList/index.tsx +59 -41
  12. package/src/containers/StructuredData/actions.tsx +1 -1
  13. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +71 -43
  14. package/src/modules/Categories/CategoriesList/CategoryItem/style.tsx +9 -1
  15. package/src/modules/Categories/CategoriesList/CategoryNav/index.tsx +0 -1
  16. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +1 -0
  17. package/src/modules/Categories/CategoriesList/helpers.tsx +135 -94
  18. package/src/modules/Categories/CategoriesList/index.tsx +153 -157
  19. package/src/modules/Categories/CategoriesList/style.tsx +0 -3
  20. package/src/modules/FileDrive/FolderTree/MenuItem/index.tsx +83 -0
  21. package/src/modules/FileDrive/FolderTree/MenuItem/style.tsx +69 -0
  22. package/src/modules/FileDrive/FolderTree/MenuList/index.tsx +26 -0
  23. package/src/modules/FileDrive/FolderTree/index.tsx +12 -58
  24. package/src/modules/FileDrive/FolderTree/style.tsx +6 -27
  25. package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +11 -17
  26. package/src/modules/Forms/FormCategoriesList/CategoryItem/style.tsx +4 -1
  27. package/src/modules/Forms/FormCategoriesList/index.tsx +68 -53
  28. package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +45 -16
  29. package/src/modules/Navigation/Menus/List/Table/Item/style.tsx +8 -3
  30. package/src/modules/Navigation/Menus/List/Table/helpers.tsx +132 -74
  31. package/src/modules/Navigation/Menus/List/Table/index.tsx +119 -86
  32. package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +11 -11
  33. package/src/modules/Settings/Integrations/IntegrationItem/style.tsx +9 -1
  34. package/src/modules/Settings/Integrations/index.tsx +59 -56
  35. package/src/types/index.tsx +4 -5
  36. package/src/modules/FileDrive/FolderTree/utils.tsx +0 -91
@@ -1,6 +1,10 @@
1
1
  import styled from "styled-components";
2
2
 
3
- const ItemWrapper = styled.div``;
3
+ const TreeWrapper = styled.div`
4
+ ul:not(:first-child) {
5
+ padding-left: ${(p) => p.theme.spacing.s};
6
+ }
7
+ `;
4
8
 
5
9
  const NewIconWrapper = styled.div`
6
10
  width: 16px;
@@ -28,37 +32,12 @@ const Item = styled.div<{ selected: boolean }>`
28
32
  }
29
33
  `;
30
34
 
31
- const Name = styled.div`
32
- ${(p) => p.theme.textStyle.uiS};
33
- color: ${(p) => p.theme.color.textMediumEmphasis};
34
- margin-left: ${(p) => p.theme.spacing.xs};
35
- white-space: nowrap;
36
- `;
37
-
38
35
  const RootName = styled.div`
39
36
  ${(p) => p.theme.textStyle.uiM};
40
37
  color: ${(p) => p.theme.color.textHighEmphasis};
41
38
  white-space: nowrap;
42
39
  `;
43
40
 
44
- const IconWrapper = styled.div`
45
- width: 16px;
46
- height: 16px;
47
- svg {
48
- path {
49
- fill: ${(p) => p.theme.color.iconMediumEmphasis};
50
- }
51
- }
52
- `;
53
-
54
- const ArrowWrapper = styled.div`
55
- width: 16px;
56
- height: 16px;
57
- margin-right: ${(p) => p.theme.spacing.xxs};
58
- white-space: nowrap;
59
- flex-shrink: 0;
60
- `;
61
-
62
41
  const Title = styled.div`
63
42
  ${(p) => p.theme.textStyle.uiM};
64
43
  color: ${(p) => p.theme.color.textMediumEmphasis};
@@ -66,4 +45,4 @@ const Title = styled.div`
66
45
  margin-bottom: ${(p) => p.theme.spacing.xs};
67
46
  `;
68
47
 
69
- export { ItemWrapper, Item, Name, RootName, IconWrapper, ArrowWrapper, Title, NewIconWrapper };
48
+ export { TreeWrapper, Item, RootName, Title, NewIconWrapper };
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { connect } from "react-redux";
3
- import { DraggableProvided } from "react-beautiful-dnd";
3
+ import { useSortable } from "@dnd-kit/sortable";
4
4
 
5
5
  import { formsActions } from "@ax/containers/Forms";
6
6
  import { useModal, usePermission } from "@ax/hooks";
@@ -12,17 +12,7 @@ import { DeleteModal } from "../atoms";
12
12
  import * as S from "./style";
13
13
 
14
14
  const CategoryItem = (props: ICategoryItemProps): JSX.Element => {
15
- const {
16
- category,
17
- isSelected,
18
- hoverCheck,
19
- isDragging,
20
- provided,
21
- isSiteView,
22
- deleteFormCategory,
23
- onChange,
24
- toggleToast,
25
- } = props;
15
+ const { category, isSelected, hoverCheck, isDragging, isSiteView, deleteFormCategory, onChange, toggleToast } = props;
26
16
 
27
17
  const { isOpen, toggleModal } = useModal();
28
18
  const { isOpen: isDeleteOpen, toggleModal: toggleDeleteModal } = useModal();
@@ -34,6 +24,10 @@ const CategoryItem = (props: ICategoryItemProps): JSX.Element => {
34
24
  const isAllowedToEditFormCategory = usePermission(editFormCategoryPermission);
35
25
  const isAllowedToDeleteFormCategory = usePermission(deleteFormCategoryPermission);
36
26
 
27
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
28
+ id: category.id,
29
+ });
30
+
37
31
  const { content } = category;
38
32
 
39
33
  const handleClick = () => !isDragging && isAllowedToEditFormCategory && toggleModal();
@@ -75,12 +69,13 @@ const CategoryItem = (props: ICategoryItemProps): JSX.Element => {
75
69
  return (
76
70
  <>
77
71
  <S.CategoryRow
78
- role="rowgroup"
79
72
  selected={isSelected}
80
73
  onClick={handleClick}
81
- ref={provided.innerRef}
82
- {...provided.draggableProps}
83
- {...provided.dragHandleProps}
74
+ ref={setNodeRef}
75
+ cssTransform={transform}
76
+ transition={transition}
77
+ {...listeners}
78
+ {...attributes}
84
79
  >
85
80
  <S.HandleCell>
86
81
  <S.IconWrapperDrag role="cell">
@@ -128,7 +123,6 @@ interface IProps {
128
123
  toggleToast(state: any): void;
129
124
  hoverCheck?: boolean;
130
125
  isDragging: boolean;
131
- provided: DraggableProvided;
132
126
  isSiteView: boolean;
133
127
  }
134
128
 
@@ -1,4 +1,5 @@
1
1
  import styled from "styled-components";
2
+ import { CSS, Transform } from "@dnd-kit/utilities";
2
3
 
3
4
  import { Cell, Row } from "@ax/components/TableList/TableItem/style";
4
5
  import { ActionMenu } from "@ax/components";
@@ -41,7 +42,9 @@ const StyledActionMenu = styled(ActionMenu)`
41
42
  margin-left: auto;
42
43
  `;
43
44
 
44
- const CategoryRow = styled(Row)`
45
+ const CategoryRow = styled(Row)<{ cssTransform: Transform | null; transition?: string }>`
46
+ transform: ${(p) => CSS.Transform.toString(p.cssTransform)};
47
+ transition: ${(p) => p.transition};
45
48
  &:hover {
46
49
  ${StyledActionMenu} {
47
50
  opacity: 1;
@@ -1,7 +1,17 @@
1
1
  import React, { useEffect, useState, useRef } from "react";
2
2
  import { connect } from "react-redux";
3
3
  import { useParams } from "react-router-dom";
4
- import { DragDropContext, Droppable, Draggable, DropResult, BeforeCapture } from "react-beautiful-dnd";
4
+ import {
5
+ closestCenter,
6
+ DndContext,
7
+ DragEndEvent,
8
+ DragStartEvent,
9
+ KeyboardSensor,
10
+ PointerSensor,
11
+ useSensor,
12
+ useSensors,
13
+ } from "@dnd-kit/core";
14
+ import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
5
15
 
6
16
  import { formsActions } from "@ax/containers/Forms";
7
17
  import { IRootState, FormCategory, FormCategoriesOrderParams } from "@ax/types";
@@ -48,6 +58,17 @@ const FormCategoriesList = (props: IProps): JSX.Element => {
48
58
  const isAllowedToCreateFormCategory = usePermission(createFormCategoryPermission);
49
59
  const isAllowedToDeleteFormCategory = usePermission(deleteFormCategoryPermission);
50
60
 
61
+ const sensors = useSensors(
62
+ useSensor(PointerSensor, {
63
+ activationConstraint: {
64
+ distance: 5,
65
+ },
66
+ }),
67
+ useSensor(KeyboardSensor, {
68
+ coordinateGetter: sortableKeyboardCoordinates,
69
+ })
70
+ );
71
+
51
72
  const catIds = categories.map((category) => category.id);
52
73
  const totalItems = categories.length;
53
74
 
@@ -116,55 +137,33 @@ const FormCategoriesList = (props: IProps): JSX.Element => {
116
137
  />
117
138
  );
118
139
 
119
- const ComponentList = React.memo(function ComponentList({ components }: any) {
120
- return components.map((category: FormCategory, i: number) => {
121
- const isItemSelected = isSelected(category.id);
122
- const isDragging = !!draggingId && draggingId !== category.id;
123
-
124
- return (
125
- <Draggable draggableId={`${category.id}`} index={i} key={category.id}>
126
- {(provided) => (
127
- <CategoryItem
128
- category={category}
129
- key={category.id}
130
- isSelected={isItemSelected}
131
- onChange={addToBulkSelection}
132
- toggleToast={toggleToast}
133
- hoverCheck={checkState.hoverCheck}
134
- isDragging={isDragging}
135
- provided={provided}
136
- isSiteView={isSiteView}
137
- />
138
- )}
139
- </Draggable>
140
- );
141
- });
142
- });
143
-
144
- const onDragEnd = async (result: DropResult) => {
145
- if (!result.destination || result.destination.index === result.source.index) {
146
- setDraggingId(null);
147
- return;
148
- }
140
+ const handleDragStart = (event: DragStartEvent) => {
141
+ const { active } = event;
142
+ setDraggingId(active.id as number);
143
+ };
149
144
 
150
- const newPosition = result.destination.index
151
- ? categories[result.destination.index - (result.destination.index > result.source.index ? 0 : 1)].position + 1
152
- : 1;
145
+ const handleOnDragEnd = async (event: DragEndEvent) => {
146
+ const { active, over } = event;
153
147
 
154
- const params: FormCategoriesOrderParams = {
155
- id: parseInt(result.draggableId),
156
- categoryType: category,
157
- position: newPosition,
158
- relatedSite: currentSiteID,
159
- };
148
+ if (over && active.id !== over.id) {
149
+ const oldIndex = categories.map((e) => e.id).indexOf(active.id as number);
150
+ const newIndex = categories.map((e) => e.id).indexOf(over.id as number);
160
151
 
161
- await orderFormCategory(params);
152
+ const newPosition = newIndex ? categories[newIndex - (newIndex > oldIndex ? 0 : 1)].position + 1 : 1;
153
+
154
+ const params: FormCategoriesOrderParams = {
155
+ id: active.id as number,
156
+ categoryType: category,
157
+ position: newPosition,
158
+ relatedSite: currentSiteID,
159
+ };
160
+
161
+ await orderFormCategory(params);
162
+ }
162
163
 
163
164
  setDraggingId(null);
164
165
  };
165
166
 
166
- const onBeforeCapture = (start: BeforeCapture) => setDraggingId(parseInt(start.draggableId));
167
-
168
167
  const isEmpty = totalItems === 0;
169
168
  const emptyProps = {
170
169
  message: isAllowedToCreateFormCategory
@@ -213,16 +212,32 @@ const FormCategoriesList = (props: IProps): JSX.Element => {
213
212
  <S.Notification>
214
213
  Reorder your category list by <strong>drag & drop</strong>.
215
214
  </S.Notification>
216
- <DragDropContext onDragEnd={onDragEnd} onBeforeCapture={onBeforeCapture}>
217
- <Droppable droppableId="formCategoriesList">
218
- {(provided) => (
219
- <div ref={provided.innerRef} {...provided.droppableProps}>
220
- <ComponentList components={categories} />
221
- {provided.placeholder}
222
- </div>
223
- )}
224
- </Droppable>
225
- </DragDropContext>
215
+ <DndContext
216
+ sensors={sensors}
217
+ collisionDetection={closestCenter}
218
+ onDragEnd={handleOnDragEnd}
219
+ onDragStart={handleDragStart}
220
+ >
221
+ <SortableContext items={catIds} strategy={verticalListSortingStrategy}>
222
+ {categories.map((category: FormCategory) => {
223
+ const isItemSelected = isSelected(category.id);
224
+ const isDragging = !!draggingId && draggingId !== category.id;
225
+
226
+ return (
227
+ <CategoryItem
228
+ category={category}
229
+ key={category.id}
230
+ isSelected={isItemSelected}
231
+ onChange={addToBulkSelection}
232
+ toggleToast={toggleToast}
233
+ hoverCheck={checkState.hoverCheck}
234
+ isDragging={isDragging}
235
+ isSiteView={isSiteView}
236
+ />
237
+ );
238
+ })}
239
+ </SortableContext>
240
+ </DndContext>
226
241
  </>
227
242
  )}
228
243
  </TableList>
@@ -1,5 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { connect } from "react-redux";
3
+ import { AnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
3
4
 
4
5
  import { menuActions } from "@ax/containers/Navigation";
5
6
  import { useModal, usePermission } from "@ax/hooks";
@@ -12,12 +13,6 @@ import { RemoveModal } from "./atoms";
12
13
 
13
14
  import * as S from "./style";
14
15
 
15
- const DragButton = ({ dragHandleProps }: any) => (
16
- <S.IconWrapperDrag {...dragHandleProps}>
17
- <Icon name="drag" size="16" />
18
- </S.IconWrapperDrag>
19
- );
20
-
21
16
  const Item = (props: IItemProps): JSX.Element => {
22
17
  const {
23
18
  item,
@@ -25,11 +20,12 @@ const Item = (props: IItemProps): JSX.Element => {
25
20
  resetItemValues,
26
21
  setItemValue,
27
22
  updateFormValue,
28
- icon,
29
- dragHandleProps,
30
23
  depth,
31
- isDragging,
24
+ indentationWidth,
25
+ collapsed,
26
+ onCollapse,
32
27
  } = props;
28
+
33
29
  const deleteItem = (item: IMenuItem, deleteChildren?: boolean) => deleteMenuItem(item.id, deleteChildren);
34
30
  const hasChildren = !!item.children.length;
35
31
 
@@ -50,6 +46,17 @@ const Item = (props: IItemProps): JSX.Element => {
50
46
  const [isOpenedSecond, setIsOpenedSecond] = useState(false);
51
47
  const { isOpen: isConfigOpen, toggleModal: toggleConfigModal } = useModal(false);
52
48
 
49
+ const animateLayoutChanges: AnimateLayoutChanges = ({ isSorting, wasDragging }) =>
50
+ isSorting || wasDragging ? false : true;
51
+
52
+ const { attributes, isDragging, listeners, setDraggableNodeRef, setDroppableNodeRef, transform, transition } =
53
+ useSortable({
54
+ id: item.id,
55
+ animateLayoutChanges,
56
+ });
57
+
58
+ const handleIconClick = (e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation();
59
+
53
60
  const toggleSecondaryPanel = () => setIsOpenedSecond(!isOpenedSecond);
54
61
  const resetValues = () => resetItemValues();
55
62
 
@@ -84,10 +91,24 @@ const Item = (props: IItemProps): JSX.Element => {
84
91
 
85
92
  return (
86
93
  <>
87
- <S.Container disabled={!isAllowedToMangageMenus}>
88
- <S.Component onClick={openModal} isGroup={isGroupingElement}>
94
+ <S.Container
95
+ disabled={!isAllowedToMangageMenus}
96
+ identationWidth={isDragging ? indentationWidth : indentationWidth * depth}
97
+ ref={setDroppableNodeRef}
98
+ >
99
+ <S.Component
100
+ onClick={openModal}
101
+ isGroup={isGroupingElement}
102
+ ref={setDraggableNodeRef}
103
+ cssTransform={transform}
104
+ transition={transition}
105
+ {...listeners}
106
+ {...attributes}
107
+ >
89
108
  <S.FlexWrapper>
90
- <DragButton dragHandleProps={dragHandleProps} />
109
+ <S.IconWrapperDrag>
110
+ <Icon name="drag" size="16" />
111
+ </S.IconWrapperDrag>
91
112
  {isGroupingElement && (
92
113
  <S.GroupIcon>
93
114
  <Icon name="unlink" size="16" />
@@ -96,7 +117,15 @@ const Item = (props: IItemProps): JSX.Element => {
96
117
  {item.label}
97
118
  </S.FlexWrapper>
98
119
  <S.FlexWrapper>
99
- <S.IconWrapper>{icon}</S.IconWrapper>
120
+ {onCollapse && (
121
+ <S.IconWrapper onClick={handleIconClick}>
122
+ {collapsed ? (
123
+ <IconAction icon="UpArrow" size="m" onClick={onCollapse} />
124
+ ) : (
125
+ <IconAction icon="DownArrow" size="m" onClick={onCollapse} />
126
+ )}
127
+ </S.IconWrapper>
128
+ )}
100
129
  {depth === 1 && hasChildren && isAllowedToMangageMenus && (
101
130
  <S.IconWrapper>
102
131
  <Tooltip content="Options view">
@@ -125,10 +154,10 @@ const Item = (props: IItemProps): JSX.Element => {
125
154
 
126
155
  interface IProps {
127
156
  item: IMenuItem;
128
- icon: JSX.Element;
129
- dragHandleProps: any;
130
157
  depth: number;
131
- isDragging: boolean;
158
+ indentationWidth: number;
159
+ collapsed: boolean;
160
+ onCollapse?: () => void;
132
161
  }
133
162
 
134
163
  interface IDispatchProps {
@@ -1,11 +1,14 @@
1
1
  import styled from "styled-components";
2
+ import { CSS, Transform } from "@dnd-kit/utilities";
3
+
2
4
  import { ActionMenu } from "@ax/components";
3
5
 
4
- const Container = styled.div<{ disabled: boolean }>`
6
+ const Container = styled.div<{ disabled: boolean; identationWidth: number }>`
5
7
  display: flex;
6
8
  align-items: center;
7
9
  width: 100%;
8
10
  pointer-events: ${(p) => (p.disabled ? "none" : "auto")};
11
+ padding-left: ${(p) => `${p.identationWidth}px`};
9
12
  `;
10
13
 
11
14
  const IconWrapper = styled.div`
@@ -27,7 +30,8 @@ const StyledActionMenu = styled(ActionMenu)`
27
30
  opacity: 0;
28
31
  `;
29
32
 
30
- const Component = styled.span<{ isGroup: boolean }>`
33
+ const Component = styled.span<{ isGroup: boolean; cssTransform: Transform | null; transition?: string }>`
34
+ ${(p) => p.theme.textStyle.fieldLabel};
31
35
  width: 100%;
32
36
  display: flex;
33
37
  align-items: center;
@@ -42,9 +46,10 @@ const Component = styled.span<{ isGroup: boolean }>`
42
46
  padding: 0 ${(p) => p.theme.spacing.s};
43
47
  border-radius: ${(p) => p.theme.radii.s};
44
48
  box-shadow: ${(p) => p.theme.shadow.shadowS};
45
- ${(p) => p.theme.textStyle.fieldLabel};
46
49
  text-align: left;
47
50
  cursor: pointer;
51
+ transform: ${(p) => CSS.Transform.toString(p.cssTransform)};
52
+ transition: ${(p) => p.transition};
48
53
 
49
54
  &::after {
50
55
  content: "";
@@ -1,92 +1,150 @@
1
- import { ItemId, TreeData, TreeItem } from "@atlaskit/tree";
2
- import { isEmptyObj } from "@ax/helpers";
3
1
  import { IMenuItem } from "@ax/types";
2
+ import type { UniqueIdentifier } from "@dnd-kit/core";
3
+ import { arrayMove } from "@dnd-kit/sortable";
4
4
 
5
- const normalizeMenu = (menu: IMenuItem[], tree: TreeData) => {
6
- const normalized: TreeData = {
7
- rootId: "root",
8
- items: {
9
- root: {
10
- id: "root",
11
- children: [],
12
- hasChildren: true,
13
- isExpanded: true,
14
- isChildrenLoading: false,
15
- },
16
- },
17
- };
18
-
19
- const previousMenu: IMenuItem[] = [];
20
- if (!isEmptyObj(tree.items)) {
21
- previousMenu.push(...formatMenu(tree));
22
- }
5
+ const flatten = (items: TreeItem[], parentId: UniqueIdentifier | null = null, depth = 0): FlattenedItem[] => {
6
+ return items.reduce<FlattenedItem[]>((acc, item, index) => {
7
+ return [...acc, { ...item, parentId, depth, index }, ...flatten(item.children || [], item.id, depth + 1)];
8
+ }, []);
9
+ };
23
10
 
24
- const isItemExpanded = (item: IMenuItem) => {
25
- if (!item) return true;
26
- return item.isExpanded;
27
- };
28
-
29
- const getElement = (item: IMenuItem, previousMenu: IMenuItem) => {
30
- normalized.items[item.id] = {
31
- ...item,
32
- children: [],
33
- isExpanded: isItemExpanded(previousMenu),
34
- };
35
-
36
- if (item.children && item.children.length > 0) {
37
- item.children.forEach((child: IMenuItem, index: number) => {
38
- normalized.items[item.id].children.push(child.id);
39
- getElement(child, previousMenu?.children[index]);
40
- });
41
- normalized.items[item.id].hasChildren = true;
42
- } else {
43
- normalized.items[item.id].hasChildren = false;
44
- }
45
- };
11
+ const flattenTree = (items: TreeItem[]): FlattenedItem[] => {
12
+ return flatten(items);
13
+ };
46
14
 
47
- menu.forEach((item: IMenuItem, index: number) => {
48
- normalized.items.root.children.push(item.id);
49
- getElement(item, previousMenu[index]);
50
- });
15
+ const buildTree = (flattenedItems: FlattenedItem[]): TreeItem[] => {
16
+ const root = { id: 0, children: [] };
17
+ const nodes: Record<string, Partial<TreeItem>> = { [root.id]: root };
18
+ const items = flattenedItems.map((item) => ({ ...item, children: [] })) as FlattenedItem[];
51
19
 
52
- return normalized;
53
- };
20
+ for (const item of items) {
21
+ const { id, children } = item;
22
+ const parentId = item.parentId ?? root.id;
23
+ const parent = nodes[parentId] ?? findItem(items, parentId);
54
24
 
55
- const getElementsFromTree = (tree: TreeData): TreeItem[] => {
56
- const { items } = tree;
57
- const elements: TreeItem[] = [];
58
- for (const item of Object.values(items)) {
59
- elements.push(item);
25
+ nodes[id] = { id, children };
26
+ parent.children && parent.children.push(item);
60
27
  }
61
- return elements;
62
- };
63
28
 
64
- const getChild = (child: ItemId, elements: any[]): IMenuItem => {
65
- const element = elements.find((element: IMenuItem) => element.id === child);
66
- const children = element?.children.map((child: ItemId) => getChild(child, elements));
67
- return { ...element, children };
29
+ return root.children;
68
30
  };
69
31
 
70
- const formatMenu = (tree: TreeData) => {
71
- const elements: TreeItem[] = getElementsFromTree(tree);
32
+ const findItem = (items: FlattenedItem[], itemId: UniqueIdentifier) => {
33
+ return items.find(({ id }) => id === itemId);
34
+ };
72
35
 
73
- const root: any = elements.find((element: TreeItem) => element.id === "root");
74
- const formattedMenu = root.children.map((child: ItemId) => {
75
- return getChild(child, elements);
36
+ function getDragDepth(offset: number, indentationWidth: number) {
37
+ return Math.round(offset / indentationWidth);
38
+ }
39
+
40
+ export function getProjection(
41
+ items: FlattenedItem[],
42
+ activeId: UniqueIdentifier,
43
+ overId: UniqueIdentifier,
44
+ dragOffset: number,
45
+ indentationWidth: number
46
+ ) {
47
+ const overItemIndex = items.findIndex(({ id }) => id === overId);
48
+ const activeItemIndex = items.findIndex(({ id }) => id === activeId);
49
+ const activeItem = items[activeItemIndex];
50
+ const newItems = arrayMove(items, activeItemIndex, overItemIndex);
51
+ const previousItem = newItems[overItemIndex - 1];
52
+ const nextItem = newItems[overItemIndex + 1];
53
+ const dragDepth = getDragDepth(dragOffset, indentationWidth);
54
+ const projectedDepth = activeItem.depth + dragDepth;
55
+ const maxDepth = getMaxDepth({
56
+ previousItem,
76
57
  });
58
+ const minDepth = getMinDepth({ nextItem });
59
+ let depth = projectedDepth;
77
60
 
78
- return formattedMenu;
79
- };
61
+ if (projectedDepth >= maxDepth) {
62
+ depth = maxDepth;
63
+ } else if (projectedDepth < minDepth) {
64
+ depth = minDepth;
65
+ }
66
+
67
+ return { depth, maxDepth, minDepth, parentId: getParentId() };
68
+
69
+ function getParentId() {
70
+ if (depth === 0 || !previousItem) {
71
+ return null;
72
+ }
73
+
74
+ if (depth === previousItem.depth) {
75
+ return previousItem.parentId;
76
+ }
77
+
78
+ if (depth > previousItem.depth) {
79
+ return previousItem.id;
80
+ }
81
+
82
+ const newParent = newItems
83
+ .slice(0, overItemIndex)
84
+ .reverse()
85
+ .find((item) => item.depth === depth)?.parentId;
80
86
 
81
- const formatItem = (item: any, tree: TreeData) => {
82
- const elements = getElementsFromTree(tree);
87
+ return newParent ?? null;
88
+ }
89
+ }
90
+
91
+ function getMinDepth({ nextItem }: { nextItem: FlattenedItem }) {
92
+ if (nextItem) {
93
+ return nextItem.depth;
94
+ }
83
95
 
84
- const formattedItem = {
85
- ...item,
86
- children: item.children.map((child: ItemId) => getChild(child, elements)),
87
- };
96
+ return 0;
97
+ }
98
+
99
+ function getMaxDepth({ previousItem }: { previousItem: FlattenedItem }) {
100
+ if (previousItem) {
101
+ return previousItem.depth + 1;
102
+ }
103
+
104
+ return 0;
105
+ }
106
+
107
+ const removeChildrenOf = (items: FlattenedItem[], ids: UniqueIdentifier[]) => {
108
+ const excludeParentIds = [...ids];
109
+
110
+ return items.filter((item) => {
111
+ if (item.parentId && excludeParentIds.includes(item.parentId)) {
112
+ if (item.children && item.children.length) {
113
+ excludeParentIds.push(item.id);
114
+ }
115
+ return false;
116
+ }
88
117
 
89
- return formattedItem;
118
+ return true;
119
+ });
90
120
  };
91
121
 
92
- export { normalizeMenu, formatMenu, formatItem };
122
+ export function setProperty<T extends keyof TreeItem>(
123
+ items: TreeItem[],
124
+ id: UniqueIdentifier,
125
+ property: T,
126
+ setter: (value: TreeItem[T]) => TreeItem[T]
127
+ ) {
128
+ for (const item of items) {
129
+ if (item.id === id) {
130
+ item[property] = setter(item[property]);
131
+ continue;
132
+ }
133
+
134
+ if (item.children && item.children.length) {
135
+ item.children = setProperty(item.children || [], id, property, setter);
136
+ }
137
+ }
138
+
139
+ return [...items];
140
+ }
141
+
142
+ export interface FlattenedItem extends IMenuItem {
143
+ parentId: UniqueIdentifier | null;
144
+ depth: number;
145
+ index: number;
146
+ }
147
+
148
+ export type TreeItem = IMenuItem;
149
+
150
+ export { flattenTree, buildTree, removeChildrenOf };