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:
parent
5677ef3b6d
commit
605b3ec92c
@ -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} />
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user