Add category image upload with thumbnail backdrop in category tiles
- Add category image API methods (upload/delete) to src/api/index.js - Add uploadCategoryImage/deleteCategoryImage thunks and extraReducers in categorySlice - Add CategoryBackdrop component showing ThumbHash placeholder with full thumb overlay - Add photo upload/delete tab to CategoryEditTile settings cycle - Category tile label band uses semi-transparent black overlay with white text - Pass thumbHash/fullHash to both CategoryTile and CategoryEditTile
This commit is contained in:
@@ -385,6 +385,27 @@ export const category = {
|
|||||||
body: { pos },
|
body: { pos },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/category/:id/image
|
||||||
|
* Body: raw AVIF bytes (Content-Type: image/avif)
|
||||||
|
* → 201 { category: { id, fullHash, thumbW, thumbH, thumbHash } }
|
||||||
|
*/
|
||||||
|
uploadImage(categoryId, file) {
|
||||||
|
return httpBinary(`/api/category/${categoryId}/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'image/avif',
|
||||||
|
body: file,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/category/:id/image
|
||||||
|
* → 200 { category: { id, thumbW: null, thumbH: null, thumbHash: null } }
|
||||||
|
*/
|
||||||
|
deleteImage(categoryId) {
|
||||||
|
return http(`/api/category/${categoryId}/image`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
||||||
|
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
||||||
|
import ThumbHashImage from '../article/ThumbHashImage';
|
||||||
|
import { thumb } from '../../api';
|
||||||
import { tileBaseSx } from './Tile';
|
import { tileBaseSx } from './Tile';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top row (1/3): sort handle | settings toggle | move handle.
|
* Top row (1/3): sort handle | settings toggle | move handle.
|
||||||
*/
|
*/
|
||||||
function TopBand({ onSettingsClick, settingsTab, terminalCount }) {
|
function TopBand({ onSettingsClick, settingsTab, terminalCount }) {
|
||||||
// settingsTab >= 0 means we're on a terminal tab
|
|
||||||
const isTerminalMode = settingsTab >= 0 && settingsTab < terminalCount;
|
const isTerminalMode = settingsTab >= 0 && settingsTab < terminalCount;
|
||||||
|
const isPhotoMode = settingsTab === -2;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -45,11 +48,13 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount }) {
|
|||||||
disableRipple
|
disableRipple
|
||||||
onClick={onSettingsClick}
|
onClick={onSettingsClick}
|
||||||
sx={{
|
sx={{
|
||||||
color: isTerminalMode ? 'primary.main' : 'action.active',
|
color: isTerminalMode || isPhotoMode ? 'primary.main' : 'action.active',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTerminalMode ? (
|
{isTerminalMode ? (
|
||||||
<PointOfSaleIcon sx={{ fontSize: 28 }} />
|
<PointOfSaleIcon sx={{ fontSize: 28 }} />
|
||||||
|
) : isPhotoMode ? (
|
||||||
|
<PhotoCameraIcon sx={{ fontSize: 28 }} />
|
||||||
) : (
|
) : (
|
||||||
<SettingsIcon sx={{ fontSize: 28 }} />
|
<SettingsIcon sx={{ fontSize: 28 }} />
|
||||||
)}
|
)}
|
||||||
@@ -69,27 +74,23 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount }) {
|
|||||||
/**
|
/**
|
||||||
* Middle row (1/3): gray band — TextField or terminal toggle.
|
* Middle row (1/3): gray band — TextField or terminal toggle.
|
||||||
*/
|
*/
|
||||||
function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle }) {
|
function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, onPhotoUpload, onPhotoDelete, categoryFullHash }) {
|
||||||
console.log('[MiddleBand] settingsTab:', settingsTab, 'terminals:', terminals?.length, 'categoryPosterminalIds:', categoryPosterminalIds);
|
const fillSx = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'grey.300',
|
||||||
|
px: 1,
|
||||||
|
};
|
||||||
|
|
||||||
// Show terminal-specific toggle when on a terminal tab
|
// Show terminal-specific toggle when on a terminal tab
|
||||||
if (settingsTab >= 0 && settingsTab < terminals.length) {
|
if (settingsTab >= 0 && settingsTab < terminals.length) {
|
||||||
const terminal = terminals[settingsTab];
|
const terminal = terminals[settingsTab];
|
||||||
const isEnabled = categoryPosterminalIds.includes(terminal.id);
|
const isEnabled = categoryPosterminalIds.includes(terminal.id);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={fillSx}>
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '33.33%',
|
|
||||||
bottom: '33.33%',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
bgcolor: 'grey.300',
|
|
||||||
px: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<PointOfSaleIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
<PointOfSaleIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||||
<Typography
|
<Typography
|
||||||
@@ -109,21 +110,26 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show photo upload UI when on photo tab
|
||||||
|
if (settingsTab === -2) {
|
||||||
|
return (
|
||||||
|
<Box sx={fillSx}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<PhotoCameraIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
noWrap
|
||||||
|
sx={{ maxWidth: 80, color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
{categoryFullHash ? 'Löschen' : 'Hochladen'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={fillSx}>
|
||||||
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}
|
{nameInput}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -207,6 +213,57 @@ 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 {
|
||||||
|
state = { thumbLoaded: false };
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.fullHash !== this.props.fullHash) {
|
||||||
|
this.setState({ thumbLoaded: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleThumbLoad = () => {
|
||||||
|
this.setState({ thumbLoaded: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleThumbError = () => {
|
||||||
|
this.setState({ thumbLoaded: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { thumbHash, fullHash } = this.props;
|
||||||
|
const { thumbLoaded } = this.state;
|
||||||
|
if (!thumbHash && !fullHash) return null;
|
||||||
|
return (
|
||||||
|
<Box sx={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden' }}>
|
||||||
|
{thumbHash && !thumbLoaded ? <ThumbHashImage thumbHash={thumbHash} /> : null}
|
||||||
|
{fullHash ? (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={thumb.url(fullHash)}
|
||||||
|
alt=""
|
||||||
|
onLoad={this.handleThumbLoad}
|
||||||
|
onError={this.handleThumbError}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
opacity: thumbLoaded ? 1 : 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Category tile (normal mode): no icon, click navigates to the category.
|
* Category tile (normal mode): no icon, click navigates to the category.
|
||||||
*/
|
*/
|
||||||
@@ -229,7 +286,7 @@ export class CategoryTile extends Component {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
bgcolor: 'grey.300',
|
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
||||||
px: 1,
|
px: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -238,7 +295,7 @@ export class CategoryTile extends Component {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
noWrap
|
noWrap
|
||||||
sx={{ lineHeight: 1.2, color: 'text.primary' }}
|
sx={{ lineHeight: 1.2, color: 'common.white' }}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -248,7 +305,7 @@ export class CategoryTile extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { sx, onClick: _onClick, label, ...rest } = this.props;
|
const { sx, onClick: _onClick, label, thumbHash, fullHash, ...rest } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="button"
|
component="button"
|
||||||
@@ -258,10 +315,14 @@ export class CategoryTile extends Component {
|
|||||||
sx={{
|
sx={{
|
||||||
...tileBaseSx,
|
...tileBaseSx,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderLabel()}
|
<CategoryBackdrop thumbHash={thumbHash} fullHash={fullHash} />
|
||||||
|
<Box sx={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||||
|
{this.renderLabel()}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -271,29 +332,36 @@ export class CategoryTile extends Component {
|
|||||||
* Category tile for edit mode: three rows of 1/3 each.
|
* Category tile for edit mode: three rows of 1/3 each.
|
||||||
*
|
*
|
||||||
* Top row: sort-handle | settings toggle | drop-handle
|
* Top row: sort-handle | settings toggle | drop-handle
|
||||||
* Middle row: gray band — TextField or POS toggle
|
* Middle row: gray band — TextField, POS toggle, or photo upload
|
||||||
* Bottom row: upper half = open-category click zone
|
* Bottom row: upper half = open-category click zone
|
||||||
* lower half = gray band with chevrons + delete
|
* lower half = gray band with chevrons + delete
|
||||||
*/
|
*/
|
||||||
export class CategoryEditTile extends Component {
|
export class CategoryEditTile extends Component {
|
||||||
state = { settingsTab: -1 }; // -1 = rename, 0..n-1 = terminal index
|
state = { settingsTab: -1 }; // -1 = rename, -2 = photo, 0..n-1 = terminal index
|
||||||
|
|
||||||
handleSettingsToggle = () => {
|
handleSettingsToggle = () => {
|
||||||
const { terminals } = this.props;
|
const { terminals } = this.props;
|
||||||
const terminalCount = terminals?.length || 0;
|
const terminalCount = terminals?.length || 0;
|
||||||
this.setState((prev) => {
|
this.setState((prev) => {
|
||||||
const next = prev.settingsTab + 1;
|
const next = prev.settingsTab + 1;
|
||||||
const nextTab = next >= terminalCount ? -1 : next;
|
// Cycle: photo (-2) → rename (-1) → terminal 0 → ... → terminal n-1 → photo (-2)
|
||||||
console.log('[CategoryEditTile] settings toggle', {
|
const nextTab = next >= terminalCount ? -2 : next;
|
||||||
terminals: terminals?.length,
|
|
||||||
terminalCount,
|
|
||||||
from: prev.settingsTab,
|
|
||||||
to: nextTab,
|
|
||||||
});
|
|
||||||
return { settingsTab: nextTab };
|
return { settingsTab: nextTab };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleMiddleBandClick = () => {
|
||||||
|
const { settingsTab } = this.state;
|
||||||
|
const { onPhotoUpload, onPhotoDelete, categoryFullHash } = this.props;
|
||||||
|
if (settingsTab === -2) {
|
||||||
|
if (categoryFullHash) {
|
||||||
|
onPhotoDelete?.();
|
||||||
|
} else {
|
||||||
|
onPhotoUpload?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@@ -304,6 +372,10 @@ export class CategoryEditTile extends Component {
|
|||||||
categoryPosterminalIds,
|
categoryPosterminalIds,
|
||||||
onTerminalToggle,
|
onTerminalToggle,
|
||||||
hasTerminals: _hasTerminals,
|
hasTerminals: _hasTerminals,
|
||||||
|
categoryFullHash,
|
||||||
|
thumbHash,
|
||||||
|
onPhotoUpload: _onPhotoUpload,
|
||||||
|
onPhotoDelete: _onPhotoDelete,
|
||||||
sx,
|
sx,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -316,23 +388,40 @@ export class CategoryEditTile extends Component {
|
|||||||
...tileBaseSx,
|
...tileBaseSx,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
'&:hover': { bgcolor: 'background.paper', boxShadow: 0 },
|
'&:hover': { bgcolor: 'background.paper', boxShadow: 0 },
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopBand
|
<CategoryBackdrop thumbHash={thumbHash} fullHash={categoryFullHash} />
|
||||||
onSettingsClick={this.handleSettingsToggle}
|
<Box sx={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||||
settingsTab={settingsTab}
|
<TopBand
|
||||||
terminalCount={terminals?.length || 0}
|
onSettingsClick={this.handleSettingsToggle}
|
||||||
/>
|
settingsTab={settingsTab}
|
||||||
<MiddleBand
|
terminalCount={terminals?.length || 0}
|
||||||
settingsTab={settingsTab}
|
/>
|
||||||
nameInput={nameInput}
|
<Box
|
||||||
terminals={terminals}
|
onClick={this.handleMiddleBandClick}
|
||||||
categoryPosterminalIds={categoryPosterminalIds}
|
sx={{
|
||||||
onTerminalToggle={onTerminalToggle}
|
position: 'absolute',
|
||||||
/>
|
top: '33.33%',
|
||||||
<BottomRow onOpen={onOpen} editBand={editBand} />
|
bottom: '33.33%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
cursor: settingsTab === -2 ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MiddleBand
|
||||||
|
settingsTab={settingsTab}
|
||||||
|
nameInput={nameInput}
|
||||||
|
terminals={terminals}
|
||||||
|
categoryPosterminalIds={categoryPosterminalIds}
|
||||||
|
onTerminalToggle={onTerminalToggle}
|
||||||
|
categoryFullHash={categoryFullHash}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<BottomRow onOpen={onOpen} editBand={editBand} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import {
|
|||||||
selectChildCategories,
|
selectChildCategories,
|
||||||
selectRenameMode,
|
selectRenameMode,
|
||||||
patchCategoryPosterminalIds,
|
patchCategoryPosterminalIds,
|
||||||
|
uploadCategoryImage,
|
||||||
|
deleteCategoryImage,
|
||||||
} from '../../store/categorySlice';
|
} from '../../store/categorySlice';
|
||||||
import {
|
import {
|
||||||
enableTerminalCategory,
|
enableTerminalCategory,
|
||||||
@@ -375,6 +377,33 @@ class ArtikelKategorienPage extends Component {
|
|||||||
}}
|
}}
|
||||||
onOpen={() => this.navigateToCategoryById(item.id)}
|
onOpen={() => this.navigateToCategoryById(item.id)}
|
||||||
hasTerminals={hasTerminals}
|
hasTerminals={hasTerminals}
|
||||||
|
categoryFullHash={item.fullHash}
|
||||||
|
thumbHash={item.thumbHash}
|
||||||
|
onPhotoUpload={() => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
// Prepare image (resize and encode to AVIF)
|
||||||
|
const { prepareArticlePhoto } = await import('../../utils/prepareArticlePhoto');
|
||||||
|
const avifFile = await prepareArticlePhoto(file);
|
||||||
|
await this.props.uploadCategoryImage({
|
||||||
|
categoryId: item.id,
|
||||||
|
file: avifFile,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}}
|
||||||
|
onPhotoDelete={() => {
|
||||||
|
void this.props.deleteCategoryImage(item.id)
|
||||||
|
.catch(notifyError);
|
||||||
|
}}
|
||||||
nameInput={
|
nameInput={
|
||||||
<TextField
|
<TextField
|
||||||
value={draftName}
|
value={draftName}
|
||||||
@@ -417,6 +446,8 @@ class ArtikelKategorienPage extends Component {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
label={label}
|
label={label}
|
||||||
onClick={() => this.navigateToCategoryById(item.id)}
|
onClick={() => this.navigateToCategoryById(item.id)}
|
||||||
|
thumbHash={item.thumbHash}
|
||||||
|
fullHash={item.fullHash}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -480,6 +511,8 @@ const mapDispatchToProps = {
|
|||||||
enableTerminalCategory,
|
enableTerminalCategory,
|
||||||
disableTerminalCategory,
|
disableTerminalCategory,
|
||||||
patchCategoryPosterminalIds,
|
patchCategoryPosterminalIds,
|
||||||
|
uploadCategoryImage,
|
||||||
|
deleteCategoryImage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withCategoryRoute(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
export default withCategoryRoute(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
||||||
|
|||||||
@@ -238,6 +238,38 @@ export const toggleCategoryPos = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/category/:id/image → 201 { category: { id, fullHash, thumbW, thumbH, thumbHash } }
|
||||||
|
*/
|
||||||
|
export const uploadCategoryImage = createAsyncThunk(
|
||||||
|
'category/uploadImage',
|
||||||
|
async ({ categoryId, file }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const data = await category.uploadImage(categoryId, file);
|
||||||
|
requireObject(data.category, 'POST /api/category/:id/image');
|
||||||
|
return data.category;
|
||||||
|
} catch (err) {
|
||||||
|
return catchBackendError(rejectWithValue)(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/category/:id/image → 200 { category: { id, thumbW: null, thumbH: null, thumbHash: null } }
|
||||||
|
*/
|
||||||
|
export const deleteCategoryImage = createAsyncThunk(
|
||||||
|
'category/deleteImage',
|
||||||
|
async (categoryId, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const data = await category.deleteImage(categoryId);
|
||||||
|
requireObject(data.category, 'DELETE /api/category/:id/image');
|
||||||
|
return data.category;
|
||||||
|
} catch (err) {
|
||||||
|
return catchBackendError(rejectWithValue)(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
companyId: null,
|
companyId: null,
|
||||||
languageId: null,
|
languageId: null,
|
||||||
@@ -442,6 +474,30 @@ const categorySlice = createSlice({
|
|||||||
state.categories[key] = { ...existing, pos: updated.pos };
|
state.categories[key] = { ...existing, pos: updated.pos };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.addCase(uploadCategoryImage.fulfilled, (state, action) => {
|
||||||
|
const { id, fullHash, thumbW, thumbH, thumbHash } = action.payload;
|
||||||
|
if (id == null || state.categories == null) return;
|
||||||
|
const key = String(id);
|
||||||
|
const existing = state.categories[key];
|
||||||
|
if (existing) {
|
||||||
|
state.categories[key] = { ...existing, fullHash, thumbW, thumbH, thumbHash };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(deleteCategoryImage.fulfilled, (state, action) => {
|
||||||
|
const { id } = action.payload;
|
||||||
|
if (id == null || state.categories == null) return;
|
||||||
|
const key = String(id);
|
||||||
|
const existing = state.categories[key];
|
||||||
|
if (existing) {
|
||||||
|
state.categories[key] = {
|
||||||
|
...existing,
|
||||||
|
fullHash: null,
|
||||||
|
thumbW: null,
|
||||||
|
thumbH: null,
|
||||||
|
thumbHash: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
.addCase(loadCurrentCompany.fulfilled, (state, action) => {
|
.addCase(loadCurrentCompany.fulfilled, (state, action) => {
|
||||||
const nextCompanyId = action.payload?.id ?? null;
|
const nextCompanyId = action.payload?.id ?? null;
|
||||||
if (nextCompanyId != null && nextCompanyId !== state.companyId) {
|
if (nextCompanyId != null && nextCompanyId !== state.companyId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user