u
This commit is contained in:
10
TODO.md
10
TODO.md
@@ -15,6 +15,7 @@
|
|||||||
| Kategorien setzen (replace-all) | `PUT /api/article/:sku/categories` | `{ categoryIds: number[] }` | `{ article }` (Detail) |
|
| Kategorien setzen (replace-all) | `PUT /api/article/:sku/categories` | `{ categoryIds: number[] }` | `{ article }` (Detail) |
|
||||||
| Kategorie umbenennen | `PATCH /api/category/:id` | `{ name }` | `{ category: { id, name, slug } }` |
|
| Kategorie umbenennen | `PATCH /api/category/:id` | `{ name }` | `{ category: { id, name, slug } }` |
|
||||||
| Kategorie-Reihenfolge | `PUT /api/category/order` | `{ parentId, categoryIds: number[] }` | `{ categoryIds }` |
|
| Kategorie-Reihenfolge | `PUT /api/category/order` | `{ parentId, categoryIds: number[] }` | `{ categoryIds }` |
|
||||||
|
| **Kategorie verschieben** (VORSCHLAG, offen) | `PATCH /api/category/:id` | `{ parentId: number }` | `{ category: { id, parentId, name, slug } }` |
|
||||||
| Detail liefert neue Felder | `GET /api/article/:sku` | – | `{ article }` mit `eans`, `categoryIds`, `weight`, `brand` |
|
| Detail liefert neue Felder | `GET /api/article/:sku` | – | `{ article }` mit `eans`, `categoryIds`, `weight`, `brand` |
|
||||||
|
|
||||||
`Brand` = `{ id, name, gsprpinfo \| null }`.
|
`Brand` = `{ id, name, gsprpinfo \| null }`.
|
||||||
@@ -43,6 +44,15 @@ Alle PUT/PATCH-Endpoints der Artikel liefern dasselbe Shape wie `GET /api/articl
|
|||||||
- **Kategorie-Reihenfolge (Drag & Drop)**: nach Sortierung → `PUT /api/category/order` mit `{ parentId, categoryIds }` (die neuen Kind-IDs in gewünschter Reihenfolge). `parentId` ist der aktuelle Elternknoten. `400 Invalid category order` bei falscher Menge/Duplikaten; nur Children desselben Elternteils angeben.
|
- **Kategorie-Reihenfolge (Drag & Drop)**: nach Sortierung → `PUT /api/category/order` mit `{ parentId, categoryIds }` (die neuen Kind-IDs in gewünschter Reihenfolge). `parentId` ist der aktuelle Elternknoten. `400 Invalid category order` bei falscher Menge/Duplikaten; nur Children desselben Elternteils angeben.
|
||||||
- Nach jedem Save das zurückgegebene Detail-Objekt in den Store übernehmen.
|
- Nach jedem Save das zurückgegebene Detail-Objekt in den Store übernehmen.
|
||||||
|
|
||||||
|
### Kategorie in Unterkategorie ablegen (Tree-Drop) — [VORSCHLAG an dashback, offen]
|
||||||
|
- **UI fertig**: oberes Drittel der Edit-Kachel zeigt links Move-Icon (`DragHandle`), rechts Tree-Icon (`AccountTree`). Beide sind Drag-Handles (`handle: .sortable-handle, .sortable-tree`). Ziehen am **Tree**-Icon sortiert die Geschwister **nicht** neu um (`handleSort` ignoriert Tree-Draggs).
|
||||||
|
- **Backend-Vorschlag** (fehlt noch in `dashback/docs/api/routes/`):
|
||||||
|
- `PATCH /api/category/:id` erweitern: optional `parentId` im Body, zusätzlich zu `name`.
|
||||||
|
- Request: `{ parentId: number }` → verschiebt die Kategorie unter den neuen Eltern. `{ name }` → nur Umbenennen (bestehend).
|
||||||
|
- Response: `{ category: { id, parentId, name, slug } }`.
|
||||||
|
- Erforderliche Validierung: Ziel-Eltern existiert (404), kein Zyklus/kein Verschieben in sich selbst oder eigene Nachkommen (400), Company-Scope (403).
|
||||||
|
- **Bis der Vertrag feststeht**: keine Frontend-Implementierung des Drops (Regel `no-assumed-api`).
|
||||||
|
|
||||||
### Fehlerbehandlung
|
### Fehlerbehandlung
|
||||||
- Fehler als Toast/Formular-Fehler abbilden:
|
- Fehler als Toast/Formular-Fehler abbilden:
|
||||||
- `Brand in use` (409), `Invalid eans` / `Invalid weight` / `Invalid categoryIds` / `Invalid brand` (400)
|
- `Brand in use` (409), `Invalid eans` / `Invalid weight` / `Invalid categoryIds` / `Invalid brand` (400)
|
||||||
|
|||||||
@@ -1,8 +1,161 @@
|
|||||||
import FolderIcon from '@mui/icons-material/Folder';
|
import { Component } from 'react';
|
||||||
import { Tile } from './Tile';
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||||
|
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||||
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import { Tile, tileBaseSx } from './Tile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top edit band (upper third) with a move handle on the left and a tree
|
||||||
|
* (drop-into-subcategory) handle on the right.
|
||||||
|
*/
|
||||||
|
function TopBand() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: '16.66%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className="sortable-handle"
|
||||||
|
size="small"
|
||||||
|
aria-label="Verschieben"
|
||||||
|
sx={{ cursor: 'grab', '&:active': { cursor: 'grabbing' } }}
|
||||||
|
>
|
||||||
|
<DragHandleIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
className="sortable-tree"
|
||||||
|
size="small"
|
||||||
|
aria-label="In Unterkategorie ablegen"
|
||||||
|
sx={{ cursor: 'grab', '&:active': { cursor: 'grabbing' } }}
|
||||||
|
>
|
||||||
|
<AccountTreeIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared grey name band spanning the middle third.
|
||||||
|
*/
|
||||||
|
function NameBand({ label, nameInput }) {
|
||||||
|
if (label == null) return null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '33.33%',
|
||||||
|
bottom: '33.33%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'grey.300',
|
||||||
|
px: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nameInput ?? (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight={600}
|
||||||
|
noWrap
|
||||||
|
sx={{ lineHeight: 1.2, color: 'text.primary' }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom edit band with left/right chevron reorder buttons and a center delete.
|
||||||
|
*/
|
||||||
|
function EditBand({ onMoveLeft, onMoveRight, onDelete, canLeft, canRight }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: '16.66%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 0.5,
|
||||||
|
bgcolor: 'grey.400',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton className="sortable-handle-btn" size="small" aria-label="Nach links verschieben" disabled={!canLeft} onClick={onMoveLeft}>
|
||||||
|
<ChevronLeftIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton className="sortable-handle-btn" size="small" aria-label="Löschen" onClick={onDelete}>
|
||||||
|
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton className="sortable-handle-btn" size="small" aria-label="Nach rechts verschieben" disabled={!canRight} onClick={onMoveRight}>
|
||||||
|
<ChevronRightIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category tile (normal mode): no icon, click navigates to the category.
|
||||||
|
*/
|
||||||
export class CategoryTile extends Tile {
|
export class CategoryTile extends Tile {
|
||||||
renderIcon() {
|
renderIcon() {
|
||||||
return <FolderIcon color="primary" sx={{ fontSize: 40 }} />;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabel() {
|
||||||
|
const { label, nameInput } = this.props;
|
||||||
|
return <NameBand label={label} nameInput={nameInput} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category tile for edit mode: no ButtonBase (no ripple, no click navigation),
|
||||||
|
* always shows the bottom edit band with chevron reorder/delete actions.
|
||||||
|
*/
|
||||||
|
export class CategoryEditTile extends Component {
|
||||||
|
render() {
|
||||||
|
const { label, nameInput, editBand, sx, ...rest } = this.props;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...rest}
|
||||||
|
sx={{
|
||||||
|
...tileBaseSx,
|
||||||
|
cursor: 'default',
|
||||||
|
position: 'relative',
|
||||||
|
'&:hover': { bgcolor: 'background.paper', boxShadow: 0 },
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TopBand />
|
||||||
|
<NameBand label={label} nameInput={nameInput} />
|
||||||
|
<EditBand
|
||||||
|
onMoveLeft={editBand.onMoveLeft}
|
||||||
|
onMoveRight={editBand.onMoveRight}
|
||||||
|
onDelete={editBand.onDelete}
|
||||||
|
canLeft={editBand.canLeft}
|
||||||
|
canRight={editBand.canRight}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export class Tile extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderExtra() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { sx, disabled } = this.props;
|
const { sx, disabled } = this.props;
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +74,7 @@ export class Tile extends Component {
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
sx={{
|
sx={{
|
||||||
...tileBaseSx,
|
...tileBaseSx,
|
||||||
|
position: 'relative',
|
||||||
...(disabled
|
...(disabled
|
||||||
? { opacity: 0.5, pointerEvents: 'none' }
|
? { opacity: 0.5, pointerEvents: 'none' }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -79,6 +84,7 @@ export class Tile extends Component {
|
|||||||
{this.renderIcon()}
|
{this.renderIcon()}
|
||||||
{this.renderLabel()}
|
{this.renderLabel()}
|
||||||
{this.renderSecondary()}
|
{this.renderSecondary()}
|
||||||
|
{this.renderExtra()}
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { TILE_SIZE, Tile, tileBaseSx } from './Tile';
|
export { TILE_SIZE, Tile, tileBaseSx } from './Tile';
|
||||||
export { BackTile } from './BackTile';
|
export { BackTile } from './BackTile';
|
||||||
export { CategoryTile } from './CategoryTile';
|
export { CategoryTile, CategoryEditTile } from './CategoryTile';
|
||||||
export { ArticleTile } from './ArticleTile';
|
export { ArticleTile } from './ArticleTile';
|
||||||
export { ActionTile } from './ActionTile';
|
export { ActionTile } from './ActionTile';
|
||||||
|
|||||||
@@ -22,3 +22,8 @@ body {
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tree-Drag (In-Unterkategorie-ablegen): kein Ghost/Shadow, Kacheln bleiben fest */
|
||||||
|
.sortable-tree-ghost {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { notifyError } from '../../utils/notify';
|
|||||||
import {
|
import {
|
||||||
ArticleTile,
|
ArticleTile,
|
||||||
BackTile,
|
BackTile,
|
||||||
|
CategoryEditTile,
|
||||||
CategoryTile,
|
CategoryTile,
|
||||||
TILE_SIZE,
|
TILE_SIZE,
|
||||||
tileBaseSx,
|
|
||||||
} from '../../components/tiles';
|
} from '../../components/tiles';
|
||||||
import withCategoryRoute from './withCategoryRoute';
|
import withCategoryRoute from './withCategoryRoute';
|
||||||
import { artikelDetailPath, articleSku } from './articleRoute';
|
import { artikelDetailPath, articleSku } from './articleRoute';
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from './categoryRoute';
|
} from './categoryRoute';
|
||||||
import {
|
import {
|
||||||
categoryDisplayName,
|
categoryDisplayName,
|
||||||
|
deleteCategory,
|
||||||
loadCategoryArticles,
|
loadCategoryArticles,
|
||||||
loadCategoryTree,
|
loadCategoryTree,
|
||||||
navigateToCategory,
|
navigateToCategory,
|
||||||
@@ -57,6 +58,7 @@ class ArtikelKategorienPage extends Component {
|
|||||||
this.gridRef = createRef();
|
this.gridRef = createRef();
|
||||||
this.sortable = null;
|
this.sortable = null;
|
||||||
this.checkpointOrder = null;
|
this.checkpointOrder = null;
|
||||||
|
this.treeDragId = null;
|
||||||
this.handleRenameSubmit = this.handleRenameSubmit.bind(this);
|
this.handleRenameSubmit = this.handleRenameSubmit.bind(this);
|
||||||
this.handleDraftChange = this.handleDraftChange.bind(this);
|
this.handleDraftChange = this.handleDraftChange.bind(this);
|
||||||
}
|
}
|
||||||
@@ -103,6 +105,9 @@ class ArtikelKategorienPage extends Component {
|
|||||||
|
|
||||||
syncSortable = () => {
|
syncSortable = () => {
|
||||||
if (this.props.renameMode) {
|
if (this.props.renameMode) {
|
||||||
|
// Erst alte Instanz sicher los, falls irgendwann noch vorhanden,
|
||||||
|
// damit Sortable die aktuelle (Edit-)DOM-Struktur bindet.
|
||||||
|
this.destroySortable();
|
||||||
this.activateSortable();
|
this.activateSortable();
|
||||||
} else {
|
} else {
|
||||||
this.destroySortable();
|
this.destroySortable();
|
||||||
@@ -112,18 +117,26 @@ class ArtikelKategorienPage extends Component {
|
|||||||
activateSortable = () => {
|
activateSortable = () => {
|
||||||
const el = this.gridRef.current;
|
const el = this.gridRef.current;
|
||||||
if (!el || this.sortable) return;
|
if (!el || this.sortable) return;
|
||||||
|
// Capture-Listener im Grid: läuft vor SortableJS' Listener und vor Reacts
|
||||||
|
// synthetischem mousedown — erkennt die Drag-Quelle (Tree vs. Move) zuverlässig.
|
||||||
|
el.addEventListener('mousedown', this.handleGridMouseDownCapture, true);
|
||||||
this.sortable = Sortable.create(el, {
|
this.sortable = Sortable.create(el, {
|
||||||
draggable: '.sortable-item',
|
draggable: '.sortable-item',
|
||||||
|
handle: '.sortable-handle, .sortable-tree',
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
filter: '.not-sortable',
|
filter: '.not-sortable, .sortable-handle-btn',
|
||||||
onStart: this.handleSortStart,
|
onStart: this.handleSortStart,
|
||||||
onEnd: this.handleSort,
|
onEnd: this.handleSort,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
destroySortable = () => {
|
destroySortable = () => {
|
||||||
|
const el = this.gridRef.current;
|
||||||
|
if (el) {
|
||||||
|
el.removeEventListener('mousedown', this.handleGridMouseDownCapture, true);
|
||||||
|
}
|
||||||
if (this.sortable) {
|
if (this.sortable) {
|
||||||
this.sortable.destroy();
|
this.sortable.destroy();
|
||||||
this.sortable = null;
|
this.sortable = null;
|
||||||
@@ -140,15 +153,50 @@ class ArtikelKategorienPage extends Component {
|
|||||||
.map(Number);
|
.map(Number);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSortStart = () => {
|
handleGridMouseDownCapture = (event) => {
|
||||||
|
// Native Capture-Phase: läuft VOR SortableJS' Bubbling-Listener und vor
|
||||||
|
// Reacts synthetischem Event. Das echte DOM-Ziel wird so zuverlässig erkannt.
|
||||||
|
const target = event.target;
|
||||||
|
if (!target || typeof target.closest !== 'function') return;
|
||||||
|
const treeEl = target.closest('.sortable-tree');
|
||||||
|
const isTree = treeEl != null;
|
||||||
|
this.treeDragId = isTree
|
||||||
|
? Number(treeEl?.closest('.sortable-item')?.dataset?.id)
|
||||||
|
: null;
|
||||||
|
// Beim Tree-Drag Sortable-Deaktivierung (sort=false) setzen, damit nichts umsortiert.
|
||||||
|
this.sortable?.option('sort', !isTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSortStart = (evt) => {
|
||||||
|
const isTree = this.treeDragId != null;
|
||||||
|
if (isTree) {
|
||||||
|
// Kein Ghost/Shadow — Kacheln bleiben fest, nur das Ziel wird sichtbar.
|
||||||
|
this.sortable?.option('ghostClass', 'sortable-tree-ghost');
|
||||||
|
this.sortable?.option('dragClass', '');
|
||||||
|
} else {
|
||||||
|
this.sortable?.option('ghostClass', 'sortable-ghost');
|
||||||
|
this.sortable?.option('dragClass', 'sortable-drag');
|
||||||
|
}
|
||||||
// Snapshot der Reihenfolge vor dem ersten Drag.
|
// Snapshot der Reihenfolge vor dem ersten Drag.
|
||||||
this.checkpointOrder = this.sortableIds();
|
this.checkpointOrder = this.sortableIds();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSort = () => {
|
handleSort = () => {
|
||||||
const ids = this.sortableIds();
|
// Sortable-Klassen für den nächsten Drag zurücksetzen.
|
||||||
|
this.sortable?.option('ghostClass', 'sortable-ghost');
|
||||||
|
this.sortable?.option('dragClass', 'sortable-drag');
|
||||||
|
this.sortable?.option('sort', true);
|
||||||
|
// Tree-Drag (in Unterkategorie ablegen): Geschwister-Reihenfolge nicht ändern.
|
||||||
const before = this.checkpointOrder ?? [];
|
const before = this.checkpointOrder ?? [];
|
||||||
this.checkpointOrder = null;
|
this.checkpointOrder = null;
|
||||||
|
if (this.treeDragId != null) {
|
||||||
|
// Kacheln visuell an Original-Position zurückdrehen — kein Reorder-Call.
|
||||||
|
this.treeDragId = null;
|
||||||
|
if (before.length) this.revertDomOrder(before);
|
||||||
|
this.clearSortableState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ids = this.sortableIds();
|
||||||
if (
|
if (
|
||||||
before.length === 0 ||
|
before.length === 0 ||
|
||||||
before.length !== ids.length ||
|
before.length !== ids.length ||
|
||||||
@@ -164,9 +212,36 @@ class ArtikelKategorienPage extends Component {
|
|||||||
// Reihenfolge im DOM zurückdrehen, ohne React neu zu rendern —
|
// Reihenfolge im DOM zurückdrehen, ohne React neu zu rendern —
|
||||||
// die Sortable-Instanz bleibt dadurch funktionsfähig.
|
// die Sortable-Instanz bleibt dadurch funktionsfähig.
|
||||||
this.revertDomOrder(before);
|
this.revertDomOrder(before);
|
||||||
|
this.clearSortableState();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt verbliebene SortableJS-Drag/Chosen/Ghost-Klassen von allen
|
||||||
|
* Kacheln. Nötig, weil revertDomOrder die DOM-Knoten umsortiert und
|
||||||
|
* SortableJS sein internes Cleanup dann ggf. nicht mehr korrekt anwendet.
|
||||||
|
*/
|
||||||
|
clearSortableState = () => {
|
||||||
|
const container = this.gridRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const classes = ['sortable-chosen', 'sortable-drag', 'sortable-ghost', 'sortable-tree-ghost'];
|
||||||
|
for (const el of container.querySelectorAll('.sortable-item')) {
|
||||||
|
el.classList.remove(...classes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMoveCategory = (category, direction) => {
|
||||||
|
const ids = this.sortableIds();
|
||||||
|
const index = ids.indexOf(category.id);
|
||||||
|
if (index === -1) return;
|
||||||
|
const target = index + direction;
|
||||||
|
if (target < 0 || target >= ids.length) return; // nicht verschiebbar
|
||||||
|
const next = [...ids];
|
||||||
|
next[index] = ids[target];
|
||||||
|
next[target] = category.id;
|
||||||
|
void this.props.reorderCategories({ categoryIds: next }).unwrap().catch(notifyError);
|
||||||
|
};
|
||||||
|
|
||||||
revertDomOrder = (order) => {
|
revertDomOrder = (order) => {
|
||||||
const container = this.gridRef.current;
|
const container = this.gridRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -260,6 +335,15 @@ class ArtikelKategorienPage extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleDeleteCategory = async (category) => {
|
||||||
|
try {
|
||||||
|
await this.props.deleteCategory(category.id).unwrap();
|
||||||
|
enqueueSnackbar('Kategorie gelöscht.', { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleRetryTree = () => {
|
handleRetryTree = () => {
|
||||||
if (this.props.companyId) this.props.loadCategoryTree();
|
if (this.props.companyId) this.props.loadCategoryTree();
|
||||||
};
|
};
|
||||||
@@ -352,7 +436,7 @@ class ArtikelKategorienPage extends Component {
|
|||||||
<BackTile onClick={this.handleNavigateBack} />
|
<BackTile onClick={this.handleNavigateBack} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{childCategories.map((item) => {
|
{childCategories.map((item, index) => {
|
||||||
const label = categoryDisplayName(item.name, item.id, rootCategoryId);
|
const label = categoryDisplayName(item.name, item.id, rootCategoryId);
|
||||||
if (renameMode) {
|
if (renameMode) {
|
||||||
const draftName =
|
const draftName =
|
||||||
@@ -360,44 +444,46 @@ class ArtikelKategorienPage extends Component {
|
|||||||
? this.state.drafts[item.id]
|
? this.state.drafts[item.id]
|
||||||
: label;
|
: label;
|
||||||
return (
|
return (
|
||||||
<Box
|
<CategoryEditTile
|
||||||
key={item.id}
|
key={item.id}
|
||||||
data-id={String(item.id)}
|
data-id={String(item.id)}
|
||||||
className="sortable-item"
|
className="sortable-item"
|
||||||
component="form"
|
label={draftName}
|
||||||
onSubmit={(event) => {
|
nameInput={
|
||||||
event.preventDefault();
|
|
||||||
void this.handleRenameSubmit(item.id, this.state.drafts[item.id] ?? label);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
...tileBaseSx,
|
|
||||||
cursor: 'grab',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
my: 0.5,
|
|
||||||
bgcolor: 'action.hover',
|
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
variant="standard"
|
|
||||||
value={draftName}
|
value={draftName}
|
||||||
onChange={(e) => this.handleDraftChange(item.id, e.target.value)}
|
onChange={(e) => this.handleDraftChange(item.id, e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void this.handleRenameSubmit(item.id, draftName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="standard"
|
||||||
|
size="small"
|
||||||
slotProps={{ inputLabel: { shrink: true } }}
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
sx={{
|
sx={{
|
||||||
mx: 0.5,
|
width: '100%',
|
||||||
'& .MuiInput-underline:before, & .MuiInput-underline:after': {
|
'& .MuiInput-underline:before, & .MuiInput-underline:after': {
|
||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
},
|
},
|
||||||
'& .MuiInputBase-input': {
|
'& .MuiInputBase-input': {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
py: '6px',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
}
|
||||||
|
editBand={{
|
||||||
|
onMoveLeft: () => this.handleMoveCategory(item, -1),
|
||||||
|
onMoveRight: () => this.handleMoveCategory(item, 1),
|
||||||
|
onDelete: () => this.handleDeleteCategory(item),
|
||||||
|
canLeft: index > 0,
|
||||||
|
canRight: index < childCategories.length - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -458,6 +544,7 @@ const mapDispatchToProps = {
|
|||||||
loadCategoryArticles,
|
loadCategoryArticles,
|
||||||
navigateToCategory,
|
navigateToCategory,
|
||||||
renameCategory,
|
renameCategory,
|
||||||
|
deleteCategory,
|
||||||
reorderCategories,
|
reorderCategories,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user