diff --git a/src/components/article/ArticleNameDialog.jsx b/src/components/article/ArticleNameDialog.jsx
index 83a32e7..74c84e3 100644
--- a/src/components/article/ArticleNameDialog.jsx
+++ b/src/components/article/ArticleNameDialog.jsx
@@ -1,13 +1,211 @@
-import NameDialog from '../NameDialog';
+import { Component } from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import Alert from '@mui/material/Alert';
+import BackendError from '../BackendError';
+import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
+import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
-export default function ArticleNameDialog({ open, onClose, onSubmit }) {
- return (
-
- );
+const BULK_MODE = 'bulk';
+const SINGLE_MODE = 'single';
+
+/** Split a bulk textarea into non-empty, trimmed names (one per line). */
+function parseBulkNames(text) {
+ return String(text ?? '')
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean);
}
+
+/**
+ * New-article dialog.
+ * Single mode: one name. Bulk mode ("Sammelanlegen"): one name per line.
+ * Props: open, onClose, onSubmit({ names: string[] }).
+ */
+class ArticleNameDialog extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ mode: SINGLE_MODE,
+ name: '',
+ bulkText: '',
+ busy: false,
+ error: null,
+ };
+ this.handleClose = this.handleClose.bind(this);
+ this.handleSingleSubmit = this.handleSingleSubmit.bind(this);
+ this.handleBulkSubmit = this.handleBulkSubmit.bind(this);
+ this.openBulk = this.openBulk.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.open && !prevProps.open) {
+ this.setState({
+ mode: SINGLE_MODE,
+ name: '',
+ bulkText: '',
+ busy: false,
+ error: null,
+ });
+ }
+ }
+
+ handleClose() {
+ if (this.state.busy) return;
+ this.props.onClose();
+ }
+
+ async handleSingleSubmit(event) {
+ event.preventDefault();
+ const name = this.state.name.trim();
+ if (!name) {
+ this.setState({ error: 'Bitte Name eingeben.' });
+ return;
+ }
+
+ this.setState({ busy: true, error: null });
+ try {
+ await this.props.onSubmit({ names: [name] });
+ this.props.onClose();
+ } catch (err) {
+ this.setState({ error: backendErrorFrom(err), busy: false });
+ }
+ }
+
+ async handleBulkSubmit(event) {
+ event.preventDefault();
+ const names = parseBulkNames(this.state.bulkText);
+ if (names.length === 0) {
+ this.setState({ error: 'Bitte mindestens einen Namen eingeben.' });
+ return;
+ }
+
+ this.setState({ busy: true, error: null });
+ try {
+ await this.props.onSubmit({ names });
+ this.props.onClose();
+ } catch (err) {
+ this.setState({ error: backendErrorFrom(err), busy: false });
+ }
+ }
+
+ openBulk() {
+ if (this.state.busy) return;
+ this.setState({ mode: BULK_MODE, error: null });
+ }
+
+ render() {
+ const { open } = this.props;
+ const { mode, name, bulkText, busy, error } = this.state;
+ const isBulk = mode === BULK_MODE;
+ const canSubmitSingle = name.trim() !== '';
+ const bulkNames = parseBulkNames(bulkText);
+ const canSubmitBulk = bulkNames.length > 0;
+
+ return (
+
+ );
+ }
+}
+
+export default ArticleNameDialog;
+
diff --git a/src/components/article/NewArticleTile.jsx b/src/components/article/NewArticleTile.jsx
index c963c57..0d133f6 100644
--- a/src/components/article/NewArticleTile.jsx
+++ b/src/components/article/NewArticleTile.jsx
@@ -1,7 +1,10 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { ActionTile } from '../tiles';
-import { createArticle as createArticleThunk } from '../../store/categorySlice';
+import {
+ createArticles as createArticlesThunk,
+ createArticle as createArticleThunk,
+} from '../../store/categorySlice';
import ArticleNameDialog from './ArticleNameDialog';
class NewArticleTile extends Component {
@@ -21,11 +24,21 @@ class NewArticleTile extends Component {
this.setState({ open: false });
}
- async handleSubmit({ name }) {
+ async handleSubmit({ names }) {
const { parentId } = this.props;
- const article = await this.props.createArticle({ parentId, name }).unwrap();
- if (article) {
- this.props.onOpenArticle?.(article);
+ const list = (names ?? []).filter(Boolean);
+ if (list.length <= 1) {
+ const article = await this.props
+ .createArticle({ parentId, name: list[0] ?? '' })
+ .unwrap();
+ if (article) {
+ this.props.onOpenArticle?.(article);
+ }
+ return;
+ }
+ const created = await this.props.createArticles({ parentId, names: list }).unwrap();
+ if (created?.[0]) {
+ this.props.onOpenArticle?.(created[0]);
}
}
@@ -51,6 +64,7 @@ class NewArticleTile extends Component {
const mapDispatchToProps = {
createArticle: createArticleThunk,
+ createArticles: createArticlesThunk,
};
export default connect(null, mapDispatchToProps)(NewArticleTile);
diff --git a/src/components/category/CategoryNameDialog.jsx b/src/components/category/CategoryNameDialog.jsx
index ea76c87..41005cb 100644
--- a/src/components/category/CategoryNameDialog.jsx
+++ b/src/components/category/CategoryNameDialog.jsx
@@ -1,4 +1,14 @@
-import NameDialog from '../NameDialog';
+import { Component } from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import Alert from '@mui/material/Alert';
+import BackendError from '../BackendError';
+import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
+import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
const TITLES = {
create: 'Neue Kategorie',
@@ -10,21 +20,206 @@ const SUBMIT_LABELS = {
edit: 'Speichern',
};
-export default function CategoryNameDialog({
- open,
- mode = 'create',
- initialName,
- onClose,
- onSubmit,
-}) {
- return (
-
- );
+const BULK_MODE = 'bulk';
+const SINGLE_MODE = 'single';
+
+/** Split a bulk textarea into non-empty, trimmed names (one per line). */
+function parseBulkNames(text) {
+ return String(text ?? '')
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean);
}
+
+/**
+ * Category name dialog.
+ * Single mode: one name (create/edit). Bulk mode ("Sammelanlegen", create
+ * only): one name per line. Props: open, mode ('create' | 'edit'), initialName,
+ * onClose, onSubmit({ names: string[] }).
+ */
+class CategoryNameDialog extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ mode: SINGLE_MODE,
+ name: '',
+ bulkText: '',
+ busy: false,
+ error: null,
+ };
+ this.handleClose = this.handleClose.bind(this);
+ this.handleSingleSubmit = this.handleSingleSubmit.bind(this);
+ this.handleBulkSubmit = this.handleBulkSubmit.bind(this);
+ this.openBulk = this.openBulk.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.open && !prevProps.open) {
+ this.setState({
+ mode: SINGLE_MODE,
+ name: this.props.initialName ?? '',
+ bulkText: '',
+ busy: false,
+ error: null,
+ });
+ }
+ }
+
+ handleClose() {
+ if (this.state.busy) return;
+ this.props.onClose();
+ }
+
+ async handleSingleSubmit(event) {
+ event.preventDefault();
+ const name = this.state.name.trim();
+ if (!name) {
+ this.setState({ error: 'Bitte Name eingeben.' });
+ return;
+ }
+
+ this.setState({ busy: true, error: null });
+ try {
+ await this.props.onSubmit({ names: [name] });
+ this.props.onClose();
+ } catch (err) {
+ this.setState({ error: backendErrorFrom(err), busy: false });
+ }
+ }
+
+ async handleBulkSubmit(event) {
+ event.preventDefault();
+ const names = parseBulkNames(this.state.bulkText);
+ if (names.length === 0) {
+ this.setState({ error: 'Bitte mindestens einen Namen eingeben.' });
+ return;
+ }
+
+ this.setState({ busy: true, error: null });
+ try {
+ await this.props.onSubmit({ names });
+ this.props.onClose();
+ } catch (err) {
+ this.setState({ error: backendErrorFrom(err), busy: false });
+ }
+ }
+
+ openBulk() {
+ if (this.state.busy) return;
+ this.setState({ mode: BULK_MODE, error: null });
+ }
+
+ render() {
+ const { open, mode = 'create' } = this.props;
+ const { mode: formMode, name, bulkText, busy, error } = this.state;
+ const isBulk = formMode === BULK_MODE;
+ const canBulk = mode === 'create';
+ const canSubmitSingle = name.trim() !== '';
+ const bulkNames = parseBulkNames(bulkText);
+ const canSubmitBulk = bulkNames.length > 0;
+
+ return (
+
+ );
+ }
+}
+
+export default CategoryNameDialog;
+
diff --git a/src/components/category/NewCategoryTile.jsx b/src/components/category/NewCategoryTile.jsx
index 36163f7..667becf 100644
--- a/src/components/category/NewCategoryTile.jsx
+++ b/src/components/category/NewCategoryTile.jsx
@@ -1,7 +1,10 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { ActionTile } from '../tiles';
-import { createCategory as createCategoryThunk } from '../../store/categorySlice';
+import {
+ createCategories as createCategoriesThunk,
+ createCategory as createCategoryThunk,
+} from '../../store/categorySlice';
import CategoryNameDialog from './CategoryNameDialog';
class NewCategoryTile extends Component {
@@ -21,9 +24,14 @@ class NewCategoryTile extends Component {
this.setState({ open: false });
}
- async handleSubmit({ name }) {
+ async handleSubmit({ names }) {
const { parentId } = this.props;
- await this.props.createCategory({ parentId, name }).unwrap();
+ const list = (names ?? []).filter(Boolean);
+ if (list.length <= 1) {
+ await this.props.createCategory({ parentId, name: list[0] ?? '' }).unwrap();
+ return;
+ }
+ await this.props.createCategories({ parentId, names: list }).unwrap();
}
render() {
@@ -49,6 +57,7 @@ class NewCategoryTile extends Component {
const mapDispatchToProps = {
createCategory: createCategoryThunk,
+ createCategories: createCategoriesThunk,
};
export default connect(null, mapDispatchToProps)(NewCategoryTile);
diff --git a/src/components/tiles/CategoryTile.jsx b/src/components/tiles/CategoryTile.jsx
index 1bd45b8..d87b8b7 100644
--- a/src/components/tiles/CategoryTile.jsx
+++ b/src/components/tiles/CategoryTile.jsx
@@ -1,4 +1,4 @@
-import { Component } from 'react';
+import { Component, PureComponent } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
@@ -74,7 +74,7 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount }) {
/**
* Middle row (1/3): gray band — TextField or terminal toggle.
*/
-function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, onPhotoUpload, onPhotoDelete, categoryFullHash }) {
+function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, onPhotoUpload, onPhotoDelete, fullHash }) {
const fillSx = {
width: '100%',
height: '100%',
@@ -121,7 +121,7 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
noWrap
sx={{ maxWidth: 80, color: 'text.secondary' }}
>
- {categoryFullHash ? 'Löschen' : 'Hochladen'}
+ {fullHash ? 'Löschen' : 'Hochladen'}
@@ -217,7 +217,7 @@ function BottomRow({ onOpen, editBand }) {
* Renders a ThumbHash preview and the full thumbnail as backdrop for a category tile.
* ThumbHash is shown immediately as a placeholder; the thumb fades in once loaded.
*/
-class CategoryBackdrop extends Component {
+class CategoryBackdrop extends PureComponent {
state = { thumbLoaded: false };
componentDidUpdate(prevProps) {
@@ -265,13 +265,52 @@ class CategoryBackdrop extends Component {
}
/**
- * Category tile (normal mode): no icon, click navigates to the category.
+ * Category tile.
+ *
+ * Normal mode (edit=false): clickable tile with label overlay; click navigates.
+ * Edit mode (edit=true): three rows of 1/3 each —
+ * Top row: sort-handle | settings toggle | drop-handle
+ * Middle row: gray band — TextField, POS toggle, or photo upload
+ * Bottom row: upper half = open-category click zone
+ * lower half = gray band with chevrons + delete
*/
export class CategoryTile extends Component {
+ state = { settingsTab: -1 }; // -1 = rename, -2 = photo, 0..n-1 = terminal index
+
handleClick = () => {
this.props.onClick?.();
};
+ handleKeyDown = (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ this.handleClick();
+ }
+ };
+
+ handleSettingsToggle = () => {
+ const { terminals } = this.props;
+ const terminalCount = terminals?.length || 0;
+ this.setState((prev) => {
+ const next = prev.settingsTab + 1;
+ // Cycle: photo (-2) → rename (-1) → terminal 0 → ... → terminal n-1 → photo (-2)
+ const nextTab = next >= terminalCount ? -2 : next;
+ return { settingsTab: nextTab };
+ });
+ };
+
+ handleMiddleBandClick = () => {
+ const { settingsTab } = this.state;
+ const { onPhotoUpload, onPhotoDelete, fullHash } = this.props;
+ if (settingsTab === -2) {
+ if (fullHash) {
+ onPhotoDelete?.();
+ } else {
+ onPhotoUpload?.();
+ }
+ }
+ };
+
renderLabel() {
const { label, nameInput } = this.props;
if (label == null) return null;
@@ -304,123 +343,85 @@ export class CategoryTile extends Component {
);
}
- render() {
- const { sx, onClick: _onClick, label, thumbHash, fullHash, ...rest } = this.props;
- return (
-
-
-
- {this.renderLabel()}
-
-
- );
- }
-}
-
-/**
- * Category tile for edit mode: three rows of 1/3 each.
- *
- * Top row: sort-handle | settings toggle | drop-handle
- * Middle row: gray band — TextField, POS toggle, or photo upload
- * Bottom row: upper half = open-category click zone
- * lower half = gray band with chevrons + delete
- */
-export class CategoryEditTile extends Component {
- state = { settingsTab: -1 }; // -1 = rename, -2 = photo, 0..n-1 = terminal index
-
- handleSettingsToggle = () => {
- const { terminals } = this.props;
- const terminalCount = terminals?.length || 0;
- this.setState((prev) => {
- const next = prev.settingsTab + 1;
- // Cycle: photo (-2) → rename (-1) → terminal 0 → ... → terminal n-1 → photo (-2)
- const nextTab = next >= terminalCount ? -2 : next;
- return { settingsTab: nextTab };
- });
- };
-
- handleMiddleBandClick = () => {
- const { settingsTab } = this.state;
- const { onPhotoUpload, onPhotoDelete, categoryFullHash } = this.props;
- if (settingsTab === -2) {
- if (categoryFullHash) {
- onPhotoDelete?.();
- } else {
- onPhotoUpload?.();
- }
- }
- };
-
render() {
const {
+ sx,
+ edit,
label,
+ thumbHash,
+ fullHash,
+ onClick,
nameInput,
editBand,
onOpen,
terminals,
categoryPosterminalIds,
onTerminalToggle,
- hasTerminals: _hasTerminals,
- categoryFullHash,
- thumbHash,
- onPhotoUpload: _onPhotoUpload,
- onPhotoDelete: _onPhotoDelete,
- sx,
+ onPhotoUpload,
+ onPhotoDelete,
...rest
} = this.props;
const { settingsTab } = this.state;
+ // Shared by both modes: the thumbnail backdrop.
+ const backdrop = (
+
+ );
+
+ // Only the overlay differs between normal and edit mode.
+ const overlay = edit ? (
+ <>
+
+
+
+
+
+ >
+ ) : (
+ this.renderLabel()
+ );
+
return (
-
+ {backdrop}
-
-
-
-
-
+ {overlay}
);
diff --git a/src/components/tiles/index.js b/src/components/tiles/index.js
index 7175fb0..57772d7 100644
--- a/src/components/tiles/index.js
+++ b/src/components/tiles/index.js
@@ -1,5 +1,5 @@
export { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
export { BackTile } from './BackTile';
-export { CategoryTile, CategoryEditTile } from './CategoryTile';
+export { CategoryTile } from './CategoryTile';
export { ArticleTile } from './ArticleTile';
export { ActionTile } from './ActionTile';
diff --git a/src/pages/artikel/kategorien.jsx b/src/pages/artikel/kategorien.jsx
index 61c0306..23a8f0b 100644
--- a/src/pages/artikel/kategorien.jsx
+++ b/src/pages/artikel/kategorien.jsx
@@ -14,14 +14,13 @@ import { notifyError } from '../../utils/notify';
import {
ArticleTile,
BackTile,
- CategoryEditTile,
CategoryTile,
} from '../../components/tiles';
import { TILE_SIZE } from 'dashgui/tiles';
import withCategoryRoute from './withCategoryRoute';
import { artikelDetailPath, articleSku } from './articleRoute';
import { selectCompanyId } from '../../store/companySlice';
-import { selectHasPosterminals, selectPosterminals, loadPosterminals } from '../../store/posterminalSlice';
+import { selectPosterminals, loadPosterminals } from '../../store/posterminalSlice';
import {
buildCategoryPath,
defaultCategoryPath,
@@ -281,7 +280,6 @@ class ArtikelKategorienPage extends Component {
childCategories,
articles,
renameMode,
- hasTerminals,
terminals,
} = this.props;
@@ -346,20 +344,19 @@ class ArtikelKategorienPage extends Component {
{childCategories.map((item, index) => {
const label = categoryDisplayName(item.name, item.id, rootCategoryId);
- if (renameMode) {
- const draftName =
- this.state.drafts[item.id] !== undefined
- ? this.state.drafts[item.id]
- : label;
- return (
- {
+ const draftName =
+ this.state.drafts[item.id] !== undefined
+ ? this.state.drafts[item.id]
+ : label;
+ const editProps = renameMode
+ ? {
+ edit: true,
+ 'data-id': String(item.id),
+ className: 'sortable-item',
+ label: draftName,
+ terminals,
+ categoryPosterminalIds: item.posterminalIds || [],
+ onTerminalToggle: (terminalId, enabled) => {
const thunk = enabled
? this.props.enableTerminalCategory
: this.props.disableTerminalCategory;
@@ -373,12 +370,9 @@ class ArtikelKategorienPage extends Component {
});
})
.catch(notifyError);
- }}
- onOpen={() => this.navigateToCategoryById(item.id)}
- hasTerminals={hasTerminals}
- categoryFullHash={item.fullHash}
- thumbHash={item.thumbHash}
- onPhotoUpload={() => {
+ },
+ onOpen: () => this.navigateToCategoryById(item.id),
+ onPhotoUpload: () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
@@ -398,12 +392,12 @@ class ArtikelKategorienPage extends Component {
}
};
input.click();
- }}
- onPhotoDelete={() => {
+ },
+ onPhotoDelete: () => {
void this.props.deleteCategoryImage(item.id)
.catch(notifyError);
- }}
- nameInput={
+ },
+ nameInput: (
this.handleDraftChange(item.id, e.target.value)}
@@ -429,24 +423,26 @@ class ArtikelKategorienPage extends Component {
},
}}
/>
- }
- editBand={{
+ ),
+ editBand: {
onMoveLeft: () => this.handleMoveCategory(item, -1),
onMoveRight: () => this.handleMoveCategory(item, 1),
onDelete: () => this.handleDeleteCategory(item),
canLeft: index > 0,
canRight: index < childCategories.length - 1,
- }}
- />
- );
- }
+ },
+ }
+ : {
+ label,
+ onClick: () => this.navigateToCategoryById(item.id),
+ };
+
return (
this.navigateToCategoryById(item.id)}
thumbHash={item.thumbHash}
fullHash={item.fullHash}
+ {...editProps}
/>
);
})}
@@ -493,7 +489,6 @@ const mapStateToProps = (state) => ({
articles: selectCategoryArticles(state),
canGoBack: selectCanNavigateBack(state),
renameMode: selectRenameMode(state),
- hasTerminals: selectHasPosterminals(state),
terminals: selectPosterminals(state),
});
diff --git a/src/store/categorySlice.js b/src/store/categorySlice.js
index 233be85..1a63acc 100644
--- a/src/store/categorySlice.js
+++ b/src/store/categorySlice.js
@@ -139,6 +139,39 @@ export const createCategory = createAsyncThunk(
},
);
+/**
+ * Create several categories under the same parent in one go (bulk add).
+ * Runs the single-create API sequentially so every category is created
+ * even if a later one fails; the first error is reported.
+ * Returns the array of created (normalized) categories.
+ */
+export const createCategories = createAsyncThunk(
+ 'category/createCategories',
+ async ({ parentId, names }, { rejectWithValue }) => {
+ const list = (names ?? [])
+ .map((n) => String(n ?? '').trim())
+ .filter(Boolean);
+ if (list.length === 0) {
+ return rejectWithValue({ message: 'Keine Namen übergeben.' });
+ }
+ const created = [];
+ try {
+ for (const name of list) {
+ const data = await category.create({ parentId, name });
+ requireObject(data.category, 'POST /api/category.category');
+ created.push(normalizeCategoryNode(data.category));
+ }
+ return created;
+ } catch (err) {
+ const normalized = catchBackendError(rejectWithValue)(err);
+ // Attach already-created categories so the UI can still update optimistically.
+ return normalized && created.length > 0
+ ? rejectWithValue({ ...normalized, created })
+ : normalized;
+ }
+ },
+);
+
export const createArticle = createAsyncThunk(
'category/createArticle',
async ({ parentId, name }, { rejectWithValue }) => {
@@ -153,6 +186,40 @@ export const createArticle = createAsyncThunk(
},
);
+/**
+ * Create several articles under the same parent in one go (bulk add).
+ * Runs the single-create API sequentially so every article is created
+ * even if a later one fails; the first error is reported.
+ * Returns the array of created articles.
+ */
+export const createArticles = createAsyncThunk(
+ 'category/createArticles',
+ async ({ parentId, names }, { rejectWithValue }) => {
+ const list = (names ?? [])
+ .map((n) => String(n ?? '').trim())
+ .filter(Boolean);
+ if (list.length === 0) {
+ return rejectWithValue({ message: 'Keine Namen übergeben.' });
+ }
+ const created = [];
+ try {
+ for (const name of list) {
+ const data = await article.create({ parentId, name });
+ requireObject(data.article, 'POST /api/article.article');
+ requireDefined(data.article.id, 'article.id');
+ created.push(data.article);
+ }
+ return created;
+ } catch (err) {
+ const normalized = catchBackendError(rejectWithValue)(err);
+ // Attach already-created articles so the UI can still update optimistically.
+ return normalized && created.length > 0
+ ? rejectWithValue({ ...normalized, created })
+ : normalized;
+ }
+ },
+);
+
export const renameCategory = createAsyncThunk(
'category/rename',
async ({ categoryId, name }, { rejectWithValue }) => {
@@ -434,6 +501,15 @@ const categorySlice = createSlice({
if (!state.categories) state.categories = {};
state.categories[String(action.payload.id)] = action.payload;
})
+ .addCase(createCategories.fulfilled, (state, action) => {
+ const createdList = action.payload;
+ if (!Array.isArray(createdList)) return;
+ if (!state.categories) state.categories = {};
+ for (const item of createdList) {
+ if (item == null || item.id == null) continue;
+ state.categories[String(item.id)] = item;
+ }
+ })
.addCase(renameCategory.fulfilled, (state, action) => {
const { id, name } = action.payload;
if (id == null || name == null) return;
@@ -495,6 +571,28 @@ const categorySlice = createSlice({
state.rootArticles = [...state.rootArticles, created];
}
})
+ .addCase(createArticles.fulfilled, (state, action) => {
+ const createdList = action.payload;
+ if (!Array.isArray(createdList)) return;
+ const added = createdList.filter((item) => item != null);
+ if (added.length === 0) return;
+ if (added.some((item) => String(item.parentId) === String(state.currentCategoryId))) {
+ state.articles = [
+ ...state.articles,
+ ...added.filter((item) =>
+ String(item.parentId) === String(state.currentCategoryId),
+ ),
+ ];
+ state.articlesTotal = state.articles.length;
+ state.articlesStatus = 'succeeded';
+ }
+ const rootAdded = added.filter((item) =>
+ String(item.parentId) === String(state.rootCategoryId),
+ );
+ if (rootAdded.length > 0) {
+ state.rootArticles = [...state.rootArticles, ...rootAdded];
+ }
+ })
.addCase(deleteCategory.fulfilled, (state, action) => {
if (!state.categories) return;
delete state.categories[String(action.payload)];