Add category rename and drag-and-drop reordering in edit mode

- Edit toggle behind the "Kategorien" title enables an edit mode.
- In edit mode all category tiles become inline rename fields (no outline,
  distinct background, small margins); articles and "new article" are hidden.
- Rename via PATCH /api/category/:id; reorder via PUT /api/category/order
  (parentId + categoryIds) with SortableJS.
- Only call backend when order actually changed; revert DOM order on error
  so further dragging keeps working.
- Redux: renameCategory + reorderCategories thunks, renameMode state.
This commit is contained in:
seb
2026-08-02 19:43:38 +02:00
parent 5309b1174b
commit 2dd14bac91
7 changed files with 180 additions and 3 deletions

View File

@@ -168,6 +168,27 @@ export const renameCategory = createAsyncThunk(
},
);
/**
* Reorder sibling categories (new order of children under the current parent).
* `PUT /api/category/order` → 200 { categoryIds: number[] }
*/
export const reorderCategories = createAsyncThunk(
'category/reorder',
async ({ categoryIds }, { rejectWithValue, getState }) => {
try {
const ids = requireArray(categoryIds, 'categoryIds');
const parentId = getState().category.currentCategoryId;
requireDefined(parentId, 'currentCategoryId');
const data = await category.reorder(parentId, ids);
requireObject(data, 'PUT /api/category/order');
requireArray(data.categoryIds, 'categoryIds');
return { parentId, categoryIds: requireArray(data.categoryIds, 'categoryIds') };
} catch (err) {
return catchBackendError(rejectWithValue)(err);
}
},
);
export const deleteCategory = createAsyncThunk(
'category/delete',
async (categoryId, { rejectWithValue }) => {
@@ -309,6 +330,26 @@ const categorySlice = createSlice({
: row,
);
})
.addCase(reorderCategories.fulfilled, (state, action) => {
const { parentId, categoryIds } = action.payload;
if (!state.categories) return;
const parentKey = String(parentId);
const idToSort = new Map(
categoryIds.map((id, index) => [String(id), index]),
);
for (const node of Object.values(state.categories)) {
if (
node.parentId != null &&
String(node.parentId) === parentKey &&
idToSort.has(String(node.id))
) {
state.categories[String(node.id)] = {
...node,
sort: idToSort.get(String(node.id)),
};
}
}
})
.addCase(createArticle.fulfilled, (state, action) => {
const created = action.payload;
if (created == null) return;