mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
Temporary broken drag and drop
This commit is contained in:
parent
81cc8db0f5
commit
1d5141d073
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { ProductCategory } from '../../../models/product-category';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { ManageProductCategory } from './manage-product-category';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
import { DotsSixVertical } from 'phosphor-react';
|
||||
|
||||
interface ProductCategoriesItemProps {
|
||||
productCategories: Array<ProductCategory>,
|
||||
category: ProductCategory,
|
||||
isChild?: boolean,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
@ -14,9 +16,22 @@ interface ProductCategoriesItemProps {
|
||||
/**
|
||||
* Renders a draggable category item
|
||||
*/
|
||||
export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ productCategories, category, onSuccess, onError }) => {
|
||||
export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ productCategories, category, isChild, onSuccess, onError }) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition
|
||||
} = useSortable({ id: category.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={category.id} className='product-categories-item'>
|
||||
<div ref={setNodeRef} style={style} className={`product-categories-item ${isChild ? 'is-child' : ''}`}>
|
||||
<div className='itemInfo'>
|
||||
<p className='itemInfo-name'>{category.name}</p>
|
||||
<span className='itemInfo-count'>[count]</span>
|
||||
@ -33,7 +48,9 @@ export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ pr
|
||||
onSuccess={onSuccess} onError={onError} />
|
||||
</div>
|
||||
<div>
|
||||
<FabButton icon={<DotsSixVertical size={16} />} className='draghandle' />
|
||||
<button {...attributes} {...listeners} className='draghandle'>
|
||||
<DotsSixVertical size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { ProductCategory } from '../../../models/product-category';
|
||||
import { DotsSixVertical } from 'phosphor-react';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
import { ManageProductCategory } from './manage-product-category';
|
||||
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, DragMoveEvent } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { ProductCategoriesItem } from './product-categories-item';
|
||||
|
||||
interface ProductCategoriesTreeProps {
|
||||
productCategories: Array<ProductCategory>,
|
||||
onDnd: (list: Array<ProductCategory>) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
@ -13,30 +16,180 @@ interface ProductCategoriesTreeProps {
|
||||
/**
|
||||
* This component shows a tree list of all Product's Categories
|
||||
*/
|
||||
export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ productCategories, onSuccess, onError }) => {
|
||||
export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ productCategories, onDnd, onSuccess, onError }) => {
|
||||
const [categoriesList, setCategoriesList] = useImmer<ProductCategory[]>(productCategories);
|
||||
const [hiddenChildren, setHiddenChildren] = useState({});
|
||||
|
||||
// Initialize state from props, sorting list as a tree
|
||||
useEffect(() => {
|
||||
setCategoriesList(productCategories);
|
||||
}, [productCategories]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* On drag start
|
||||
*/
|
||||
const handleDragStart = ({ active }: DragMoveEvent) => {
|
||||
hideChildren(active.id, categoriesList.findIndex(el => el.id === active.id));
|
||||
const activeChildren = categoriesList.filter(c => c.parent_id === active.id);
|
||||
if (activeChildren.length) {
|
||||
setHiddenChildren({ [active.id]: activeChildren });
|
||||
const activeIndex = categoriesList.findIndex(el => el.id === active.id);
|
||||
const tmpList = [...categoriesList];
|
||||
tmpList.splice(activeIndex + 1, activeChildren.length);
|
||||
setCategoriesList(tmpList);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* On drag move
|
||||
*/
|
||||
const handleDragMove = ({ delta, over }: DragMoveEvent) => {
|
||||
console.log(findCategory(over.id).name);
|
||||
if (delta.x > 48) {
|
||||
console.log('Child');
|
||||
} else {
|
||||
console.log('Parent');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update categories list after an item was dropped
|
||||
*/
|
||||
|
||||
const handleDragEnd = ({ active, over }: DragMoveEvent) => {
|
||||
let newOrder = [...categoriesList];
|
||||
|
||||
// si déplacé sur une autre catégorie…
|
||||
if (active.id !== over.id) {
|
||||
// liste d'ids des catégories visibles
|
||||
const previousIdsOrder = over?.data.current.sortable.items;
|
||||
// index dans previousIdsOrder de la catégorie déplacée
|
||||
const oldIndex = active.data.current.sortable.index;
|
||||
// index dans previousIdsOrder de la catégorie de réception
|
||||
const newIndex = over.data.current.sortable.index;
|
||||
// liste de catégories mise à jour après le drop
|
||||
const newIdsOrder = arrayMove(previousIdsOrder, oldIndex, newIndex);
|
||||
// id du parent de la catégorie de réception
|
||||
const newParentId = categoriesList[newIndex].parent_id;
|
||||
|
||||
// nouvelle liste de catégories classées par newIdsOrder
|
||||
newOrder = newIdsOrder.map(sortedId => {
|
||||
// catégorie courante du map retrouvée grâce à l'id
|
||||
const categoryFromId = findCategory(sortedId);
|
||||
// si catégorie courante = catégorie déplacée…
|
||||
if (categoryFromId.id === active.id) {
|
||||
// maj du parent
|
||||
categoryFromId.parent_id = newParentId;
|
||||
}
|
||||
// retour de la catégorie courante
|
||||
return categoryFromId;
|
||||
});
|
||||
}
|
||||
// insert siblings back
|
||||
if (hiddenChildren[active.id]?.length) {
|
||||
newOrder.splice(over.data.current.sortable.index + 1, 0, ...hiddenChildren[active.id]);
|
||||
setHiddenChildren({ ...hiddenChildren, [active.id]: null });
|
||||
}
|
||||
onDnd(newOrder);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset state if the drag was canceled
|
||||
*/
|
||||
const handleDragCancel = ({ active }: DragMoveEvent) => {
|
||||
setHiddenChildren({ ...hiddenChildren, [active.id]: null });
|
||||
setCategoriesList(productCategories);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide children by their parent's id
|
||||
*/
|
||||
const hideChildren = (parentId, parentIndex) => {
|
||||
const children = findChildren(parentId);
|
||||
if (children?.length) {
|
||||
const tmpList = [...categoriesList];
|
||||
tmpList.splice(parentIndex + 1, children.length);
|
||||
setCategoriesList(tmpList);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a category by its id
|
||||
*/
|
||||
const findCategory = (id) => {
|
||||
return categoriesList.find(c => c.id === id);
|
||||
};
|
||||
/**
|
||||
* Find the children categories of a parent category by its id
|
||||
*/
|
||||
const findChildren = (id) => {
|
||||
const displayedChildren = categoriesList.filter(c => c.parent_id === id);
|
||||
if (displayedChildren.length) {
|
||||
return displayedChildren;
|
||||
}
|
||||
return hiddenChildren[id];
|
||||
};
|
||||
/**
|
||||
* Find category's status by its id
|
||||
* single | parent | child
|
||||
*/
|
||||
const categoryStatus = (id) => {
|
||||
const c = findCategory(id);
|
||||
if (!c.parent_id) {
|
||||
if (findChildren(id)?.length) {
|
||||
return 'parent';
|
||||
}
|
||||
return 'single';
|
||||
} else {
|
||||
return 'child';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate visual order into categories data positions
|
||||
*/
|
||||
const indexToPosition = (sortedIds: number[]) => {
|
||||
const sort = sortedIds.map(sortedId => categoriesList.find(el => el.id === sortedId));
|
||||
const newPositions = sort.map(c => {
|
||||
if (typeof c.parent_id === 'number') {
|
||||
const parentIndex = sort.findIndex(el => el.id === c.parent_id);
|
||||
const currentIndex = sort.findIndex(el => el.id === c.id);
|
||||
return { ...c, position: (currentIndex - parentIndex - 1) };
|
||||
}
|
||||
return c;
|
||||
});
|
||||
return newPositions;
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext items={categoriesList} strategy={verticalListSortingStrategy}>
|
||||
<div className='product-categories-tree'>
|
||||
{productCategories.map((category) => (
|
||||
<div key={category.id} className='product-categories-item'>
|
||||
<div className='itemInfo'>
|
||||
<p className='itemInfo-name'>{category.name}</p>
|
||||
<span className='itemInfo-count'>[count]</span>
|
||||
</div>
|
||||
<div className='action'>
|
||||
<div className='manage'>
|
||||
<ManageProductCategory action='update'
|
||||
{categoriesList
|
||||
.map((category) => (
|
||||
<ProductCategoriesItem key={category.id}
|
||||
productCategories={productCategories}
|
||||
productCategory={category}
|
||||
onSuccess={onSuccess} onError={onError} />
|
||||
<ManageProductCategory action='delete'
|
||||
productCategories={productCategories}
|
||||
productCategory={category}
|
||||
onSuccess={onSuccess} onError={onError} />
|
||||
</div>
|
||||
<FabButton icon={<DotsSixVertical size={16} />} className='draghandle' />
|
||||
</div>
|
||||
</div>
|
||||
category={category}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
isChild={typeof category.parent_id === 'number'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import ProductCategoryAPI from '../../../api/product-category';
|
||||
import { ManageProductCategory } from './manage-product-category';
|
||||
import { ProductCategoriesTree } from './product-categories-tree';
|
||||
import { FabAlert } from '../../base/fab-alert';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
import { IApplication } from '../../../models/application';
|
||||
import { Loader } from '../../base/loader';
|
||||
@ -41,28 +42,58 @@ const ProductCategories: React.FC<ProductCategoriesProps> = ({ onSuccess, onErro
|
||||
refreshCategories();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update state after drop
|
||||
*/
|
||||
const handleDnd = (data: ProductCategory[]) => {
|
||||
setProductCategories(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the list of categories
|
||||
*/
|
||||
const refreshCategories = () => {
|
||||
ProductCategoryAPI.index().then(data => {
|
||||
setProductCategories(data);
|
||||
// Translate ProductCategory.position to array index
|
||||
const sortedCategories = data
|
||||
.filter(c => !c.parent_id)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const childrenCategories = data
|
||||
.filter(c => typeof c.parent_id === 'number')
|
||||
.sort((a, b) => b.position - a.position);
|
||||
childrenCategories.forEach(c => {
|
||||
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
|
||||
sortedCategories.splice(parentIndex + 1, 0, c);
|
||||
});
|
||||
setProductCategories(sortedCategories);
|
||||
}).catch((error) => onError(error));
|
||||
};
|
||||
|
||||
/**
|
||||
* Save list's new order
|
||||
*/
|
||||
const handleSave = () => {
|
||||
// TODO: index to position -> send to API
|
||||
console.log('save order:', productCategories);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='product-categories'>
|
||||
<header>
|
||||
<h2>{t('app.admin.store.product_categories.title')}</h2>
|
||||
<div className='grpBtn'>
|
||||
<ManageProductCategory action='create'
|
||||
productCategories={productCategories}
|
||||
onSuccess={handleSuccess} onError={onError} />
|
||||
<FabButton className='saveBtn' onClick={handleSave}>Plop</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_categories.info" />
|
||||
</FabAlert>
|
||||
<ProductCategoriesTree
|
||||
productCategories={productCategories}
|
||||
onDnd={handleDnd}
|
||||
onSuccess={handleSuccess} onError={onError} />
|
||||
</div>
|
||||
);
|
||||
|
@ -22,6 +22,16 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.grpBtn {
|
||||
display: flex;
|
||||
& > *:not(:first-child) { margin-left: 2.4rem; }
|
||||
.saveBtn {
|
||||
background-color: var(--main);
|
||||
color: var(--gray-soft-lightest);
|
||||
border: none;
|
||||
&:hover { opacity: 0.75; }
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
@include title-lg;
|
||||
@ -40,8 +50,8 @@
|
||||
}
|
||||
|
||||
&-tree {
|
||||
& > *:not(:last-of-type) {
|
||||
margin-bottom: 1.6rem;
|
||||
& > *:not(:first-child) {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
}
|
||||
&-item {
|
||||
|
@ -47,6 +47,8 @@
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@claviska/jquery-minicolors": "^2.3.5",
|
||||
"@dnd-kit/core": "^6.0.5",
|
||||
"@dnd-kit/sortable": "^7.0.1",
|
||||
"@fortawesome/fontawesome-free": "5.14.0",
|
||||
"@lyracom/embedded-form-glue": "^0.3.3",
|
||||
"@stripe/react-stripe-js": "^1.4.0",
|
||||
|
36
yarn.lock
36
yarn.lock
@ -1488,6 +1488,37 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
|
||||
integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c"
|
||||
integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@^6.0.5":
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.5.tgz#5670ad0dcc83cd51dbf2fa8c6a5c8af4ac0c1989"
|
||||
integrity sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^3.2.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@^7.0.1":
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.1.tgz#99c6012bbab4d8bb726c0eef7b921a338c404fdb"
|
||||
integrity sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.0.tgz#b3e956ea63a1347c9d0e1316b037ddcc6140acda"
|
||||
integrity sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emotion/babel-plugin@^11.7.1":
|
||||
version "11.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95"
|
||||
@ -7452,6 +7483,11 @@ tslib@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
|
Loading…
x
Reference in New Issue
Block a user