u
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
import { Droppable } from '@shopify/draggable';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
/**
|
||||
* Encapsulates @shopify/draggable Droppable logic for moving a category
|
||||
* into another category (tree drag-and-drop).
|
||||
*
|
||||
* Usage:
|
||||
* this.droppable = new CategoryDroppable(containerEl, { onMove });
|
||||
* this.droppable.activate();
|
||||
* this.droppable.destroy();
|
||||
*/
|
||||
export default class CategoryDroppable {
|
||||
#instance = null;
|
||||
#container = null;
|
||||
#onMove = null;
|
||||
#pendingDropId = null;
|
||||
#dropTargetEl = null;
|
||||
|
||||
constructor(container, { onMove }) {
|
||||
this.#container = container;
|
||||
this.#onMove = onMove;
|
||||
}
|
||||
|
||||
activate() {
|
||||
const el = this.#container;
|
||||
if (!el || this.#instance) return;
|
||||
|
||||
this.#pendingDropId = null;
|
||||
this.#instance = new Droppable(el, {
|
||||
draggable: '.sortable-item',
|
||||
dropzone: '.sortable-item',
|
||||
handle: '.droppable-handle',
|
||||
delay: 0,
|
||||
mirror: { constrainDimensions: true },
|
||||
classes: { 'droppable:active': '', 'droppable:occupied': '' },
|
||||
});
|
||||
this.#instance.on('drag:start', this.#handleTreeDragStart);
|
||||
this.#instance.on('drag:over', this.#handleDropOver);
|
||||
this.#instance.on('drag:out', this.#handleDropOut);
|
||||
this.#instance.on('droppable:dropped', this.#handleDropped);
|
||||
this.#instance.on('droppable:returned', this.#handleDroppedReturned);
|
||||
this.#instance.on('drag:stop', this.#handleDragStop);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.#instance) {
|
||||
this.#instance.destroy();
|
||||
this.#instance = null;
|
||||
}
|
||||
this.#pendingDropId = null;
|
||||
this.#clearDropTarget();
|
||||
document.body.classList.remove('tree-dragging');
|
||||
}
|
||||
|
||||
// --- private ---
|
||||
|
||||
#handleTreeDragStart = () => {
|
||||
document.body.classList.add('tree-dragging');
|
||||
};
|
||||
|
||||
#handleDropped = (event) => {
|
||||
event.cancel();
|
||||
};
|
||||
|
||||
#handleDropOver = (event) => {
|
||||
const tile =
|
||||
event?.over && typeof event.over.closest === 'function'
|
||||
? event.over.closest('.sortable-item')
|
||||
: null;
|
||||
if (!tile) return;
|
||||
const sourceId = Number(event?.source?.dataset?.id);
|
||||
const targetId = Number(tile.dataset.id);
|
||||
if (sourceId && targetId === sourceId) {
|
||||
this.#pendingDropId = null;
|
||||
return;
|
||||
}
|
||||
this.#clearDropTarget();
|
||||
this.#pendingDropId = targetId;
|
||||
this.#dropTargetEl = tile;
|
||||
tile.classList.add('tree-drop-target');
|
||||
};
|
||||
|
||||
#handleDropOut = () => {
|
||||
this.#pendingDropId = null;
|
||||
this.#clearDropTarget();
|
||||
};
|
||||
|
||||
#handleDroppedReturned = (event) => {
|
||||
this.#pendingDropId = null;
|
||||
this.#clearDropTarget();
|
||||
event.cancel();
|
||||
};
|
||||
|
||||
#handleDragStop = (event) => {
|
||||
document.body.classList.remove('tree-dragging');
|
||||
const categoryId = Number(event?.source?.dataset?.id);
|
||||
const targetId = this.#pendingDropId;
|
||||
this.#pendingDropId = null;
|
||||
this.#clearDropTarget();
|
||||
if (!categoryId || !targetId || categoryId === targetId) return;
|
||||
void this.#onMove({ categoryId, parentId: targetId })
|
||||
.unwrap()
|
||||
.catch(notifyError);
|
||||
};
|
||||
|
||||
#clearDropTarget = () => {
|
||||
if (this.#dropTargetEl) {
|
||||
this.#dropTargetEl.classList.remove('tree-drop-target');
|
||||
this.#dropTargetEl = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import CategorySortable from './CategorySortable';
|
||||
import CategoryDroppable from './CategoryDroppable';
|
||||
import CategoryPickerDialog from '../../components/article/CategoryPickerDialog';
|
||||
import { NewCategoryTile, CategoryBreadcrumbs } from '../../components/category';
|
||||
import { NewArticleTile } from '../../components/article';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -28,7 +28,12 @@ import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import posterminalAutomat from '../../automats/posterminalAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import { categoryDisplayName, getCategoryBreadcrumbs, getCategoryChildren } from '../../utils/categoryUtils';
|
||||
import {
|
||||
categoryDisplayName,
|
||||
getCategoryBreadcrumbs,
|
||||
getCategoryChildren,
|
||||
getCategoryDescendantIds,
|
||||
} from '../../utils/categoryUtils';
|
||||
|
||||
// ─── Helper selectors for this page ────────────────────────────────────────────
|
||||
function selectFromCategory(s) {
|
||||
@@ -76,10 +81,10 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
...selectFromCategory(categoryAutomat.state),
|
||||
...selectTerminals(posterminalAutomat.state),
|
||||
companyId: companyAutomat.state.current?.id ?? null,
|
||||
moveCategory: null,
|
||||
};
|
||||
this.gridRef = createRef();
|
||||
this.sortableModule = null;
|
||||
this.droppableModule = null;
|
||||
this.handleRenameSubmit = this.handleRenameSubmit.bind(this);
|
||||
this.handleDraftChange = this.handleDraftChange.bind(this);
|
||||
}
|
||||
@@ -96,7 +101,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
this.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
this.syncSortable();
|
||||
this.syncDroppable();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevState) {
|
||||
@@ -121,7 +125,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}
|
||||
if (this.state.renameMode !== prevState.renameMode) {
|
||||
this.syncSortable();
|
||||
this.syncDroppable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +133,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
this._unsubTerm?.();
|
||||
this._unsubCompany?.();
|
||||
this.sortableModule?.destroy();
|
||||
this.droppableModule?.destroy();
|
||||
}
|
||||
|
||||
#ensureModules = () => {
|
||||
@@ -142,12 +144,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
categoryAutomat.actions.reorderCategories({ categoryIds }),
|
||||
});
|
||||
}
|
||||
if (!this.droppableModule) {
|
||||
this.droppableModule = new CategoryDroppable(el, {
|
||||
onMove: ({ categoryId, parentId }) =>
|
||||
categoryAutomat.actions.moveCategory({ categoryId, parentId }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
syncSortable = () => {
|
||||
@@ -160,20 +156,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
syncDroppable = () => {
|
||||
this.#ensureModules();
|
||||
if (this.state.renameMode) {
|
||||
this.droppableModule?.destroy();
|
||||
this.droppableModule?.activate();
|
||||
} else {
|
||||
this.droppableModule?.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
handleMoveCategory = (category, direction) => {
|
||||
this.sortableModule?.handleMoveCategory(category.id, direction);
|
||||
};
|
||||
|
||||
ensureTreeLoaded = () => {
|
||||
const { companyId, treeStatus } = this.state;
|
||||
if (!companyId || treeStatus !== 'idle') return;
|
||||
@@ -252,10 +234,28 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleDeleteCategory = async (category) => {
|
||||
handleOpenMoveCategory = (category) => {
|
||||
this.setState({ moveCategory: category });
|
||||
};
|
||||
|
||||
handleCloseMoveCategory = () => {
|
||||
this.setState({ moveCategory: null });
|
||||
};
|
||||
|
||||
handleConfirmMoveCategory = async (targetParentId) => {
|
||||
const { moveCategory } = this.state;
|
||||
if (!moveCategory || targetParentId == null) return;
|
||||
if (String(targetParentId) === String(moveCategory.parentId)) {
|
||||
this.setState({ moveCategory: null });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await categoryAutomat.actions.deleteCategory(category.id);
|
||||
enqueueSnackbar('Kategorie gelöscht.', { variant: 'success' });
|
||||
await categoryAutomat.actions.moveCategory({
|
||||
categoryId: moveCategory.id,
|
||||
parentId: targetParentId,
|
||||
});
|
||||
enqueueSnackbar(`Kategorie „${moveCategory.name}“ verschoben.`, { variant: 'success' });
|
||||
this.setState({ moveCategory: null });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
@@ -286,6 +286,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
companyId,
|
||||
categories,
|
||||
rootCategoryId,
|
||||
treeStatus,
|
||||
articlesStatus,
|
||||
@@ -298,6 +299,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
renameMode,
|
||||
terminals,
|
||||
drafts,
|
||||
moveCategory,
|
||||
} = this.state;
|
||||
|
||||
if (!companyId) {
|
||||
@@ -387,6 +389,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
})
|
||||
.catch(notifyError);
|
||||
},
|
||||
onPlacementClick: () => this.handleOpenMoveCategory(item),
|
||||
onOpen: () => this.navigateToCategoryById(item.id),
|
||||
onPhotoUpload: () => {
|
||||
const input = document.createElement('input');
|
||||
@@ -439,13 +442,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}}
|
||||
/>
|
||||
),
|
||||
editBand: {
|
||||
onMoveLeft: () => this.handleMoveCategory(item, -1),
|
||||
onMoveRight: () => this.handleMoveCategory(item, 1),
|
||||
onDelete: () => this.handleDeleteCategory(item),
|
||||
canLeft: index > 0,
|
||||
canRight: index < childCategories.length - 1,
|
||||
},
|
||||
}
|
||||
: {
|
||||
label,
|
||||
@@ -462,13 +458,14 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
);
|
||||
})}
|
||||
|
||||
{!renameMode && articles.map((item) => (
|
||||
{!renameMode && articles.map((item, index) => (
|
||||
<ArticleTile
|
||||
key={item.id}
|
||||
label={item.name != null ? item.name.trim() : undefined}
|
||||
article={item}
|
||||
disabled={!articleSku(item)}
|
||||
onClick={() => this.handleArticleClick(item)}
|
||||
sx={index === 0 ? { gridColumnStart: 1 } : undefined}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -477,6 +474,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
<NewArticleTile
|
||||
parentId={currentCategoryId}
|
||||
onOpenArticle={this.handleArticleClick}
|
||||
sx={{ gridColumnStart: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -486,6 +484,21 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
<CircularProgress size={28} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{moveCategory && (
|
||||
<CategoryPickerDialog
|
||||
open={Boolean(moveCategory)}
|
||||
onClose={this.handleCloseMoveCategory}
|
||||
categories={categories}
|
||||
rootCategoryId={rootCategoryId}
|
||||
title={`„${categoryDisplayName(moveCategory.name, moveCategory.id, rootCategoryId)}“ verschieben nach…`}
|
||||
confirmLabel="Hierher verschieben"
|
||||
mode="single"
|
||||
selectedId={moveCategory.parentId}
|
||||
excludeIds={getCategoryDescendantIds(categories, moveCategory.id)}
|
||||
onSelectCategory={this.handleConfirmMoveCategory}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user