From 8bbd6fade1d930ba7c617cd929719521e7f8c347 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 10 Aug 2026 03:13:53 +0200 Subject: [PATCH] u --- src/components/article/ArticleNameDialog.jsx | 220 ++++++++++++++++- src/components/article/NewArticleTile.jsx | 24 +- .../category/CategoryNameDialog.jsx | 231 ++++++++++++++++-- src/components/category/NewCategoryTile.jsx | 15 +- src/components/tiles/CategoryTile.jsx | 197 +++++++-------- src/components/tiles/index.js | 2 +- src/pages/artikel/kategorien.jsx | 67 +++-- src/store/categorySlice.js | 98 ++++++++ 8 files changed, 682 insertions(+), 172 deletions(-) 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 ( + + Neuer Artikel + + {!isBulk && ( + + + {error ? ( + isBackendFailure(error) ? ( + + ) : ( + + {typeof error === 'string' ? error : error.message} + + ) + ) : null} + + this.setState({ name: nextName })} + /> + + + + + + + + + )} + + {isBulk && ( + + + {error ? ( + isBackendFailure(error) ? ( + + ) : ( + + {typeof error === 'string' ? error : error.message} + + ) + ) : null} + + this.setState({ bulkText: nextText })} + helperText={ + bulkNames.length > 0 + ? `${bulkNames.length} Artikel werden angelegt.` + : 'Ein Name pro Zeile eingeben.' + } + /> + + + + + + + + + )} + + ); + } +} + +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 ( + + {TITLES[mode]} + + {!isBulk && ( + + + {error ? ( + isBackendFailure(error) ? ( + + ) : ( + + {typeof error === 'string' ? error : error.message} + + ) + ) : null} + + this.setState({ name: nextName })} + /> + + + {canBulk && ( + + )} + + + + + + )} + + {isBulk && ( + + + {error ? ( + isBackendFailure(error) ? ( + + ) : ( + + {typeof error === 'string' ? error : error.message} + + ) + ) : null} + + this.setState({ bulkText: nextText })} + helperText={ + bulkNames.length > 0 + ? `${bulkNames.length} Kategorien werden angelegt.` + : 'Ein Name pro Zeile eingeben.' + } + /> + + + + + + + + + )} + + ); + } +} + +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)];