u
This commit is contained in:
@@ -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 (
|
||||
<NameDialog
|
||||
open={open}
|
||||
title="Neuer Artikel"
|
||||
submitLabel="Anlegen"
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<Dialog open={open} onClose={this.handleClose} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Neuer Artikel</DialogTitle>
|
||||
|
||||
{!isBulk && (
|
||||
<Box component="form" onSubmit={this.handleSingleSubmit} noValidate>
|
||||
<DialogContent>
|
||||
{error ? (
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
<SingleLineTextEdit
|
||||
required
|
||||
autoFocus
|
||||
label="Name"
|
||||
value={name}
|
||||
disabled={busy}
|
||||
onChange={(nextName) => this.setState({ name: nextName })}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button type="button" onClick={this.openBulk} disabled={busy}>
|
||||
Sammelanlegen
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button type="button" onClick={this.handleClose} disabled={busy}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={busy || !canSubmitSingle}
|
||||
>
|
||||
Anlegen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isBulk && (
|
||||
<Box component="form" onSubmit={this.handleBulkSubmit} noValidate>
|
||||
<DialogContent>
|
||||
{error ? (
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
<SingleLineTextEdit
|
||||
autoFocus
|
||||
multiline
|
||||
minRows={6}
|
||||
label="Namen (einer pro Zeile)"
|
||||
placeholder={'Artikel A\nArtikel B\nArtikel C'}
|
||||
value={bulkText}
|
||||
disabled={busy}
|
||||
onChange={(nextText) => this.setState({ bulkText: nextText })}
|
||||
helperText={
|
||||
bulkNames.length > 0
|
||||
? `${bulkNames.length} Artikel werden angelegt.`
|
||||
: 'Ein Name pro Zeile eingeben.'
|
||||
}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => this.setState({ mode: SINGLE_MODE, error: null })}
|
||||
disabled={busy}
|
||||
>
|
||||
Einzel
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button type="button" onClick={this.handleClose} disabled={busy}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={busy || !canSubmitBulk}
|
||||
>
|
||||
Anlegen ({bulkNames.length})
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ArticleNameDialog;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<NameDialog
|
||||
open={open}
|
||||
title={TITLES[mode]}
|
||||
submitLabel={SUBMIT_LABELS[mode]}
|
||||
initialName={initialName}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<Dialog open={open} onClose={this.handleClose} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>{TITLES[mode]}</DialogTitle>
|
||||
|
||||
{!isBulk && (
|
||||
<Box component="form" onSubmit={this.handleSingleSubmit} noValidate>
|
||||
<DialogContent>
|
||||
{error ? (
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
<SingleLineTextEdit
|
||||
required
|
||||
autoFocus
|
||||
label="Name"
|
||||
value={name}
|
||||
disabled={busy}
|
||||
onChange={(nextName) => this.setState({ name: nextName })}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
{canBulk && (
|
||||
<Button type="button" onClick={this.openBulk} disabled={busy}>
|
||||
Sammelanlegen
|
||||
</Button>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button type="button" onClick={this.handleClose} disabled={busy}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={busy || !canSubmitSingle}
|
||||
>
|
||||
{SUBMIT_LABELS[mode]}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isBulk && (
|
||||
<Box component="form" onSubmit={this.handleBulkSubmit} noValidate>
|
||||
<DialogContent>
|
||||
{error ? (
|
||||
isBackendFailure(error) ? (
|
||||
<BackendError error={error} sx={{ mb: 2 }} />
|
||||
) : (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
|
||||
<SingleLineTextEdit
|
||||
autoFocus
|
||||
multiline
|
||||
minRows={6}
|
||||
label="Namen (einer pro Zeile)"
|
||||
placeholder={'Kategorie A\nKategorie B\nKategorie C'}
|
||||
value={bulkText}
|
||||
disabled={busy}
|
||||
onChange={(nextText) => this.setState({ bulkText: nextText })}
|
||||
helperText={
|
||||
bulkNames.length > 0
|
||||
? `${bulkNames.length} Kategorien werden angelegt.`
|
||||
: 'Ein Name pro Zeile eingeben.'
|
||||
}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => this.setState({ mode: SINGLE_MODE, error: null })}
|
||||
disabled={busy}
|
||||
>
|
||||
Einzel
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button type="button" onClick={this.handleClose} disabled={busy}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={busy || !canSubmitBulk}
|
||||
>
|
||||
Anlegen ({bulkNames.length})
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryNameDialog;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -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 (
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={this.handleClick}
|
||||
{...rest}
|
||||
sx={{
|
||||
...tileBaseSx,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<CategoryBackdrop thumbHash={thumbHash} fullHash={fullHash} />
|
||||
<Box sx={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||
{this.renderLabel()}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = (
|
||||
<CategoryBackdrop thumbHash={thumbHash} fullHash={fullHash} />
|
||||
);
|
||||
|
||||
// Only the overlay differs between normal and edit mode.
|
||||
const overlay = edit ? (
|
||||
<>
|
||||
<TopBand
|
||||
onSettingsClick={this.handleSettingsToggle}
|
||||
settingsTab={settingsTab}
|
||||
terminalCount={terminals?.length || 0}
|
||||
/>
|
||||
<Box
|
||||
onClick={this.handleMiddleBandClick}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '33.33%',
|
||||
bottom: '33.33%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
cursor: settingsTab === -2 ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
<MiddleBand
|
||||
settingsTab={settingsTab}
|
||||
nameInput={nameInput}
|
||||
terminals={terminals}
|
||||
categoryPosterminalIds={categoryPosterminalIds}
|
||||
onTerminalToggle={onTerminalToggle}
|
||||
fullHash={fullHash}
|
||||
/>
|
||||
</Box>
|
||||
<BottomRow onOpen={onOpen} editBand={editBand} />
|
||||
</>
|
||||
) : (
|
||||
this.renderLabel()
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...rest}
|
||||
role={edit ? undefined : 'button'}
|
||||
tabIndex={edit ? undefined : 0}
|
||||
onClick={edit ? undefined : this.handleClick}
|
||||
onKeyDown={edit ? undefined : this.handleKeyDown}
|
||||
sx={{
|
||||
...tileBaseSx,
|
||||
cursor: 'default',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&:hover': { bgcolor: 'background.paper', boxShadow: 0 },
|
||||
...(edit
|
||||
? { cursor: 'default', '&:hover': { bgcolor: 'background.paper', boxShadow: 0 } }
|
||||
: { cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<CategoryBackdrop thumbHash={thumbHash} fullHash={categoryFullHash} />
|
||||
{backdrop}
|
||||
<Box sx={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||
<TopBand
|
||||
onSettingsClick={this.handleSettingsToggle}
|
||||
settingsTab={settingsTab}
|
||||
terminalCount={terminals?.length || 0}
|
||||
/>
|
||||
<Box
|
||||
onClick={this.handleMiddleBandClick}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '33.33%',
|
||||
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} />
|
||||
{overlay}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user