Route article detail tabs in the URL and pin a preferred default.
Use /artikel/:sku/:tab with a localStorage-backed default, and a pin control to set which tab opens first.
This commit is contained in:
@@ -81,7 +81,7 @@ class App extends Component {
|
|||||||
<Route index element={<ArtikelUebersichtPage />} />
|
<Route index element={<ArtikelUebersichtPage />} />
|
||||||
<Route path="kategorien/*" element={<RedirectArtikelKategorien />} />
|
<Route path="kategorien/*" element={<RedirectArtikelKategorien />} />
|
||||||
<Route path="lager" element={<ArtikelLagerPage />} />
|
<Route path="lager" element={<ArtikelLagerPage />} />
|
||||||
<Route path=":sku" element={<ArtikelSkuPage />} />
|
<Route path=":sku/:tab?" element={<ArtikelSkuPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="auftrag" element={<SectionLayout sectionPath="/auftrag" />}>
|
<Route path="auftrag" element={<SectionLayout sectionPath="/auftrag" />}>
|
||||||
<Route index element={<AuftragAktivePage />} />
|
<Route index element={<AuftragAktivePage />} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Tabs from '@mui/material/Tabs';
|
import Tabs from '@mui/material/Tabs';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
@@ -11,56 +12,55 @@ import HistoryIcon from '@mui/icons-material/History';
|
|||||||
import LabelIcon from '@mui/icons-material/Label';
|
import LabelIcon from '@mui/icons-material/Label';
|
||||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
||||||
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import { article } from '../../api';
|
import { article } from '../../api';
|
||||||
|
import {
|
||||||
|
ARTICLE_TAB,
|
||||||
|
DEFAULT_ARTICLE_TAB,
|
||||||
|
articleShowsCompoundTab,
|
||||||
|
readPreferredArticleTab,
|
||||||
|
writePreferredArticleTab,
|
||||||
|
} from '../../pages/artikel/articleRoute';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from '../panels';
|
||||||
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
|
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
|
||||||
|
|
||||||
const TAB = {
|
|
||||||
history: 'history',
|
|
||||||
supplier: 'supplier',
|
|
||||||
seo: 'seo',
|
|
||||||
label: 'label',
|
|
||||||
attributes: 'attributes',
|
|
||||||
compound: 'compound',
|
|
||||||
settings: 'settings',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALWAYS_TABS = [
|
const ALWAYS_TABS = [
|
||||||
{ id: TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
|
{ id: ARTICLE_TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
|
||||||
{ id: TAB.supplier, label: 'Supplier', color: '#bbf7d0', Icon: LocalShippingIcon },
|
{ id: ARTICLE_TAB.supplier, label: 'Supplier', color: '#bbf7d0', Icon: LocalShippingIcon },
|
||||||
{ id: TAB.seo, label: 'SEO & Beschreibung', color: '#99f6e4', Icon: ManageSearchIcon },
|
{
|
||||||
{ id: TAB.label, label: 'Label', color: '#fde68a', Icon: LabelIcon },
|
id: ARTICLE_TAB.seo,
|
||||||
{ id: TAB.attributes, label: 'Attribute und Merkmale', color: '#bfdbfe', Icon: CategoryIcon },
|
label: 'SEO & Beschreibung',
|
||||||
|
color: '#99f6e4',
|
||||||
|
Icon: ManageSearchIcon,
|
||||||
|
},
|
||||||
|
{ id: ARTICLE_TAB.label, label: 'Label', color: '#fde68a', Icon: LabelIcon },
|
||||||
|
{
|
||||||
|
id: ARTICLE_TAB.attributes,
|
||||||
|
label: 'Attribute und Merkmale',
|
||||||
|
color: '#bfdbfe',
|
||||||
|
Icon: CategoryIcon,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const COMPOUND_TAB = {
|
const COMPOUND_TAB = {
|
||||||
id: TAB.compound,
|
id: ARTICLE_TAB.compound,
|
||||||
label: 'Compound Editor',
|
label: 'Compound Editor',
|
||||||
color: '#e9d5ff',
|
color: '#e9d5ff',
|
||||||
Icon: AccountTreeIcon,
|
Icon: AccountTreeIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_TAB = {
|
const SETTINGS_TAB = {
|
||||||
id: TAB.settings,
|
id: ARTICLE_TAB.settings,
|
||||||
label: 'Einstellungen',
|
label: 'Einstellungen',
|
||||||
color: '#fca5a5',
|
color: '#fca5a5',
|
||||||
Icon: SettingsIcon,
|
Icon: SettingsIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isCompoundArticle(article) {
|
|
||||||
return (
|
|
||||||
article?.articleListCount > 0 ||
|
|
||||||
article?.optionsCount > 0 ||
|
|
||||||
article?.childCount > 0 ||
|
|
||||||
article?.fatherArticleId != null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function visibleTabs(article) {
|
function visibleTabs(article) {
|
||||||
const tabs = [...ALWAYS_TABS];
|
const tabs = [...ALWAYS_TABS];
|
||||||
if (isCompoundArticle(article)) {
|
if (articleShowsCompoundTab(article)) {
|
||||||
tabs.push(COMPOUND_TAB);
|
tabs.push(COMPOUND_TAB);
|
||||||
}
|
}
|
||||||
tabs.push(SETTINGS_TAB);
|
tabs.push(SETTINGS_TAB);
|
||||||
@@ -86,7 +86,7 @@ function PanelValue({ label, children }) {
|
|||||||
function Placeholder({ label }) {
|
function Placeholder({ label }) {
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{label} — noch nicht verfügbar.
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,21 +94,24 @@ function Placeholder({ label }) {
|
|||||||
class ArticleTileTab extends Component {
|
class ArticleTileTab extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { tab: TAB.seo, busy: false };
|
this.state = {
|
||||||
|
busy: false,
|
||||||
|
preferredTab: readPreferredArticleTab(),
|
||||||
|
};
|
||||||
this.handleTabChange = this.handleTabChange.bind(this);
|
this.handleTabChange = this.handleTabChange.bind(this);
|
||||||
|
this.handlePinDefault = this.handlePinDefault.bind(this);
|
||||||
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
|
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
handleTabChange(_event, tab) {
|
||||||
if (prevProps.article === this.props.article) return;
|
this.props.onTabChange?.(tab);
|
||||||
const tabs = visibleTabs(this.props.article);
|
|
||||||
if (!tabs.some((row) => row.id === this.state.tab)) {
|
|
||||||
this.setState({ tab: TAB.seo });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTabChange(_event, tab) {
|
handlePinDefault() {
|
||||||
this.setState({ tab });
|
const { tab } = this.props;
|
||||||
|
if (!tab) return;
|
||||||
|
writePreferredArticleTab(tab);
|
||||||
|
this.setState({ preferredTab: tab });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteArticle() {
|
async handleDeleteArticle() {
|
||||||
@@ -159,20 +162,20 @@ class ArticleTileTab extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPanel() {
|
renderPanel() {
|
||||||
switch (this.state.tab) {
|
switch (this.props.tab) {
|
||||||
case TAB.history:
|
case ARTICLE_TAB.history:
|
||||||
return <Placeholder label="History" />;
|
return <Placeholder label="History" />;
|
||||||
case TAB.supplier:
|
case ARTICLE_TAB.supplier:
|
||||||
return <Placeholder label="Supplier" />;
|
return <Placeholder label="Supplier" />;
|
||||||
case TAB.seo:
|
case ARTICLE_TAB.seo:
|
||||||
return this.renderSeo();
|
return this.renderSeo();
|
||||||
case TAB.label:
|
case ARTICLE_TAB.label:
|
||||||
return <Placeholder label="Label" />;
|
return <Placeholder label="Label" />;
|
||||||
case TAB.attributes:
|
case ARTICLE_TAB.attributes:
|
||||||
return <Placeholder label="Attribute und Merkmale" />;
|
return <Placeholder label="Attribute und Merkmale" />;
|
||||||
case TAB.compound:
|
case ARTICLE_TAB.compound:
|
||||||
return <Placeholder label="Compound Editor" />;
|
return <Placeholder label="Compound Editor" />;
|
||||||
case TAB.settings:
|
case ARTICLE_TAB.settings:
|
||||||
return this.renderSettings();
|
return this.renderSettings();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -180,11 +183,24 @@ class ArticleTileTab extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { article: articleData, wide = 3, minHeight = 200, sx, tileKey } = this.props;
|
const {
|
||||||
|
article: articleData,
|
||||||
|
wide = 3,
|
||||||
|
minHeight = 200,
|
||||||
|
sx,
|
||||||
|
tileKey,
|
||||||
|
tab = DEFAULT_ARTICLE_TAB,
|
||||||
|
} = this.props;
|
||||||
|
const { preferredTab } = this.state;
|
||||||
const tabs = visibleTabs(articleData);
|
const tabs = visibleTabs(articleData);
|
||||||
const { tab } = this.state;
|
const active =
|
||||||
const active = tabs.find((row) => row.id === tab) ?? ALWAYS_TABS.find((row) => row.id === TAB.seo);
|
tabs.find((row) => row.id === tab) ??
|
||||||
|
ALWAYS_TABS.find((row) => row.id === DEFAULT_ARTICLE_TAB);
|
||||||
const activeColor = active?.color;
|
const activeColor = active?.color;
|
||||||
|
const effectivePreferred = tabs.some((row) => row.id === preferredTab)
|
||||||
|
? preferredTab
|
||||||
|
: DEFAULT_ARTICLE_TAB;
|
||||||
|
const isPinnedDefault = tab === effectivePreferred;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -197,72 +213,110 @@ class ArticleTileTab extends Component {
|
|||||||
'drop-shadow(0px 2px 4px rgba(0,0,0,0.2)) drop-shadow(0px 4px 5px rgba(0,0,0,0.14)) drop-shadow(0px 1px 10px rgba(0,0,0,0.12))',
|
'drop-shadow(0px 2px 4px rgba(0,0,0,0.2)) drop-shadow(0px 4px 5px rgba(0,0,0,0.14)) drop-shadow(0px 1px 10px rgba(0,0,0,0.12))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Box
|
||||||
value={tab}
|
|
||||||
onChange={this.handleTabChange}
|
|
||||||
variant="standard"
|
|
||||||
sx={{
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
gap: 0.5,
|
||||||
|
minWidth: 0,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
minHeight: 56,
|
|
||||||
px: 0,
|
|
||||||
mb: 0,
|
|
||||||
borderBottom: 'none',
|
|
||||||
overflow: 'visible',
|
|
||||||
'& .MuiTabs-scroller': {
|
|
||||||
px: 0,
|
|
||||||
borderBottom: 'none',
|
|
||||||
overflow: 'visible !important',
|
|
||||||
},
|
|
||||||
'& .MuiTabs-list, & .MuiTabs-flexContainer': {
|
|
||||||
flexWrap: 'wrap-reverse',
|
|
||||||
columnGap: 3,
|
|
||||||
rowGap: 0,
|
|
||||||
alignItems: 'stretch',
|
|
||||||
alignContent: 'flex-end',
|
|
||||||
},
|
|
||||||
'& .MuiTabs-indicator': { display: 'none' },
|
|
||||||
'& .MuiTab-root': {
|
|
||||||
flexShrink: 0,
|
|
||||||
maxWidth: 'none',
|
|
||||||
minHeight: 56,
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((row) => {
|
<Tabs
|
||||||
const selected = tab === row.id;
|
value={tab}
|
||||||
const { Icon } = row;
|
onChange={this.handleTabChange}
|
||||||
return (
|
variant="standard"
|
||||||
<Tab
|
sx={{
|
||||||
key={row.id}
|
flex: 1,
|
||||||
value={row.id}
|
minWidth: 0,
|
||||||
aria-label={row.label}
|
minHeight: 56,
|
||||||
title={row.label}
|
px: 0,
|
||||||
icon={<Icon sx={{ fontSize: 40 }} />}
|
mb: 0,
|
||||||
sx={{
|
borderBottom: 'none',
|
||||||
minHeight: 56,
|
overflow: 'visible',
|
||||||
minWidth: 64,
|
'& .MuiTabs-scroller': {
|
||||||
py: 0.5,
|
px: 0,
|
||||||
px: 1.25,
|
borderBottom: 'none',
|
||||||
border: 'none',
|
overflow: 'visible !important',
|
||||||
borderBottom: 'none',
|
},
|
||||||
borderRadius: '8px 8px 0 0',
|
'& .MuiTabs-list, & .MuiTabs-flexContainer': {
|
||||||
textTransform: 'none',
|
flexWrap: 'wrap-reverse',
|
||||||
bgcolor: row.color,
|
columnGap: 3,
|
||||||
opacity: selected ? 1 : 0.72,
|
rowGap: 0,
|
||||||
color: 'text.primary',
|
alignItems: 'stretch',
|
||||||
zIndex: selected ? 2 : 1,
|
alignContent: 'flex-end',
|
||||||
boxShadow: 'none',
|
},
|
||||||
'&.Mui-selected': {
|
'& .MuiTabs-indicator': { display: 'none' },
|
||||||
color: 'text.primary',
|
'& .MuiTab-root': {
|
||||||
|
flexShrink: 0,
|
||||||
|
maxWidth: 'none',
|
||||||
|
minHeight: 56,
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((row) => {
|
||||||
|
const selected = tab === row.id;
|
||||||
|
const { Icon } = row;
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
key={row.id}
|
||||||
|
value={row.id}
|
||||||
|
aria-label={row.label}
|
||||||
|
title={row.label}
|
||||||
|
icon={<Icon sx={{ fontSize: 40 }} />}
|
||||||
|
sx={{
|
||||||
|
minHeight: 56,
|
||||||
|
minWidth: 64,
|
||||||
|
py: 0.5,
|
||||||
|
px: 1.25,
|
||||||
|
border: 'none',
|
||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
},
|
borderRadius: '8px 8px 0 0',
|
||||||
}}
|
textTransform: 'none',
|
||||||
/>
|
bgcolor: row.color,
|
||||||
);
|
opacity: selected ? 1 : 0.72,
|
||||||
})}
|
color: 'text.primary',
|
||||||
</Tabs>
|
zIndex: selected ? 2 : 1,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'text.primary',
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
aria-label={
|
||||||
|
isPinnedDefault
|
||||||
|
? 'Standard-Tab'
|
||||||
|
: 'Als Standard-Tab festlegen'
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
isPinnedDefault
|
||||||
|
? 'Standard-Tab'
|
||||||
|
: 'Als Standard-Tab festlegen'
|
||||||
|
}
|
||||||
|
onClick={this.handlePinDefault}
|
||||||
|
disabled={isPinnedDefault}
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
mb: 0.75,
|
||||||
|
color: isPinnedDefault ? 'success.main' : 'action.active',
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
color: 'success.main',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PushPinIcon sx={{ fontSize: 28 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
<PanelTile
|
<PanelTile
|
||||||
wide={wide}
|
wide={wide}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { PANEL_TILE_KEYS } from '../../theme';
|
import { PANEL_TILE_KEYS } from '../../theme';
|
||||||
import { formatStock } from '../../pages/artikel/searchFormat';
|
import { formatStock } from '../../pages/artikel/searchFormat';
|
||||||
|
import { artikelDetailPath } from '../../pages/artikel/articleRoute';
|
||||||
import {
|
import {
|
||||||
buildCategoryPath,
|
buildCategoryPath,
|
||||||
defaultCategoryPath,
|
defaultCategoryPath,
|
||||||
@@ -551,7 +552,12 @@ function PriceUnitCorner({ hasUnit, onToggleHasUnit, onOpenEditor }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleView({ article: articleData, onArticleUpdated }) {
|
export default function ArticleView({
|
||||||
|
article: articleData,
|
||||||
|
tab,
|
||||||
|
onTabChange,
|
||||||
|
onArticleUpdated,
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
|
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
|
||||||
@@ -891,6 +897,8 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
tileKey={PANEL_TILE_KEYS.content}
|
tileKey={PANEL_TILE_KEYS.content}
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
article={articleData}
|
article={articleData}
|
||||||
|
tab={tab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
onArticleDeleted={handleArticleDeleted}
|
onArticleDeleted={handleArticleDeleted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -908,7 +916,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
{articleData.fatherSku ? (
|
{articleData.fatherSku ? (
|
||||||
<Link
|
<Link
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={`/artikel/${encodeURIComponent(articleData.fatherSku)}`}
|
to={artikelDetailPath(articleData.fatherSku)}
|
||||||
>
|
>
|
||||||
{articleData.fatherSku}
|
{articleData.fatherSku}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,11 +1,108 @@
|
|||||||
|
export const ARTICLE_TAB = {
|
||||||
|
history: 'history',
|
||||||
|
supplier: 'supplier',
|
||||||
|
seo: 'seo',
|
||||||
|
label: 'label',
|
||||||
|
attributes: 'attributes',
|
||||||
|
compound: 'compound',
|
||||||
|
settings: 'settings',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Factory fallback when nothing is stored in localStorage. */
|
||||||
|
export const DEFAULT_ARTICLE_TAB = ARTICLE_TAB.seo;
|
||||||
|
|
||||||
|
const PREFERRED_ARTICLE_TAB_KEY = 'dashfront:article-default-tab';
|
||||||
|
|
||||||
|
const ALL_ARTICLE_TAB_IDS = new Set(Object.values(ARTICLE_TAB));
|
||||||
|
|
||||||
|
const BASE_ARTICLE_TAB_IDS = [
|
||||||
|
ARTICLE_TAB.history,
|
||||||
|
ARTICLE_TAB.supplier,
|
||||||
|
ARTICLE_TAB.seo,
|
||||||
|
ARTICLE_TAB.label,
|
||||||
|
ARTICLE_TAB.attributes,
|
||||||
|
];
|
||||||
|
|
||||||
export function articleSku(article) {
|
export function articleSku(article) {
|
||||||
if (!article) return null;
|
if (!article) return null;
|
||||||
const sku = article.sku?.trim();
|
const sku = article.sku?.trim();
|
||||||
return sku || null;
|
return sku || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function artikelDetailPath(article) {
|
export function articleShowsCompoundTab(article) {
|
||||||
const sku = articleSku(article);
|
return (
|
||||||
if (!sku) return null;
|
article?.articleListCount > 0 ||
|
||||||
return `/artikel/${encodeURIComponent(sku)}`;
|
article?.optionsCount > 0 ||
|
||||||
|
article?.childCount > 0 ||
|
||||||
|
article?.fatherArticleId != null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function articleTabIds(article) {
|
||||||
|
const ids = [...BASE_ARTICLE_TAB_IDS];
|
||||||
|
if (articleShowsCompoundTab(article)) {
|
||||||
|
ids.push(ARTICLE_TAB.compound);
|
||||||
|
}
|
||||||
|
ids.push(ARTICLE_TAB.settings);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArticleTabId(tab) {
|
||||||
|
return ALL_ARTICLE_TAB_IDS.has(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readPreferredArticleTab() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(PREFERRED_ARTICLE_TAB_KEY);
|
||||||
|
if (raw && isArticleTabId(raw)) return raw;
|
||||||
|
} catch {
|
||||||
|
// ignore quota / private mode
|
||||||
|
}
|
||||||
|
return DEFAULT_ARTICLE_TAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writePreferredArticleTab(tab) {
|
||||||
|
if (!isArticleTabId(tab)) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(PREFERRED_ARTICLE_TAB_KEY, tab);
|
||||||
|
} catch {
|
||||||
|
// ignore quota / private mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preferred default, clamped to tabs visible for this article. */
|
||||||
|
export function preferredArticleTab(article) {
|
||||||
|
const preferred = readPreferredArticleTab();
|
||||||
|
const ids = articleTabIds(article);
|
||||||
|
if (ids.includes(preferred)) return preferred;
|
||||||
|
return DEFAULT_ARTICLE_TAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveArticleTab(tabParam, article) {
|
||||||
|
const ids = articleTabIds(article);
|
||||||
|
if (tabParam && ids.includes(tabParam)) return tabParam;
|
||||||
|
return preferredArticleTab(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object|string} articleOrSku
|
||||||
|
* @param {string} [tab] omitted → preferred default from localStorage
|
||||||
|
*/
|
||||||
|
export function artikelDetailPath(articleOrSku, tab) {
|
||||||
|
const sku =
|
||||||
|
typeof articleOrSku === 'string'
|
||||||
|
? articleOrSku.trim() || null
|
||||||
|
: articleSku(articleOrSku);
|
||||||
|
if (!sku) return null;
|
||||||
|
|
||||||
|
let segment;
|
||||||
|
if (isArticleTabId(tab)) {
|
||||||
|
segment = tab;
|
||||||
|
} else if (articleOrSku && typeof articleOrSku === 'object') {
|
||||||
|
segment = preferredArticleTab(articleOrSku);
|
||||||
|
} else {
|
||||||
|
segment = readPreferredArticleTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/artikel/${encodeURIComponent(sku)}/${segment}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
parseFilter,
|
parseFilter,
|
||||||
} from './searchFormat';
|
} from './searchFormat';
|
||||||
import { preserveSearchFocus } from './searchFocus';
|
import { preserveSearchFocus } from './searchFocus';
|
||||||
|
import { artikelDetailPath } from './articleRoute';
|
||||||
import withSearchParams from './withSearchParams';
|
import withSearchParams from './withSearchParams';
|
||||||
|
|
||||||
const resultRowSx = {
|
const resultRowSx = {
|
||||||
@@ -430,7 +431,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.persistQuery();
|
this.persistQuery();
|
||||||
this.props.navigate(`/artikel/${encodeURIComponent(row.sku)}`);
|
this.props.navigate(artikelDetailPath(row));
|
||||||
}}
|
}}
|
||||||
sx={resultRowSx}
|
sx={resultRowSx}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import { ArticleView } from '../../components/article';
|
|||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
|
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
|
||||||
|
import {
|
||||||
|
artikelDetailPath,
|
||||||
|
resolveArticleTab,
|
||||||
|
} from './articleRoute';
|
||||||
|
|
||||||
export default function ArtikelSkuPage() {
|
export default function ArtikelSkuPage() {
|
||||||
const { sku: skuParam } = useParams();
|
const { sku: skuParam, tab: tabParam } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -69,6 +73,14 @@ export default function ArtikelSkuPage() {
|
|||||||
};
|
};
|
||||||
}, [sku, reloadKey]);
|
}, [sku, reloadKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
const resolved = resolveArticleTab(tabParam, data);
|
||||||
|
if (tabParam !== resolved) {
|
||||||
|
navigate(artikelDetailPath(data, resolved), { replace: true });
|
||||||
|
}
|
||||||
|
}, [data, tabParam, navigate]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
@@ -95,5 +107,17 @@ export default function ArtikelSkuPage() {
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
return <ArticleView article={data} onArticleUpdated={setData} />;
|
const tab = resolveArticleTab(tabParam, data);
|
||||||
|
if (tabParam !== tab) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArticleView
|
||||||
|
article={data}
|
||||||
|
tab={tab}
|
||||||
|
onTabChange={(nextTab) => {
|
||||||
|
navigate(artikelDetailPath(data, nextTab));
|
||||||
|
}}
|
||||||
|
onArticleUpdated={setData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user