Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dad41f9
fix(knowledge-network): quieter version status and older-to-newer com…
sh00tg0a1 Nov 11, 2026
c81ee36
feat(knowledge-network): pick the version from the top bar trail
sh00tg0a1 Nov 11, 2026
ce6c7a8
feat(knowledge-network): version new networks by default
sh00tg0a1 Nov 11, 2026
960326e
fix(knowledge-network): show the version picker in the workspace's ow…
sh00tg0a1 Nov 11, 2026
b6916b5
fix(knowledge-network): put the version picker beside the network's t…
sh00tg0a1 Nov 11, 2026
c9559b0
fix(knowledge-network): fold the version actions into a borderless pi…
sh00tg0a1 Nov 11, 2026
b0c1919
feat(knowledge-network): show version differences definition by defin…
sh00tg0a1 Nov 11, 2026
5cf532c
fix(knowledge-network): make publishing a draft a primary button
sh00tg0a1 Nov 11, 2026
4b4702a
fix(knowledge-network): draw the version views with the console's col…
sh00tg0a1 Nov 11, 2026
f3c91d3
feat(knowledge-network): gather discarding and rolling back in a dang…
sh00tg0a1 Nov 11, 2026
3023f6c
fix(knowledge-network): publish from the right of the network's title
sh00tg0a1 Nov 11, 2026
33bf43d
fix(knowledge-network): draw the version danger zone like the console…
sh00tg0a1 Nov 11, 2026
53217ad
fix(knowledge-network): lead the title's actions with publishing
sh00tg0a1 Nov 11, 2026
c4c1a15
chore(knowledge-network): leave the create dialog to the always-on ch…
sh00tg0a1 Nov 11, 2026
0e687c8
feat(knowledge-network): drop the way to turn versioning on
sh00tg0a1 Nov 11, 2026
abbbbac
feat(knowledge-network): show a network without versions as its v1
sh00tg0a1 Nov 11, 2026
2bf2bf4
fix(knowledge-network): name only v1 in the closed picker of a networ…
sh00tg0a1 Nov 11, 2026
a7e249f
feat(knowledge-network): publish a network without versions as v1
sh00tg0a1 Nov 11, 2026
022a50c
fix(knowledge-network): say discarding empties the draft before a fir…
sh00tg0a1 Nov 11, 2026
cab9bbb
Merge origin/main into fix/kn-version-bar-status
sh00tg0a1 Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions src/app/router/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useEffect, useState } from "react";
import { SideNav } from "@/app/shell/SideNav";
import { TopBar } from "@/app/shell/TopBar";
import { WorkspaceLayout } from "@/app/shell/WorkspaceLayout";
import { ShellTrailSlotProvider } from "@/app/shell/workspace-slots";
import { LicenseStateBanner } from "@/framework/entitlement/LicenseStateBanner";

const SIDENAV_COLLAPSED_STORAGE_KEY = "bkn-studio:sidenav-collapsed";
Expand All @@ -29,22 +30,24 @@ export function AppShell() {
}, [sidenavCollapsed]);

return (
<div className="console-shell">
<TopBar />
<LicenseStateBanner />
<div className={sidenavCollapsed ? "console-body is-sidenav-collapsed" : "console-body"}>
<SideNav
collapsed={sidenavCollapsed}
onToggleCollapsed={() => {
setSidenavCollapsed((current) => !current);
}}
/>
<main className="console-main">
<WorkspaceLayout>
<Outlet />
</WorkspaceLayout>
</main>
<ShellTrailSlotProvider>
<div className="console-shell">
<TopBar />
<LicenseStateBanner />
<div className={sidenavCollapsed ? "console-body is-sidenav-collapsed" : "console-body"}>
<SideNav
collapsed={sidenavCollapsed}
onToggleCollapsed={() => {
setSidenavCollapsed((current) => !current);
}}
/>
<main className="console-main">
<WorkspaceLayout>
<Outlet />
</WorkspaceLayout>
</main>
</div>
</div>
</div>
</ShellTrailSlotProvider>
);
}
5 changes: 4 additions & 1 deletion src/app/shell/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import {
} from "@ant-design/icons";
import { Badge, Modal } from "antd";
import type { MenuProps } from "antd";
import { useEffect, useMemo, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useMatches, useNavigate, useParams } from "react-router-dom";

import { getConsoleNavTrail } from "@/app/shell/console-navigation";
import { UserPill } from "@/app/shell/UserPill";
import { ShellTrailSlotContext } from "@/app/shell/workspace-slots-context";
import { useResolvedTheme, useToggleTheme } from "@/app/theme/theme-context";
import openBknLogo from "@/assets/brand/openbkn-logo-compact.webp";
import type { AppRouteHandle } from "@/app/shell/route-meta";
Expand All @@ -49,6 +50,7 @@ import {

export function TopBar() {
const { t } = useTranslation();
const trailSlot = useContext(ShellTrailSlotContext);
const location = useLocation();
const matches = useMatches();
const navigate = useNavigate();
Expand Down Expand Up @@ -367,6 +369,7 @@ export function TopBar() {
))}
</div>
) : null}
<span className="console-brand-trail-slot" ref={trailSlot?.setTrailHost} />
</div>
</div>

Expand Down
8 changes: 8 additions & 0 deletions src/app/shell/workspace-slots-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ export type WorkspaceSlotsValue = {
};

export const WorkspaceSlotsContext = createContext<WorkspaceSlotsValue | null>(null);

/** Where a page may add a small control right after the top bar's trail, such as a version picker. */
export type ShellTrailSlotValue = {
setTrailHost: (host: HTMLSpanElement | null) => void;
trailHost: HTMLSpanElement | null;
};

export const ShellTrailSlotContext = createContext<ShellTrailSlotValue | null>(null);
28 changes: 26 additions & 2 deletions src/app/shell/workspace-slots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
*/

import type { PropsWithChildren, ReactNode } from "react";
import { useContext } from "react";
import { useContext, useMemo, useState } from "react";

import { createPortal } from "react-dom";

import { WorkspaceSlotsContext } from "@/app/shell/workspace-slots-context";
import { ShellTrailSlotContext, WorkspaceSlotsContext } from "@/app/shell/workspace-slots-context";

export function WorkspaceToolbarPortal({
children,
Expand All @@ -24,3 +24,27 @@ export function WorkspaceToolbarPortal({

return createPortal(children, context.toolbarHost);
}

/** Renders after the top bar's trail; without a top bar it renders `fallback` in place. */
export function ShellTrailPortal({
children,
fallback = null,
}: PropsWithChildren<{ fallback?: ReactNode }>) {
const context = useContext(ShellTrailSlotContext);

if (!context?.trailHost) {
return <>{fallback}</>;
}

return createPortal(children, context.trailHost);
}

/**
* Lets a page add a control after the top bar's trail. Wrap both the TopBar and the page content
* in it; pages that render their own TopBar outside the console shell need it too.
*/
export function ShellTrailSlotProvider({ children }: PropsWithChildren) {
const [trailHost, setTrailHost] = useState<HTMLSpanElement | null>(null);
const value = useMemo(() => ({ setTrailHost, trailHost }), [trailHost]);
return <ShellTrailSlotContext.Provider value={value}>{children}</ShellTrailSlotContext.Provider>;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ describe("KnDiffModal", () => {
expect(diffVersions).toHaveBeenLastCalledWith("kn-1", 2, "draft");
});

it("reads a pair picked newer-first from the older side", async () => {
diffVersions.mockResolvedValue({ changes: [] });
render(<KnDiffModal {...props} initialBase="running" initialTarget={2} open />);
await waitFor(() => expect(diffVersions).toHaveBeenCalledTimes(1));
expect(diffVersions).toHaveBeenLastCalledWith("kn-1", 2, "running");
});

it("puts the draft after any version", async () => {
diffVersions.mockResolvedValue({ changes: [] });
render(<KnDiffModal {...props} initialBase="draft" initialTarget={2} open />);
await waitFor(() => expect(diffVersions).toHaveBeenCalledTimes(1));
expect(diffVersions).toHaveBeenLastCalledWith("kn-1", 2, "draft");
});

it("keeps the latest comparison when an earlier one answers last", async () => {
let answerFirst: (value: KnDiffResult) => void = () => undefined;
const first = { changes: [{ id: "old" }] } as unknown as KnDiffResult;
Expand Down
42 changes: 35 additions & 7 deletions src/modules/knowledge-network/components/versions/KnDiffModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
useReadableKnVersions,
versionOptionLabel,
} from "@/modules/knowledge-network/components/versions/useReadableKnVersions";
import { parseKnVersionView } from "@/modules/knowledge-network/lib/kn-version-format";
import {
knVersionName,
parseKnVersionView,
} from "@/modules/knowledge-network/lib/kn-version-format";
import { diffKnVersions } from "@/modules/knowledge-network/services/kn-version.service";
import type {
KnDiffResult,
Expand All @@ -24,7 +27,7 @@ import type {
} from "@/modules/knowledge-network/types/kn-version";

type KnDiffModalProps = {
/** What to show on the left when the dialog opens. */
/** The pair to compare when the dialog opens; the older one ends up on the left. */
initialBase?: KnVersionView;
initialTarget?: KnVersionView;
networkId: string;
Expand Down Expand Up @@ -73,7 +76,25 @@ export function KnDiffModal({
const asValue = (view: KnVersionView) =>
view === "running" ? String(versioning.running_version) : String(view);

const compare = (from: KnVersionView = base, to: KnVersionView = target) => {
const viewName = (view: KnVersionView) =>
view === "draft"
? t("knowledgeNetwork.versions.draft")
: knVersionName(view === "running" ? versioning.running_version : view);

// Older first: the draft is newer than every version, and numbers grow with time.
const age = (view: KnVersionView) =>
view === "draft"
? Number.POSITIVE_INFINITY
: view === "running"
? versioning.running_version
: view;

// A comparison always reads from the older side to the newer one, so "added" means
// what the newer side has; a pair picked the other way round is turned around.
const compare = (picked: KnVersionView = base, other: KnVersionView = target) => {
const [from, to] = age(picked) > age(other) ? [other, picked] : [picked, other];
setBase(from);
setTarget(to);
const request = ++latest.current;
setLoading(true);
setError(null);
Expand Down Expand Up @@ -102,10 +123,11 @@ export function KnDiffModal({
if (!open) {
return;
}
setBase(initialBase);
setTarget(initialTarget);
if (versioning.running_version > 0) {
compare(initialBase, initialTarget);
} else {
setBase(initialBase);
setTarget(initialTarget);
}
// The selectors run it again on demand.
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -117,7 +139,7 @@ export function KnDiffModal({
onCancel={onClose}
open={open}
title={t("knowledgeNetwork.versions.diff.title")}
width={720}
width={1040}
>
<Space style={{ marginBottom: 12 }} wrap>
<Select
Expand Down Expand Up @@ -146,7 +168,13 @@ export function KnDiffModal({
type="info"
/>
) : (
<KnDiffSummary error={error} loading={loading} result={result} />
<KnDiffSummary
baseLabel={viewName(base)}
error={error}
loading={loading}
result={result}
targetLabel={viewName(target)}
/>
)}
</Modal>
);
Expand Down
Loading
Loading