diff --git a/src/App.jsx b/src/App.jsx
index e3dcd07..48d4038 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -81,7 +81,7 @@ class App extends Component {
} />
} />
} />
- } />
+ } />
}>
} />
diff --git a/src/components/article/ArticleTileTab.jsx b/src/components/article/ArticleTileTab.jsx
index 6c3ba00..c5df224 100644
--- a/src/components/article/ArticleTileTab.jsx
+++ b/src/components/article/ArticleTileTab.jsx
@@ -1,6 +1,7 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
+import IconButton from '@mui/material/IconButton';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
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 LocalShippingIcon from '@mui/icons-material/LocalShipping';
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
+import PushPinIcon from '@mui/icons-material/PushPin';
import SettingsIcon from '@mui/icons-material/Settings';
import { article } from '../../api';
+import {
+ ARTICLE_TAB,
+ DEFAULT_ARTICLE_TAB,
+ articleShowsCompoundTab,
+ readPreferredArticleTab,
+ writePreferredArticleTab,
+} from '../../pages/artikel/articleRoute';
import { notifyError } from '../../utils/notify';
import { PanelTile } from '../panels';
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 = [
- { id: TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
- { id: 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: TAB.attributes, label: 'Attribute und Merkmale', color: '#bfdbfe', Icon: CategoryIcon },
+ { id: ARTICLE_TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
+ { id: ARTICLE_TAB.supplier, label: 'Supplier', color: '#bbf7d0', Icon: LocalShippingIcon },
+ {
+ id: ARTICLE_TAB.seo,
+ 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 = {
- id: TAB.compound,
+ id: ARTICLE_TAB.compound,
label: 'Compound Editor',
color: '#e9d5ff',
Icon: AccountTreeIcon,
};
const SETTINGS_TAB = {
- id: TAB.settings,
+ id: ARTICLE_TAB.settings,
label: 'Einstellungen',
color: '#fca5a5',
Icon: SettingsIcon,
};
-function isCompoundArticle(article) {
- return (
- article?.articleListCount > 0 ||
- article?.optionsCount > 0 ||
- article?.childCount > 0 ||
- article?.fatherArticleId != null
- );
-}
-
function visibleTabs(article) {
const tabs = [...ALWAYS_TABS];
- if (isCompoundArticle(article)) {
+ if (articleShowsCompoundTab(article)) {
tabs.push(COMPOUND_TAB);
}
tabs.push(SETTINGS_TAB);
@@ -86,7 +86,7 @@ function PanelValue({ label, children }) {
function Placeholder({ label }) {
return (
- {label} — noch nicht verfügbar.
+ {label}
);
}
@@ -94,21 +94,24 @@ function Placeholder({ label }) {
class ArticleTileTab extends Component {
constructor(props) {
super(props);
- this.state = { tab: TAB.seo, busy: false };
+ this.state = {
+ busy: false,
+ preferredTab: readPreferredArticleTab(),
+ };
this.handleTabChange = this.handleTabChange.bind(this);
+ this.handlePinDefault = this.handlePinDefault.bind(this);
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
}
- componentDidUpdate(prevProps) {
- if (prevProps.article === this.props.article) return;
- const tabs = visibleTabs(this.props.article);
- if (!tabs.some((row) => row.id === this.state.tab)) {
- this.setState({ tab: TAB.seo });
- }
+ handleTabChange(_event, tab) {
+ this.props.onTabChange?.(tab);
}
- handleTabChange(_event, tab) {
- this.setState({ tab });
+ handlePinDefault() {
+ const { tab } = this.props;
+ if (!tab) return;
+ writePreferredArticleTab(tab);
+ this.setState({ preferredTab: tab });
}
async handleDeleteArticle() {
@@ -159,20 +162,20 @@ class ArticleTileTab extends Component {
}
renderPanel() {
- switch (this.state.tab) {
- case TAB.history:
+ switch (this.props.tab) {
+ case ARTICLE_TAB.history:
return ;
- case TAB.supplier:
+ case ARTICLE_TAB.supplier:
return ;
- case TAB.seo:
+ case ARTICLE_TAB.seo:
return this.renderSeo();
- case TAB.label:
+ case ARTICLE_TAB.label:
return ;
- case TAB.attributes:
+ case ARTICLE_TAB.attributes:
return ;
- case TAB.compound:
+ case ARTICLE_TAB.compound:
return ;
- case TAB.settings:
+ case ARTICLE_TAB.settings:
return this.renderSettings();
default:
return null;
@@ -180,11 +183,24 @@ class ArticleTileTab extends Component {
}
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 { tab } = this.state;
- const active = tabs.find((row) => row.id === tab) ?? ALWAYS_TABS.find((row) => row.id === TAB.seo);
+ const active =
+ tabs.find((row) => row.id === tab) ??
+ ALWAYS_TABS.find((row) => row.id === DEFAULT_ARTICLE_TAB);
const activeColor = active?.color;
+ const effectivePreferred = tabs.some((row) => row.id === preferredTab)
+ ? preferredTab
+ : DEFAULT_ARTICLE_TAB;
+ const isPinnedDefault = tab === effectivePreferred;
return (
-
- {tabs.map((row) => {
- const selected = tab === row.id;
- const { Icon } = row;
- return (
- }
- sx={{
- minHeight: 56,
- minWidth: 64,
- py: 0.5,
- px: 1.25,
- border: 'none',
- borderBottom: 'none',
- borderRadius: '8px 8px 0 0',
- textTransform: 'none',
- bgcolor: row.color,
- opacity: selected ? 1 : 0.72,
- color: 'text.primary',
- zIndex: selected ? 2 : 1,
- boxShadow: 'none',
- '&.Mui-selected': {
- color: 'text.primary',
+
+ {tabs.map((row) => {
+ const selected = tab === row.id;
+ const { Icon } = row;
+ return (
+ }
+ sx={{
+ minHeight: 56,
+ minWidth: 64,
+ py: 0.5,
+ px: 1.25,
+ border: 'none',
borderBottom: 'none',
- },
- }}
- />
- );
- })}
-
+ borderRadius: '8px 8px 0 0',
+ textTransform: 'none',
+ bgcolor: row.color,
+ opacity: selected ? 1 : 0.72,
+ color: 'text.primary',
+ zIndex: selected ? 2 : 1,
+ boxShadow: 'none',
+ '&.Mui-selected': {
+ color: 'text.primary',
+ borderBottom: 'none',
+ },
+ }}
+ />
+ );
+ })}
+
+
+
+
+
@@ -908,7 +916,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
{articleData.fatherSku ? (
{articleData.fatherSku}
diff --git a/src/pages/artikel/articleRoute.js b/src/pages/artikel/articleRoute.js
index d641367..95f85a5 100644
--- a/src/pages/artikel/articleRoute.js
+++ b/src/pages/artikel/articleRoute.js
@@ -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) {
if (!article) return null;
const sku = article.sku?.trim();
return sku || null;
}
-export function artikelDetailPath(article) {
- const sku = articleSku(article);
- if (!sku) return null;
- return `/artikel/${encodeURIComponent(sku)}`;
+export function articleShowsCompoundTab(article) {
+ return (
+ article?.articleListCount > 0 ||
+ 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}`;
}
diff --git a/src/pages/artikel/index.jsx b/src/pages/artikel/index.jsx
index ac4f5bb..eb79250 100644
--- a/src/pages/artikel/index.jsx
+++ b/src/pages/artikel/index.jsx
@@ -29,6 +29,7 @@ import {
parseFilter,
} from './searchFormat';
import { preserveSearchFocus } from './searchFocus';
+import { artikelDetailPath } from './articleRoute';
import withSearchParams from './withSearchParams';
const resultRowSx = {
@@ -430,7 +431,7 @@ class ArtikelSearchPage extends Component {
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
this.persistQuery();
- this.props.navigate(`/artikel/${encodeURIComponent(row.sku)}`);
+ this.props.navigate(artikelDetailPath(row));
}}
sx={resultRowSx}
>
diff --git a/src/pages/artikel/sku.jsx b/src/pages/artikel/sku.jsx
index d68e54f..66241ab 100644
--- a/src/pages/artikel/sku.jsx
+++ b/src/pages/artikel/sku.jsx
@@ -8,9 +8,13 @@ import { ArticleView } from '../../components/article';
import BackendError from '../../components/BackendError';
import { requireObject } from '../../utils/apiShape';
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
+import {
+ artikelDetailPath,
+ resolveArticleTab,
+} from './articleRoute';
export default function ArtikelSkuPage() {
- const { sku: skuParam } = useParams();
+ const { sku: skuParam, tab: tabParam } = useParams();
const navigate = useNavigate();
const sku = skuParam ? decodeURIComponent(skuParam) : '';
const [data, setData] = useState(null);
@@ -69,6 +73,14 @@ export default function ArtikelSkuPage() {
};
}, [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) {
return (
@@ -95,5 +107,17 @@ export default function ArtikelSkuPage() {
if (!data) return null;
- return ;
+ const tab = resolveArticleTab(tabParam, data);
+ if (tabParam !== tab) return null;
+
+ return (
+ {
+ navigate(artikelDetailPath(data, nextTab));
+ }}
+ onArticleUpdated={setData}
+ />
+ );
}