Skip to content

Commit 13432ac

Browse files
authored
Fix: Use sidebar from top navigation if no other matches (zuplo#838)
1 parent 196d8ad commit 13432ac

File tree

8 files changed

+63
-26
lines changed

8 files changed

+63
-26
lines changed

packages/zudoku/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
"loglevel": "1.9.2",
231231
"lru-cache": "11.0.2",
232232
"lucide-react": "0.475.0",
233+
"minimatch": "10.0.1",
233234
"nanoevents": "^9.1.0",
234235
"next-themes": "0.4.4",
235236
"oauth4webapi": "2.17.0",

packages/zudoku/src/config/validators/common.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ const TopNavigationItemSchema = z.object({
192192
label: z.string(),
193193
id: z.string(),
194194
default: z.string().optional(),
195-
display: z.enum(["auth", "anon", "always"]).default("always").optional(),
195+
display: z
196+
.enum(["auth", "anon", "always", "hide"])
197+
.default("always")
198+
.optional(),
196199
});
197200

198201
type BannerColorType = ZodOptional<

packages/zudoku/src/lib/components/MobileTopNavigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const MobileTopNavigation = () => {
4444
</li>
4545
{topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
4646
<li key={item.label}>
47-
<button onClick={() => setDrawerOpen(false)}>
47+
<button type="button" onClick={() => setDrawerOpen(false)}>
4848
<TopNavItem {...item} />
4949
</button>
5050
</li>

packages/zudoku/src/lib/components/TopNavigation.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { cx } from "class-variance-authority";
22
import { Suspense } from "react";
33
import { NavLink, useNavigation } from "react-router";
4-
import { TopNavigationItem } from "../../config/validators/common.js";
4+
import type { TopNavigationItem } from "../../config/validators/common.js";
55
import { useAuth } from "../authentication/hook.js";
6-
import { ZudokuError } from "../util/invariant.js";
7-
import { joinPath } from "../util/joinPath.js";
6+
import { joinUrl } from "../util/joinUrl.js";
87
import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
98
import { traverseSidebar } from "./navigation/utils.js";
109
import { Slotlet } from "./SlotletProvider.js";
1110

1211
export const isHiddenItem =
1312
(isAuthenticated?: boolean) =>
14-
(item: { display?: "auth" | "anon" | "always" }) => {
13+
(item: { display?: "auth" | "anon" | "always" | "hide" }): boolean => {
14+
if (item.display === "hide") return false;
1515
return (
1616
(item.display === "auth" && isAuthenticated) ||
1717
(item.display === "anon" && !isAuthenticated) ||
@@ -24,17 +24,18 @@ export const TopNavigation = () => {
2424
const { topNavigation } = useZudoku();
2525
const { isAuthenticated } = useAuth();
2626

27-
// Hide top nav if there is only one item
28-
if (topNavigation.length <= 1) {
27+
const filteredItems = topNavigation.filter(isHiddenItem(isAuthenticated));
28+
29+
if (filteredItems.length === 0) {
2930
return <style>{`:root { --top-nav-height: 0px; }`}</style>;
3031
}
3132

3233
return (
3334
<Suspense>
34-
<div className=" items-center justify-between px-8 h-[--top-nav-height] hidden lg:flex text-sm">
35+
<div className="items-center justify-between px-8 h-[--top-nav-height] hidden lg:flex text-sm">
3536
<nav className="text-sm">
3637
<ul className="flex flex-row items-center gap-8">
37-
{topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
38+
{filteredItems.map((item) => (
3839
<li key={item.id}>
3940
<TopNavItem {...item} />
4041
</li>
@@ -66,15 +67,10 @@ export const TopNavItem = ({
6667
defaultLink ??
6768
(currentSidebar
6869
? traverseSidebar(currentSidebar, (item) => {
69-
if (item.type === "doc") return joinPath(item.id);
70+
if (item.type === "doc") return joinUrl(item.id);
7071
})
71-
: joinPath(id));
72-
73-
if (!first) {
74-
throw new ZudokuError("Page not found.", {
75-
developerHint: `No links found in top navigation for '${id}'. Check that the sidebar isn't empty or that a default link is set.`,
76-
});
77-
}
72+
: joinUrl(id)) ??
73+
joinUrl(id);
7874

7975
return (
8076
// We don't use isActive here because it has to be inside the sidebar,

packages/zudoku/src/lib/components/context/ZudokuContext.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createContext, useContext } from "react";
33
import { matchPath, useLocation } from "react-router";
44
import { useAuth } from "../../authentication/hook.js";
55
import type { ZudokuContext } from "../../core/ZudokuContext.js";
6-
import { joinPath } from "../../util/joinPath.js";
6+
import { joinUrl } from "../../util/joinUrl.js";
77
import { CACHE_KEYS, NO_DEHYDRATE } from "../cache.js";
88
import { traverseSidebar } from "../navigation/utils.js";
99

@@ -39,13 +39,13 @@ export const useCurrentNavigation = () => {
3939
matchPath(route, location.pathname),
4040
);
4141

42-
const currentSidebarItem = Object.entries(sidebars).find(([, sidebar]) => {
42+
let currentSidebarItem = Object.entries(sidebars).find(([, sidebar]) => {
4343
return traverseSidebar(sidebar, (item) => {
4444
const itemId =
4545
item.type === "doc"
46-
? joinPath(item.id)
46+
? joinUrl(item.id)
4747
: item.type === "category" && item.link
48-
? joinPath(item.link.id)
48+
? joinUrl(item.link.id)
4949
: undefined;
5050

5151
if (itemId === location.pathname) {
@@ -57,6 +57,14 @@ export const useCurrentNavigation = () => {
5757
topNavigation.find((t) => t.id === currentSidebarItem?.[0]) ??
5858
topNavigation.find((item) => matchPath(item.id, location.pathname));
5959

60+
if (
61+
currentTopNavItem &&
62+
!currentSidebarItem &&
63+
currentTopNavItem.id in sidebars
64+
) {
65+
currentSidebarItem = ["", sidebars[currentTopNavItem.id]!];
66+
}
67+
6068
const { data } = useSuspenseQuery({
6169
queryFn: () => getPluginSidebar(location.pathname),
6270
// We just want to suspend here and don't store in SSR dehydrated state

packages/zudoku/src/lib/util/joinPath.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @deprecated Use `joinUrl` instead.
3+
*/
14
export const joinPath = (
25
...parts: Array<string | null | undefined | boolean>
36
) => {

packages/zudoku/src/vite/plugin-docs.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,47 @@
11
import { glob } from "glob";
2+
import { minimatch } from "minimatch";
23
import path from "node:path";
3-
import { type Plugin } from "vite";
4+
import type { Plugin, ViteDevServer } from "vite";
45
import type { LoadedConfig } from "../config/config.js";
56
import { DocResolver } from "../lib/plugins/markdown/resolver.js";
7+
import { joinUrl } from "../lib/util/joinUrl.js";
68
import { writePluginDebugCode } from "./debug.js";
9+
import { reload } from "./plugin-config-reload.js";
710

8-
const ensureLeadingSlash = (str: string) =>
9-
str.startsWith("/") ? str : `/${str}`;
11+
const ensureLeadingSlash = joinUrl;
1012

1113
const viteDocsPlugin = (getConfig: () => LoadedConfig): Plugin => {
1214
const virtualModuleId = "virtual:zudoku-docs-plugins";
1315
const resolvedVirtualModuleId = "\0" + virtualModuleId;
1416

17+
let server: ViteDevServer;
18+
1519
return {
1620
name: "zudoku-docs-plugin",
1721
resolveId(id) {
1822
if (id === virtualModuleId) {
1923
return resolvedVirtualModuleId;
2024
}
2125
},
26+
configureServer(srv) {
27+
server = srv;
28+
},
29+
watchChange(id, change) {
30+
if (change.event !== "delete" && change.event !== "create") return;
31+
32+
const config = getConfig();
33+
const resolver = new DocResolver(config);
34+
const docsConfigs = resolver.getDocsConfigs();
35+
36+
const matches = docsConfigs.some((docConfig) =>
37+
minimatch(
38+
ensureLeadingSlash(path.relative(config.__meta.rootDir, id)),
39+
docConfig.files,
40+
),
41+
);
42+
43+
if (matches) reload(server);
44+
},
2245
async load(id) {
2346
if (id === resolvedVirtualModuleId) {
2447
const config = getConfig();

pnpm-lock.yaml

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)