Outsource custom GUI class components to dashgui library
Move reusable GUI primitives (PanelTile, EditablePanelTile, base Tile, ElevatedPill, SingleLineTextEdit) into the shared dashgui package and consume them via a file: dependency. EditablePanelTile now accepts an onError prop so error notification stays app-side.
This commit is contained in:
13
README.md
13
README.md
@@ -27,6 +27,19 @@ To test the full PWA precache (production build + preview, no HMR), use
|
|||||||
Dashback must be running and your Google account must be authorized on the
|
Dashback must be running and your Google account must be authorized on the
|
||||||
backend.
|
backend.
|
||||||
|
|
||||||
|
## Shared GUI library (`dashgui`)
|
||||||
|
|
||||||
|
Reusable custom GUI class components live in the **`dashgui`** package
|
||||||
|
(`../dashgui`), installed here as a `file:` dependency (npm workspace symlink)
|
||||||
|
so source imports are reflected instantly.
|
||||||
|
|
||||||
|
- `dashgui/panels` — `PanelTile`, `PanelTileGrid`, `EditablePanelTile`, layout helpers
|
||||||
|
- `dashgui/tiles` — base `Tile` (extended by `src/components/tiles/*`)
|
||||||
|
- `dashgui/SingleLineTextEdit`, `dashgui/ElevatedPill`
|
||||||
|
|
||||||
|
See `../dashgui/README.md` for details and the dev/mock-page harness used for
|
||||||
|
future test cases.
|
||||||
|
|
||||||
## Auth flow
|
## Auth flow
|
||||||
|
|
||||||
1. On boot the app calls `GET /api/auth/me` with `credentials: 'include'`.
|
1. On boot the app calls `GET /api/auth/me` with `credentials: 'include'`.
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@mui/material": "^9.2.0",
|
"@mui/material": "^9.2.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
|
"dashgui": "file:../dashgui",
|
||||||
"notistack": "^3.0.2",
|
"notistack": "^3.0.2",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
@@ -31,6 +32,27 @@
|
|||||||
"vite-plugin-pwa": "^1.3.0"
|
"vite-plugin-pwa": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../dashgui": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^9.2.0",
|
||||||
|
"@mui/material": "^9.2.0",
|
||||||
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
"react": "^19.2.8",
|
||||||
|
"react-dom": "^19.2.8",
|
||||||
|
"vite": "^8.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^9.0.0",
|
||||||
|
"@mui/material": "^9.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@apideck/better-ajv-errors": {
|
"node_modules/@apideck/better-ajv-errors": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz",
|
||||||
@@ -3575,6 +3597,10 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dashgui": {
|
||||||
|
"resolved": "../dashgui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
"notistack": "^3.0.2",
|
"notistack": "^3.0.2",
|
||||||
|
"dashgui": "file:../dashgui",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
|||||||
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from './BackendError';
|
import BackendError from './BackendError';
|
||||||
import SingleLineTextEdit from './SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { StorageManagerContext } from '../api/storageManagerContext';
|
import { StorageManagerContext } from '../api/storageManagerContext';
|
||||||
import { requireDefined, requireObject } from '../utils/apiShape';
|
import { requireDefined, requireObject } from '../utils/apiShape';
|
||||||
import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm';
|
import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm';
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Switch from '@mui/material/Switch';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { alpha } from '@mui/material/styles';
|
|
||||||
|
|
||||||
/** Base pill with elevation, shared styling, and an overridable trailing element. */
|
|
||||||
class ElevatedPill extends Component {
|
|
||||||
renderTrailing() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { label, active = false, onClick } = this.props;
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
component="label"
|
|
||||||
onClick={onClick}
|
|
||||||
sx={(theme) => ({
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 0.5,
|
|
||||||
px: 1.5,
|
|
||||||
py: 0.25,
|
|
||||||
borderRadius: 999,
|
|
||||||
background: active
|
|
||||||
? `linear-gradient(180deg, ${alpha(theme.palette.primary.main, 0.28)}, ${alpha(
|
|
||||||
theme.palette.primary.main,
|
|
||||||
0.22,
|
|
||||||
)})`
|
|
||||||
: `linear-gradient(180deg, ${alpha(theme.palette.primary.main, 0.16)}, ${alpha(
|
|
||||||
theme.palette.primary.main,
|
|
||||||
0.10,
|
|
||||||
)})`,
|
|
||||||
color: 'text.primary',
|
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none',
|
|
||||||
boxShadow: theme.shadows[1],
|
|
||||||
transition: 'background 150ms ease, box-shadow 150ms ease',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Typography variant="body2" sx={{ lineHeight: 1 }}>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
{this.renderTrailing()}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ToggleElevatedPill extends ElevatedPill {
|
|
||||||
renderTrailing() {
|
|
||||||
const { checked, onToggle } = this.props;
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={checked}
|
|
||||||
onChange={(event) => onToggle(event.target.checked)}
|
|
||||||
sx={(theme) => ({
|
|
||||||
'& .MuiSwitch-switchBase': {
|
|
||||||
color: checked ? 'success.main' : 'grey.300',
|
|
||||||
},
|
|
||||||
'& .Mui-checked': {
|
|
||||||
color: 'success.main',
|
|
||||||
},
|
|
||||||
'& .Mui-checked + .MuiSwitch-track': {
|
|
||||||
backgroundColor: alpha(theme.palette.success.main, 0.35),
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ActionElevatedPill extends ElevatedPill {
|
|
||||||
renderTrailing() {
|
|
||||||
return this.props.icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ElevatedPill;
|
|
||||||
@@ -7,7 +7,7 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import BackendError from './BackendError';
|
import BackendError from './BackendError';
|
||||||
import SingleLineTextEdit from './SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import PageLayout from './PageLayout';
|
|||||||
import { NAV_ITEMS, SUB_NAV, resolveActiveSub } from '../nav';
|
import { NAV_ITEMS, SUB_NAV, resolveActiveSub } from '../nav';
|
||||||
import { isKategorienPath } from '../pages/artikel/categoryRoute';
|
import { isKategorienPath } from '../pages/artikel/categoryRoute';
|
||||||
import { selectRenameMode, setRenameMode } from '../store/categorySlice';
|
import { selectRenameMode, setRenameMode } from '../store/categorySlice';
|
||||||
import { ActionElevatedPill, ToggleElevatedPill } from './ElevatedPill';
|
import { ActionElevatedPill, ToggleElevatedPill } from 'dashgui/ElevatedPill';
|
||||||
import { article, category } from '../api';
|
import { article, category } from '../api';
|
||||||
|
|
||||||
function triggerDownload(blob, filename) {
|
function triggerDownload(blob, filename) {
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centralized single-line name field — same outlined look everywhere.
|
|
||||||
* Props: value, onChange(nextValue), plus optional TextField props.
|
|
||||||
*/
|
|
||||||
class SingleLineTextEdit extends Component {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
inputRef,
|
|
||||||
label = 'Name',
|
|
||||||
disabled = false,
|
|
||||||
readOnly = false,
|
|
||||||
autoFocus = false,
|
|
||||||
autoComplete = 'off',
|
|
||||||
size,
|
|
||||||
fullWidth = true,
|
|
||||||
required = false,
|
|
||||||
sx,
|
|
||||||
slotProps,
|
|
||||||
...rest
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
inputRef={inputRef}
|
|
||||||
fullWidth={fullWidth}
|
|
||||||
required={required}
|
|
||||||
size={size}
|
|
||||||
variant="outlined"
|
|
||||||
label={label}
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
if (!readOnly) onChange(event.target.value);
|
|
||||||
}}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
slotProps={{
|
|
||||||
...slotProps,
|
|
||||||
input: {
|
|
||||||
readOnly,
|
|
||||||
...slotProps?.input,
|
|
||||||
},
|
|
||||||
htmlInput: {
|
|
||||||
spellCheck: false,
|
|
||||||
readOnly,
|
|
||||||
tabIndex: readOnly ? -1 : undefined,
|
|
||||||
...slotProps?.htmlInput,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sx={sx}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SingleLineTextEdit;
|
|
||||||
@@ -2,7 +2,7 @@ import { PureComponent, createRef } from 'react';
|
|||||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
} from '../../pages/artikel/articleRoute';
|
} from '../../pages/artikel/articleRoute';
|
||||||
import { router } from '../../appRouter';
|
import { router } from '../../appRouter';
|
||||||
import { matchPath } from 'react-router-dom';
|
import { matchPath } from 'react-router-dom';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
|
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from 'dashgui/panels/panelTileLayout';
|
||||||
import ArticleAttributesTab from './tabs/ArticleAttributesTab';
|
import ArticleAttributesTab from './tabs/ArticleAttributesTab';
|
||||||
import ArticleCompoundTab from './tabs/ArticleCompoundTab';
|
import ArticleCompoundTab from './tabs/ArticleCompoundTab';
|
||||||
import ArticleHistoryTab from './tabs/ArticleHistoryTab';
|
import ArticleHistoryTab from './tabs/ArticleHistoryTab';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { PanelTileGrid } from '../panels';
|
import { PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import withNavigate from '../withNavigate';
|
import withNavigate from '../withNavigate';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Autocomplete from '@mui/material/Autocomplete';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
@@ -150,6 +150,7 @@ export default class ArticleViewBrand extends PureComponent {
|
|||||||
color="#e9d5ff"
|
color="#e9d5ff"
|
||||||
value={value}
|
value={value}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
|
onError={notifyError}
|
||||||
isDirty={brandDraftDirty}
|
isDirty={brandDraftDirty}
|
||||||
canSave={brandDraftValid}
|
canSave={brandDraftValid}
|
||||||
display={<PanelValue>{value.name || null}</PanelValue>}
|
display={<PanelValue>{value.name || null}</PanelValue>}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import Autocomplete from '@mui/material/Autocomplete';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
import { requireObject } from '../../../utils/apiShape';
|
import { requireObject } from '../../../utils/apiShape';
|
||||||
import { articleEans } from '../../../utils/articleDetail';
|
import { articleEans } from '../../../utils/articleDetail';
|
||||||
|
import { notifyError } from '../../../utils/notify';
|
||||||
class PanelValue extends Component {
|
class PanelValue extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { label, children } = this.props;
|
const { label, children } = this.props;
|
||||||
@@ -115,6 +116,7 @@ export default class ArticleViewEANs extends PureComponent {
|
|||||||
contentAlignItems="flex-start"
|
contentAlignItems="flex-start"
|
||||||
value={value}
|
value={value}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
|
onError={notifyError}
|
||||||
isDirty={eansDraftDirty}
|
isDirty={eansDraftDirty}
|
||||||
canSave={eansDraftValid}
|
canSave={eansDraftValid}
|
||||||
display={<EansDisplay sku={articleData.sku} eans={value.eans} />}
|
display={<EansDisplay sku={articleData.sku} eans={value.eans} />}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Component, PureComponent } from 'react';
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { PanelTile } from '../../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { artikelDetailPath } from '../../../pages/artikel/articleRoute';
|
import { artikelDetailPath } from '../../../pages/artikel/articleRoute';
|
||||||
|
|
||||||
class CountLine extends Component {
|
class CountLine extends Component {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Component, PureComponent } from 'react';
|
import { Component, PureComponent } from 'react';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from 'dashgui/panels';
|
||||||
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
|
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
|
||||||
import { Box, Chip, IconButton } from '@mui/material';
|
import { Box, Chip, IconButton } from '@mui/material';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import CategoryPickerDialog from '../CategoryPickerDialog';
|
import CategoryPickerDialog from '../CategoryPickerDialog';
|
||||||
|
import { notifyError } from '../../../utils/notify';
|
||||||
|
|
||||||
const articleNameTextSx = {
|
const articleNameTextSx = {
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -288,6 +289,7 @@ export default class ArticleViewName extends PureComponent {
|
|||||||
color="#ffffff"
|
color="#ffffff"
|
||||||
value={value}
|
value={value}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
|
onError={notifyError}
|
||||||
isDirty={() => draft != null && draftDirty(draft, value)}
|
isDirty={() => draft != null && draftDirty(draft, value)}
|
||||||
canSave={() => draft != null && draftValid(draft)}
|
canSave={() => draft != null && draftValid(draft)}
|
||||||
display={
|
display={
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box';
|
|||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from 'dashgui/panels';
|
||||||
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
||||||
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
@@ -510,6 +510,7 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
contentAlignItems="flex-start"
|
contentAlignItems="flex-start"
|
||||||
value={priceValue}
|
value={priceValue}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
|
onError={notifyError}
|
||||||
isDirty={priceDraftDirty}
|
isDirty={priceDraftDirty}
|
||||||
canSave={priceDraftValid}
|
canSave={priceDraftValid}
|
||||||
overlay={({ editing, draft, onChange }) => (
|
overlay={({ editing, draft, onChange }) => (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, PureComponent } from 'react';
|
import { Component, PureComponent } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { PanelTile } from '../../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { formatStock } from '../../../pages/artikel/searchFormat';
|
import { formatStock } from '../../../pages/artikel/searchFormat';
|
||||||
|
|
||||||
class PanelValue extends Component {
|
class PanelValue extends Component {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../BackendError';
|
import BackendError from '../BackendError';
|
||||||
import { PanelTile, PanelTileGrid } from '../panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { PureComponent } from 'react';
|
|||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import SingleLineTextEdit from '../SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { manifestConfig, partyDisplayName } from './manifestConfig';
|
import { manifestConfig, partyDisplayName } from './manifestConfig';
|
||||||
|
|
||||||
class ManifestHeader extends PureComponent {
|
class ManifestHeader extends PureComponent {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import TableContainer from '@mui/material/TableContainer';
|
|||||||
import TableHead from '@mui/material/TableHead';
|
import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { LINE_COLUMNS } from './manifestConfig';
|
import { LINE_COLUMNS } from './manifestConfig';
|
||||||
import ManifestFormLine from './ManifestFormLine';
|
import ManifestFormLine from './ManifestFormLine';
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
|||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../BackendError';
|
import BackendError from '../BackendError';
|
||||||
import { PanelTile, PanelTileGrid } from '../panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
import { Component, createRef } from 'react';
|
|
||||||
import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined';
|
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
|
||||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
|
||||||
import Alert from '@mui/material/Alert';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { injectMuiTheme } from './injectMuiTheme';
|
|
||||||
import { notifyError } from '../../utils/notify';
|
|
||||||
import { panelTilePaperSx, TILE_ACTION_PADDING, tileActionIconSx, tileActionSx } from './panelTileLayout';
|
|
||||||
|
|
||||||
function valuesEqual(a, b) {
|
|
||||||
return JSON.stringify(a) === JSON.stringify(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Panel tile with centralized inline-edit chrome.
|
|
||||||
* Pass `onSave` to enable edit UI; omit it for read-only tiles (no edit icon).
|
|
||||||
*
|
|
||||||
* Content:
|
|
||||||
* display — React node shown when not editing
|
|
||||||
* edit — React node, or ( { draft, onChange, inputRef } ) => node, while editing
|
|
||||||
* overlay — optional ( { editing, draft, onChange } ) => node
|
|
||||||
*/
|
|
||||||
class EditablePanelTileBase extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.inputRef = createRef();
|
|
||||||
this.state = {
|
|
||||||
editing: false,
|
|
||||||
draft: null,
|
|
||||||
saving: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(_prevProps, prevState) {
|
|
||||||
if (!prevState.editing && this.state.editing) {
|
|
||||||
this.inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSavedValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDirty() {
|
|
||||||
const { draft, editing } = this.state;
|
|
||||||
if (!editing || draft == null) return false;
|
|
||||||
if (this.props.isDirty) {
|
|
||||||
return this.props.isDirty(draft, this.getSavedValue());
|
|
||||||
}
|
|
||||||
return !valuesEqual(draft, this.getSavedValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
canSave() {
|
|
||||||
const { draft, saving } = this.state;
|
|
||||||
if (saving || !this.isDirty() || draft == null) return false;
|
|
||||||
if (this.props.canSave) {
|
|
||||||
return this.props.canSave(draft, this.getSavedValue());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStartEdit = (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.setState({
|
|
||||||
editing: true,
|
|
||||||
draft: structuredClone(this.getSavedValue()),
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCancel = (event) => {
|
|
||||||
event?.stopPropagation();
|
|
||||||
this.setState({
|
|
||||||
editing: false,
|
|
||||||
draft: null,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDraftChange = (nextDraft) => {
|
|
||||||
this.setState({ draft: nextDraft, error: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEditKeyDown = (event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.handleCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (this.canSave()) {
|
|
||||||
void this.handleSave(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSave = async (event) => {
|
|
||||||
event?.stopPropagation();
|
|
||||||
if (!this.canSave()) return;
|
|
||||||
|
|
||||||
const { onSave } = this.props;
|
|
||||||
const { draft } = this.state;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await onSave(draft);
|
|
||||||
this.setState({ editing: false, draft: null, saving: false, error: null });
|
|
||||||
} catch (err) {
|
|
||||||
const normalized = notifyError(err);
|
|
||||||
this.setState({ error: normalized.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTitle() {
|
|
||||||
const { title, onSave } = this.props;
|
|
||||||
if (!title) return null;
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="overline"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ display: 'block', mb: 1, pr: onSave ? 9 : 0 }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderActions() {
|
|
||||||
const { onSave } = this.props;
|
|
||||||
if (!onSave) return null;
|
|
||||||
|
|
||||||
const { editing, saving } = this.state;
|
|
||||||
const iconSx = tileActionIconSx;
|
|
||||||
|
|
||||||
if (!editing) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
disabled
|
|
||||||
aria-hidden
|
|
||||||
tabIndex={-1}
|
|
||||||
sx={{
|
|
||||||
...tileActionSx,
|
|
||||||
right: 48,
|
|
||||||
visibility: 'hidden',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SaveOutlinedIcon sx={iconSx} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={this.handleStartEdit}
|
|
||||||
aria-label="Bearbeiten"
|
|
||||||
sx={{ ...tileActionSx, right: 4 }}
|
|
||||||
>
|
|
||||||
<EditOutlinedIcon sx={iconSx} />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
onClick={this.handleSave}
|
|
||||||
disabled={!this.canSave()}
|
|
||||||
aria-label="Speichern"
|
|
||||||
color={this.canSave() ? 'primary' : 'default'}
|
|
||||||
sx={{ ...tileActionSx, right: 48 }}
|
|
||||||
>
|
|
||||||
{saving ? (
|
|
||||||
<CircularProgress size={22} color="inherit" />
|
|
||||||
) : (
|
|
||||||
<SaveOutlinedIcon sx={iconSx} />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={this.handleCancel}
|
|
||||||
disabled={saving}
|
|
||||||
aria-label="Abbrechen"
|
|
||||||
sx={{ ...tileActionSx, right: 4 }}
|
|
||||||
>
|
|
||||||
<CloseOutlinedIcon sx={iconSx} />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBody() {
|
|
||||||
const { display, edit } = this.props;
|
|
||||||
const { editing, draft } = this.state;
|
|
||||||
|
|
||||||
if (editing && draft != null) {
|
|
||||||
if (typeof edit === 'function') {
|
|
||||||
return edit({
|
|
||||||
draft,
|
|
||||||
onChange: this.handleDraftChange,
|
|
||||||
inputRef: this.inputRef,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return edit;
|
|
||||||
}
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderOverlayNode() {
|
|
||||||
const { overlay } = this.props;
|
|
||||||
const { editing, draft } = this.state;
|
|
||||||
if (typeof overlay !== 'function') return null;
|
|
||||||
return overlay({
|
|
||||||
editing,
|
|
||||||
draft,
|
|
||||||
onChange: this.handleDraftChange,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
onSave,
|
|
||||||
theme,
|
|
||||||
wide = 1,
|
|
||||||
sx,
|
|
||||||
minHeight = 120,
|
|
||||||
color,
|
|
||||||
contentAlignItems = 'center',
|
|
||||||
} = this.props;
|
|
||||||
const { editing, error } = this.state;
|
|
||||||
const pending = this.isDirty();
|
|
||||||
const actionPadding = onSave ? TILE_ACTION_PADDING : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
elevation={4}
|
|
||||||
sx={panelTilePaperSx({
|
|
||||||
wide,
|
|
||||||
sx,
|
|
||||||
minHeight,
|
|
||||||
editing,
|
|
||||||
pending,
|
|
||||||
color,
|
|
||||||
theme,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.renderActions()}
|
|
||||||
{this.renderOverlayNode()}
|
|
||||||
{this.renderTitle()}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
pr: actionPadding,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: contentAlignItems,
|
|
||||||
}}
|
|
||||||
onKeyDown={editing ? this.handleEditKeyDown : undefined}
|
|
||||||
>
|
|
||||||
{this.renderBody()}
|
|
||||||
</Box>
|
|
||||||
{error ? (
|
|
||||||
<Alert severity="error" sx={{ mt: 1, py: 0 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditablePanelTile = injectMuiTheme(EditablePanelTileBase);
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { Component, createRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
|
||||||
import { injectMuiTheme } from './injectMuiTheme';
|
|
||||||
import {
|
|
||||||
GRID_COLUMNS,
|
|
||||||
TILE_ACTION_PADDING,
|
|
||||||
panelTilePaperSx,
|
|
||||||
tileActionIconSx,
|
|
||||||
tileActionSx,
|
|
||||||
} from './panelTileLayout';
|
|
||||||
|
|
||||||
class PanelTileBase extends Component {
|
|
||||||
renderTitle() {
|
|
||||||
const { title, action } = this.props;
|
|
||||||
if (!title) return null;
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="overline"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ display: 'block', mb: 1, pr: action ? 9 : 0 }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAction() {
|
|
||||||
const { action } = this.props;
|
|
||||||
if (!action) return null;
|
|
||||||
|
|
||||||
const { label, icon: Icon, onClick, busy } = action;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
disabled
|
|
||||||
aria-hidden
|
|
||||||
tabIndex={-1}
|
|
||||||
sx={{
|
|
||||||
...tileActionSx,
|
|
||||||
right: 48,
|
|
||||||
visibility: 'hidden',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SaveOutlinedIcon sx={tileActionIconSx} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
aria-label={label}
|
|
||||||
disabled={busy}
|
|
||||||
sx={{ ...tileActionSx, right: 4 }}
|
|
||||||
>
|
|
||||||
{busy ? (
|
|
||||||
<CircularProgress size={22} color="inherit" />
|
|
||||||
) : (
|
|
||||||
<Icon sx={tileActionIconSx} />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
theme,
|
|
||||||
wide = 1,
|
|
||||||
children,
|
|
||||||
sx,
|
|
||||||
minHeight = 120,
|
|
||||||
color,
|
|
||||||
action,
|
|
||||||
elevation = 4,
|
|
||||||
} = this.props;
|
|
||||||
const actionPadding = action ? TILE_ACTION_PADDING : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
elevation={elevation}
|
|
||||||
sx={panelTilePaperSx({
|
|
||||||
wide,
|
|
||||||
sx,
|
|
||||||
minHeight,
|
|
||||||
color,
|
|
||||||
theme,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.renderAction()}
|
|
||||||
{this.renderTitle()}
|
|
||||||
<Box sx={{ flex: 1, minWidth: 0, pr: actionPadding }}>{children}</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PanelTile = injectMuiTheme(PanelTileBase);
|
|
||||||
|
|
||||||
export function PanelTileGrid({ children, sx }) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: '1fr',
|
|
||||||
sm: 'repeat(2, 1fr)',
|
|
||||||
md: `repeat(${GRID_COLUMNS}, 1fr)`,
|
|
||||||
},
|
|
||||||
gap: 2,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GRID_COLUMNS };
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { PanelTile, PanelTileGrid, GRID_COLUMNS } from './PanelTile';
|
|
||||||
export { EditablePanelTile } from './EditablePanelTile';
|
|
||||||
export { PENDING_EDIT_BORDER } from './panelTileLayout';
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
|
|
||||||
/** Injects MUI theme into class components (replaces removed withTheme HOC). */
|
|
||||||
export function injectMuiTheme(Wrapped) {
|
|
||||||
function ComponentWithTheme(props) {
|
|
||||||
const theme = useTheme();
|
|
||||||
return <Wrapped {...props} theme={theme} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = Wrapped.displayName || Wrapped.name || 'Component';
|
|
||||||
ComponentWithTheme.displayName = `injectMuiTheme(${name})`;
|
|
||||||
return ComponentWithTheme;
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
const GRID_COLUMNS = 4;
|
|
||||||
|
|
||||||
export const PENDING_EDIT_BORDER = '#e11d48';
|
|
||||||
export const EDITING_TILE_BORDER = '#94a3b8';
|
|
||||||
|
|
||||||
function clampWide(wide) {
|
|
||||||
return Math.min(Math.max(wide, 1), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function responsiveColumnSpan(wide) {
|
|
||||||
const span = clampWide(wide);
|
|
||||||
return {
|
|
||||||
xs: '1 / -1',
|
|
||||||
sm: span >= 2 ? 'span 2' : 'span 1',
|
|
||||||
md: `span ${span}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PENDING_EDIT_RING_PX = 4;
|
|
||||||
export const TILE_ACTION_PADDING = 12;
|
|
||||||
|
|
||||||
export const tileActionSx = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 4,
|
|
||||||
color: 'text.secondary',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tileActionIconSx = {
|
|
||||||
fontSize: 28,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PANEL_TILE_BORDER_RADIUS = 4;
|
|
||||||
|
|
||||||
function tileRingBoxShadow(ringColor, elevationShadow) {
|
|
||||||
const inset = `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${ringColor}`;
|
|
||||||
return elevationShadow ? `${inset}, ${elevationShadow}` : inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function panelTilePaperSx({
|
|
||||||
wide,
|
|
||||||
sx,
|
|
||||||
minHeight = 120,
|
|
||||||
editing = false,
|
|
||||||
pending = false,
|
|
||||||
color,
|
|
||||||
theme,
|
|
||||||
}) {
|
|
||||||
const elevationShadow = theme?.shadows?.[4];
|
|
||||||
const ringColor = pending
|
|
||||||
? PENDING_EDIT_BORDER
|
|
||||||
: editing
|
|
||||||
? EDITING_TILE_BORDER
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridColumn: responsiveColumnSpan(wide),
|
|
||||||
position: 'relative',
|
|
||||||
p: 2,
|
|
||||||
minHeight,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: PANEL_TILE_BORDER_RADIUS,
|
|
||||||
...(color ? { bgcolor: color } : {}),
|
|
||||||
...(ringColor
|
|
||||||
? {
|
|
||||||
boxShadow: tileRingBoxShadow(ringColor, elevationShadow),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...sx,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GRID_COLUMNS };
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Tile } from './Tile';
|
import { Tile } from 'dashgui/tiles';
|
||||||
|
|
||||||
export class ActionTile extends Tile {
|
export class ActionTile extends Tile {
|
||||||
renderIcon() {
|
renderIcon() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import ButtonBase from '@mui/material/ButtonBase';
|
import ButtonBase from '@mui/material/ButtonBase';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Tile, tileBaseSx } from './Tile';
|
import { Tile, tileBaseSx } from 'dashgui/tiles';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
import { articleSku } from '../../pages/artikel/articleRoute';
|
import { articleSku } from '../../pages/artikel/articleRoute';
|
||||||
import ArticlePhoto from '../article/ArticlePhoto';
|
import ArticlePhoto from '../article/ArticlePhoto';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Tile, tileBaseSx } from './Tile';
|
import { Tile, tileBaseSx } from 'dashgui/tiles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BackTile renders as a <div> (Box) instead of <button> (ButtonBase)
|
* BackTile renders as a <div> (Box) instead of <button> (ButtonBase)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
|||||||
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
||||||
import ThumbHashImage from '../article/ThumbHashImage';
|
import ThumbHashImage from '../article/ThumbHashImage';
|
||||||
import { thumb } from '../../api';
|
import { thumb } from '../../api';
|
||||||
import { tileBaseSx } from './Tile';
|
import { tileBaseSx } from 'dashgui/tiles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top row (1/3): sort handle | settings toggle | move handle.
|
* Top row (1/3): sort handle | settings toggle | move handle.
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import ButtonBase from '@mui/material/ButtonBase';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
|
|
||||||
export const TILE_SIZE = 140;
|
|
||||||
|
|
||||||
/** Matches panel tile rounding (theme.shape.borderRadius × 4 → typically 16px). */
|
|
||||||
export const TILE_BORDER_RADIUS = 4;
|
|
||||||
|
|
||||||
export const tileBaseSx = {
|
|
||||||
aspectRatio: '1 / 1',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 1,
|
|
||||||
p: 1.5,
|
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none',
|
|
||||||
textAlign: 'center',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: TILE_BORDER_RADIUS,
|
|
||||||
overflow: 'hidden',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
boxShadow: 3,
|
|
||||||
transition: 'box-shadow 0.15s, background-color 0.15s',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'action.hover',
|
|
||||||
boxShadow: 6,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Tile extends Component {
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onClick?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
renderIcon() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLabel() {
|
|
||||||
const { label } = this.props;
|
|
||||||
if (!label) return null;
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.2 }}>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSecondary() {
|
|
||||||
const { secondary } = this.props;
|
|
||||||
if (!secondary) return null;
|
|
||||||
return (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}>
|
|
||||||
{secondary}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderExtra() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { sx, disabled } = this.props;
|
|
||||||
return (
|
|
||||||
<ButtonBase
|
|
||||||
component="button"
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
sx={{
|
|
||||||
...tileBaseSx,
|
|
||||||
position: 'relative',
|
|
||||||
...(disabled
|
|
||||||
? { opacity: 0.5, pointerEvents: 'none' }
|
|
||||||
: {}),
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.renderIcon()}
|
|
||||||
{this.renderLabel()}
|
|
||||||
{this.renderSecondary()}
|
|
||||||
{this.renderExtra()}
|
|
||||||
</ButtonBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { TILE_SIZE, Tile, tileBaseSx } from './Tile';
|
export { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
|
||||||
export { BackTile } from './BackTile';
|
export { BackTile } from './BackTile';
|
||||||
export { CategoryTile, CategoryEditTile } from './CategoryTile';
|
export { CategoryTile, CategoryEditTile } from './CategoryTile';
|
||||||
export { ArticleTile } from './ArticleTile';
|
export { ArticleTile } from './ArticleTile';
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
BackTile,
|
BackTile,
|
||||||
CategoryEditTile,
|
CategoryEditTile,
|
||||||
CategoryTile,
|
CategoryTile,
|
||||||
TILE_SIZE,
|
|
||||||
} from '../../components/tiles';
|
} from '../../components/tiles';
|
||||||
|
import { TILE_SIZE } from 'dashgui/tiles';
|
||||||
import withCategoryRoute from './withCategoryRoute';
|
import withCategoryRoute from './withCategoryRoute';
|
||||||
import { artikelDetailPath, articleSku } from './articleRoute';
|
import { artikelDetailPath, articleSku } from './articleRoute';
|
||||||
import { selectCompanyId } from '../../store/companySlice';
|
import { selectCompanyId } from '../../store/companySlice';
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|||||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
|||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { router } from '../../appRouter';
|
import { router } from '../../appRouter';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import Select from '@mui/material/Select';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { router } from '../../appRouter';
|
import { router } from '../../appRouter';
|
||||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
|||||||
import StorefrontIcon from '@mui/icons-material/Storefront';
|
import StorefrontIcon from '@mui/icons-material/Storefront';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|||||||
Reference in New Issue
Block a user