Skip to content
Merged
68 changes: 55 additions & 13 deletions frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,48 @@
import { safeTranslate } from '$lib/utils/i18n';
import { pageTitle } from '$lib/utils/stores';

async function trimBreadcrumbsToCurrentPath(
function hrefPathname(href: string | undefined): string | undefined {
if (!href) return undefined;
const queryIdx = href.indexOf('?');
return queryIdx === -1 ? href : href.slice(0, queryIdx);
}

// Detect sibling navigation: same depth and same first segment (model).
// Used to replace (not append) the last crumb when navigating to a
// sibling resource — e.g. save-and-next between requirement assessments.
function isSiblingPath(a: string | undefined, b: string): boolean {
if (!a) return false;
const aSegs = a.split('/').filter(Boolean);
const bSegs = b.split('/').filter(Boolean);
if (aSegs.length === 0 || aSegs.length !== bSegs.length) return false;
return aSegs[0] === bSegs[0];
}

function syncBreadcrumbsToCurrentUrl(
breadcrumbs: Breadcrumb[],
currentPath: string
): Promise<Breadcrumb[]> {
const idx = breadcrumbs.findIndex((c) => c.href?.startsWith(currentPath));
// First breadcrumb is home, its href is always '/'
if (idx > 0 && idx < breadcrumbs.length - 1) {
breadcrumbs = breadcrumbs.slice(0, idx + 1);
currentPath: string,
currentUrl: string,
fallbackLabel: string
): Breadcrumb[] {
// Skip home (index 0, href '/').
const idx = breadcrumbs.findIndex((c, i) => i > 0 && hrefPathname(c.href) === currentPath);
if (idx > 0) {
// Refresh the matched crumb's href with the current query so filters
// survive a round-trip.
const trimmed = breadcrumbs.slice(0, idx + 1);
const matched = trimmed[idx];
trimmed[idx] = { ...matched, href: currentUrl };
return trimmed;
}
// Sibling nav (e.g. save-and-next): replace last crumb instead of
// appending so the trail doesn't grow indefinitely.
const last = breadcrumbs[breadcrumbs.length - 1];
if (breadcrumbs.length > 1 && isSiblingPath(hrefPathname(last?.href), currentPath)) {
const replaced = breadcrumbs.slice();
replaced[replaced.length - 1] = { label: fallbackLabel, href: currentUrl };
return replaced;
}
return breadcrumbs;
return [...breadcrumbs, { label: fallbackLabel, href: currentUrl }];
}

function getPageTitle(): string {
Expand All @@ -38,13 +70,24 @@
return URL_MODEL_MAP[lastPathSegment]?.localNamePlural;
}

afterNavigate(async () => {
$breadcrumbs = await trimBreadcrumbsToCurrentPath($breadcrumbs, page.url.pathname);
});
function sync(fallbackLabel: string) {
const currentPath = page.url.pathname;
const currentUrl = currentPath + page.url.search;
const current = $breadcrumbs;
const next = syncBreadcrumbsToCurrentUrl(current, currentPath, currentUrl, fallbackLabel);
// Skip writes that don't change anything to avoid effect loops.
if (
next.length !== current.length ||
next.some((c, i) => c.href !== current[i].href || c.label !== current[i].label)
) {
$breadcrumbs = next;
}
}

afterNavigate(() => sync(getPageTitle()));

$effect(() => {
$pageTitle = getPageTitle();
if ($breadcrumbs.length < 2) breadcrumbs.push([{ label: $pageTitle, href: page.url.pathname }]);
});
Comment thread
Mohamed-Hacene marked this conversation as resolved.
</script>

Expand All @@ -69,7 +112,6 @@
data-testid="crumb-item"
href={c.href}
title={safeTranslate(c.label)}
onclick={() => breadcrumbs.slice(i)}
>
{#if c.icon}
<i class={c.icon}></i>
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/lib/components/ModelTable/ModelTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -351,14 +351,20 @@
if (finalFilterValue) {
finalFilterValue.forEach(({ value }) => page.url.searchParams.append(field, value));
}

const hrefPattern = new RegExp(`^/${URLModel}(\\?.*)?$`);
const fullPath = page.url.pathname + page.url.search;
if (hrefPattern.test(fullPath)) {
breadcrumbs.updateCrumb(hrefPattern, { href: fullPath });
}
}
history.replaceState(history.state, '', page.url.pathname + page.url.search);
// Sync the current crumb's href with the new filter query.
breadcrumbs.update((crumbs) => {
if (crumbs.length < 2) return crumbs;
const last = crumbs[crumbs.length - 1];
const lastPath = last.href?.split('?')[0];
if (lastPath !== page.url.pathname) return crumbs;
const newHref = page.url.pathname + page.url.search;
if (last.href === newHref) return crumbs;
const next = crumbs.slice();
next[next.length - 1] = { ...last, href: newHref };
return next;
});
// untracked so resetFilters can delete the entry without retriggering us
if (isStandaloneTable) {
untrack(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { superValidate } from 'sveltekit-superforms';
import { zod4 as zod } from 'sveltekit-superforms/adapters';
import type { PageServerLoad, Actions } from '../$types';
import { defaultWriteFormAction } from '$lib/utils/actions';
import { m } from '$paraglide/messages';

export const load: PageServerLoad = async (event) => {
const URLModel = 'ebios-rm';
Expand Down Expand Up @@ -38,7 +39,7 @@ export const load: PageServerLoad = async (event) => {
}
}
model.selectOptions = selectOptions;
return { form, model, object, selectOptions, URLModel };
return { form, model, object, selectOptions, URLModel, title: m.edit() };
};

export const actions: Actions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ export const load: LayoutServerLoad = async (event) => {
}
model.selectOptions = selectOptions;

return { form, model, object, selectOptions, URLModel };
return { form, model, object, selectOptions, URLModel, title: m.edit() };
};
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
{#if canEditRequirementAssessment}
<Anchor
breadcrumbAction="push"
label={title || m.requirementAssessment()}
href="/requirement-assessments/{ra_id}/edit?next={page.url.pathname}"
>
{#if title || description}
Expand All @@ -221,6 +222,7 @@
{:else}
<Anchor
breadcrumbAction="push"
label={title || m.requirementAssessment()}
href="/requirement-assessments/{ra_id}?next={page.url.pathname}"
>
{#if title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import { complianceResultColorMap } from '$lib/utils/constants';
import { hideSuggestions } from '$lib/utils/stores';
import { breadcrumbs } from '$lib/utils/breadcrumbs';
import { m } from '$paraglide/messages';
import { countMasked } from '$lib/utils/related-visibility';
import CommentsPanel from '$lib/components/CommentsPanel/CommentsPanel.svelte';
Expand Down Expand Up @@ -371,13 +370,6 @@
if (createAppliedControlsLoading === true && form) createAppliedControlsLoading = false;
});

$effect(() => {
breadcrumbs.updateCrumb(/^\/requirement-assessments\/[^/]+\/edit/, {
label: data.requirementAssessment.name,
href: page.url.pathname + page.url.search
});
});

let computedScoreAndResult = $derived(
computeRequirementScoreAndResult(data.requirementAssessment, $formStore.answers)
);
Expand Down
Loading