1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(wip) drag and drop categories

This commit is contained in:
vincent 2022-08-15 14:52:09 +02:00
parent 5677ef3b6d
commit 605b3ec92c
3 changed files with 73 additions and 37 deletions

View File

@ -5,12 +5,12 @@ 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 { CaretDown, DotsSixVertical } from 'phosphor-react';
import { ArrowElbowDownRight, ArrowElbowLeftUp, CaretDown, DotsSixVertical } from 'phosphor-react';
interface ProductCategoriesItemProps {
productCategories: Array<ProductCategory>,
category: ProductCategory,
offset: boolean,
offset: 'up' | 'down' | null,
collapsed?: boolean,
handleCollapse?: (id: number) => void,
status: 'child' | 'single' | 'parent',
@ -39,13 +39,16 @@ export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ pr
return (
<div ref={setNodeRef} style={style}
className={`product-categories-item ${(status === 'child' && collapsed) ? 'is-collapsed' : ''}`}>
{(status === 'child' || offset) &&
<div className='offset'></div>
{((isDragging && offset) || status === 'child') &&
<div className='offset'>
{(offset === 'down') && <ArrowElbowDownRight size={32} weight="light" />}
{(offset === 'up') && <ArrowElbowLeftUp size={32} weight="light" />}
</div>
}
<div className="wrap">
<div className='wrap'>
<div className='itemInfo'>
{status === 'parent' && <div className='collapse-handle'>
<button className={collapsed ? '' : 'rotate'} onClick={() => handleCollapse(category.id)}>
<button className={collapsed || isDragging ? '' : 'rotate'} onClick={() => handleCollapse(category.id)}>
<CaretDown size={16} weight="bold" />
</button>
</div>}
@ -53,16 +56,18 @@ export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ pr
<span className='itemInfo-count'>[count]</span>
</div>
<div className='actions'>
<div className='manage'>
<ManageProductCategory action='update'
productCategories={productCategories}
productCategory={category}
onSuccess={onSuccess} onError={onError} />
<ManageProductCategory action='delete'
productCategories={productCategories}
productCategory={category}
onSuccess={onSuccess} onError={onError} />
</div>
{!isDragging &&
<div className='manage'>
<ManageProductCategory action='update'
productCategories={productCategories}
productCategory={category}
onSuccess={onSuccess} onError={onError} />
<ManageProductCategory action='delete'
productCategories={productCategories}
productCategory={category}
onSuccess={onSuccess} onError={onError} />
</div>
}
<div className='drag-handle'>
<button {...attributes} {...listeners}>
<DotsSixVertical size={20} />

View File

@ -21,17 +21,15 @@ interface ProductCategoriesTreeProps {
export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ productCategories, onDnd, onSuccess, onError }) => {
const [categoriesList, setCategoriesList] = useImmer<ProductCategory[]>(productCategories);
const [activeData, setActiveData] = useImmer<ActiveData>(initActiveData);
// TODO: type extractedChildren: {[parentId]: ProductCategory[]} ???
const [extractedChildren, setExtractedChildren] = useImmer({});
const [collapsed, setCollapsed] = useImmer<number[]>([]);
const [offset, setOffset] = useState<boolean>(false);
// Initialize state from props
useEffect(() => {
setCategoriesList(productCategories);
}, [productCategories]);
// Dnd Kit config
// @dnd-kit config
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@ -63,11 +61,31 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
* On drag move
*/
const handleDragMove = ({ delta, active, over }: DragMoveEvent) => {
if ((getStatus(active.id) === 'single' || getStatus(active.id) === 'child') && getStatus(over.id) === 'single') {
if (delta.x > 32) {
setOffset(true);
const activeStatus = getStatus(active.id);
if (activeStatus === 'single') {
if (Math.ceil(delta.x) > 32 && getStatus(over.id) !== 'child') {
setActiveData(draft => {
return { ...draft, offset: 'down' };
});
} else {
setOffset(false);
setActiveData(draft => {
return { ...draft, offset: null };
});
}
}
if (activeStatus === 'child') {
if (Math.ceil(delta.x) > 32 && getStatus(over.id) !== 'child') {
setActiveData(draft => {
return { ...draft, offset: 'down' };
});
} else if (Math.ceil(delta.x) < -32 && getStatus(over.id) === 'child') {
setActiveData(draft => {
return { ...draft, offset: 'up' };
});
} else {
setActiveData(draft => {
return { ...draft, offset: null };
});
}
}
};
@ -83,12 +101,13 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
// [A] Single |> [B] Single
if (getStatus(active.id) === 'single' && getStatus(over.id) === 'single') {
console.log('[A] Single |> [B] Single');
const newIdsOrder = arrayMove(currentIdsOrder, activeData.index, newIndex);
newOrder = newIdsOrder.map(sortedId => {
let category = getCategory(sortedId);
if (offset && sortedId === active.id && activeData.index < newIndex) {
if (activeData.offset === 'down' && sortedId === active.id && activeData.index < newIndex && active.id !== over.id) {
category = { ...category, parent_id: Number(over.id) };
} else if (activeData.offset === 'down' && sortedId === active.id && (activeData.index > newIndex || active.id === over.id)) {
category = { ...category, parent_id: getPreviousAdopter(over.id) };
}
return category;
});
@ -96,13 +115,14 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
// [A] Child |> [B] Single
if ((getStatus(active.id) === 'child') && getStatus(over.id) === 'single') {
console.log('[A] Child |> [B] Single');
const newIdsOrder = arrayMove(currentIdsOrder, activeData.index, newIndex);
newOrder = newIdsOrder.map(sortedId => {
let category = getCategory(sortedId);
if (offset && sortedId === active.id && activeData.index < newIndex) {
if (activeData.offset === 'down' && sortedId === active.id && activeData.index < newIndex) {
category = { ...category, parent_id: Number(over.id) };
} else if (sortedId === active.id && activeData.index < newIndex) {
} else if (activeData.offset === 'down' && sortedId === active.id && activeData.index > newIndex) {
category = { ...category, parent_id: getPreviousAdopter(over.id) };
} else if (sortedId === active.id) {
category = { ...category, parent_id: null };
}
return category;
@ -113,8 +133,8 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
if (getStatus(active.id) === 'single' || getStatus(active.id) === 'child') {
// [B] Parent
if (getStatus(over.id) === 'parent') {
const newIdsOrder = arrayMove(currentIdsOrder, activeData.index, newIndex);
if (activeData.index < newIndex) {
const newIdsOrder = arrayMove(currentIdsOrder, activeData.index, newIndex);
newOrder = newIdsOrder.map(sortedId => {
let category = getCategory(sortedId);
if (sortedId === active.id) {
@ -122,12 +142,13 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
}
return category;
});
} else {
const newIdsOrder = arrayMove(currentIdsOrder, activeData.index, newIndex);
} else if (activeData.index > newIndex) {
newOrder = newIdsOrder.map(sortedId => {
let category = getCategory(sortedId);
if (sortedId === active.id) {
if (sortedId === active.id && !activeData.offset) {
category = { ...category, parent_id: null };
} else if (sortedId === active.id && activeData.offset === 'down') {
category = { ...category, parent_id: getPreviousAdopter(over.id) };
}
return category;
});
@ -183,9 +204,8 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
// insert children back
newOrder = showChildren(active.id, newOrder, newIndex);
}
setActiveData(initActiveData);
onDnd(newOrder);
setOffset(false);
};
/**
@ -216,6 +236,16 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
return extractedChildren[id];
};
/**
* Get previous category that can have children
*/
const getPreviousAdopter = (overId) => {
const reversedList = [...categoriesList].reverse();
const dropIndex = reversedList.findIndex(c => c.id === overId);
const adopter = reversedList.find((c, index) => index > dropIndex && !c.parent_id)?.id;
return adopter || null;
};
/**
* Get category's status by its id
* child | single | parent
@ -285,7 +315,7 @@ export const ProductCategoriesTree: React.FC<ProductCategoriesTreeProps> = ({ pr
category={category}
onSuccess={onSuccess}
onError={onError}
offset={category.id === activeData.category?.id && activeData?.offset}
offset={category.id === activeData.category?.id ? activeData?.offset : null}
collapsed={collapsed.includes(category.id) || collapsed.includes(category.parent_id)}
handleCollapse={handleCollapse}
status={getStatus(category.id)}
@ -302,12 +332,12 @@ interface ActiveData {
category: ProductCategory,
status: 'child' | 'single' | 'parent',
children: ProductCategory[],
offset: boolean
offset: 'up' | 'down' | null
}
const initActiveData: ActiveData = {
index: null,
category: null,
status: null,
children: [],
offset: false
offset: null
};

View File

@ -64,6 +64,7 @@
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
&.is-child { margin-left: 4.8rem; }
.itemInfo {
display: flex;
align-items: center;