1
0
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:
vincent 2022-07-28 15:20:25 +02:00 committed by Sylvain
parent 81cc8db0f5
commit 1d5141d073
6 changed files with 286 additions and 37 deletions

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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 {

View File

@ -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",

View File

@ -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"