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:
seb
2026-08-01 06:45:25 +02:00
parent 80f093649a
commit 780f946eff
6 changed files with 302 additions and 118 deletions

View File

@@ -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}`;
}

View File

@@ -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}
>

View File

@@ -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 (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
@@ -95,5 +107,17 @@ export default function ArtikelSkuPage() {
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}
/>
);
}