Skip to content

feat: Add navigation bar #2490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Button, Group, Stack, Switch } from "@mantine/core";

import { useSession } from "@homarr/auth/client";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";

Expand All @@ -14,10 +15,13 @@

export const BehaviorSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useForm({
initialValues: {
disableStatus: board.disableStatus,
showInNavigation: board.showInNavigation ?? false,

Check failure on line 24 in apps/nextjs/src/app/[locale]/boards/[name]/settings/_behavior.tsx

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined
},
});

Expand All @@ -37,6 +41,14 @@
{...form.getInputProps("disableStatus", { type: "checkbox" })}
/>

{isAdmin && (
<Switch
label={t("board.field.showInNavigation.label")}
description={t("board.field.showInNavigation.description")}
{...form.getInputProps("showInNavigation", { type: "checkbox" })}
/>
)}

<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
Expand Down
4 changes: 3 additions & 1 deletion apps/nextjs/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppShellHeader, Group, UnstyledButton } from "@mantine/core";
import { Spotlight } from "@homarr/spotlight";

import { ClientBurger } from "./header/burger";
import { NavigationBar } from "./header/navigation-bar";
import { DesktopSearchInput, MobileSearchButton } from "./header/search";
import { UserButton } from "./header/user";
import { HomarrLogoWithTitle } from "./logo/homarr-logo";
Expand All @@ -24,9 +25,10 @@ export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
<UnstyledButton component={Link} href="/">
{logo ?? <HomarrLogoWithTitle size="md" />}
</UnstyledButton>
<NavigationBar />
</Group>
<DesktopSearchInput />
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
<DesktopSearchInput />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal was to still have the search input at the same position when no navigation items are selected

{actions}
<MobileSearchButton />
<UserButton />
Expand Down
46 changes: 46 additions & 0 deletions apps/nextjs/src/components/layout/header/navigation-bar.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need mobile support (Burger Icon or similar)

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { Group, NavLink } from "@mantine/core";

import { clientApi } from "@homarr/api/client";

export const NavigationBar = () => {
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
});
const pathname = usePathname();

// Filter boards that should be shown in navigation
const navigationBoards = boards.filter((board) => board.showInNavigation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a new route that returns only those from the navigation bar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I would suggest, that we fetch this data on the server and pass it down to this component, then it does not need to make a roundtrip. THen we define it as initialData so we still can invalidate the values when adding one


if (navigationBoards.length === 0) {
return null;
}

return (
<Group gap="xs" wrap="nowrap" style={{ overflow: 'auto', maxWidth: '100%' }}>
{navigationBoards.map((board) => {
const boardUrl = `/boards/${board.name}`;
const isActive = pathname === boardUrl;

return (
<NavLink
key={board.id}
component={Link}
href={boardUrl}
label={board.name}
active={isActive}
variant="subtle"
style={{
borderRadius: 5,
padding: '0 10px',
}}
/>
);
})}
</Group>
);
Comment on lines +23 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't handle the overflow of them, so if you add to many links or you shrink your screen it looks not good

};
22 changes: 17 additions & 5 deletions packages/api/src/router/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const boardRouter = createTRPCRouter({
name: true,
logoImageUrl: true,
isPublic: true,
showInNavigation: true,
},
with: {
creator: {
Expand Down Expand Up @@ -708,9 +709,9 @@ export const boardRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");

await ctx.db
.update(boards)
.set({
// Only allow admins to change showInNavigation
const isAdmin = ctx.session.user.permissions.includes("admin");
const settingsToUpdate = {
// general settings
pageTitle: input.pageTitle,
metaTitle: input.metaTitle,
Expand All @@ -735,7 +736,16 @@ export const boardRouter = createTRPCRouter({

// Behavior settings
disableStatus: input.disableStatus,
})
};

// Only include showInNavigation in the update if user is admin
if (isAdmin) {
settingsToUpdate.showInNavigation = input.showInNavigation;
}

await ctx.db
.update(boards)
.set(settingsToUpdate)
.where(eq(boards.id, input.id));
}),
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
Expand Down Expand Up @@ -1110,7 +1120,9 @@ export const boardRouter = createTRPCRouter({
parentSectionId: sectionLayout.parentSectionId,
})
.where(
and(eq(sectionLayouts.sectionId, section.id), eq(sectionLayouts.layoutId, sectionLayout.layoutId)),
and(
eq(sectionLayouts.sectionId, section.id),
eq(sectionLayouts.layoutId, sectionLayout.layoutId)),
)
.run();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `board` ADD COLUMN `show_in_navigation` boolean DEFAULT false NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "5",
"dialect": "mysql",
"id": "0032_add_show_in_navigation",
"name": "add_show_in_navigation",
"statements": [
{
"type": "alter_table",
"name": "board",
"operation": {
"type": "add_column",
"column": {
"name": "show_in_navigation",
"type": "boolean",
"default": {
"type": "literal",
"value": false
},
"not_null": true
}
}
}
]
}
7 changes: 7 additions & 0 deletions packages/db/migrations/mysql/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@
"when": 1740784837957,
"tag": "0031_add_dynamic_section_options",
"breakpoints": true
},
{
"idx": 32,
"version": "5",
"when": 1741000000000,
"tag": "0032_add_show_in_navigation",
"breakpoints": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `board` ADD `show_in_navigation` integer DEFAULT 0 NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0032_add_show_in_navigation",
"name": "add_show_in_navigation",
"statements": [
{
"type": "alter_table",
"name": "board",
"operation": {
"type": "add_column",
"column": {
"name": "show_in_navigation",
"type": "integer",
"default": {
"type": "literal",
"value": 0
},
"not_null": true
}
}
}
]
}
7 changes: 7 additions & 0 deletions packages/db/migrations/sqlite/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@
"when": 1740784849045,
"tag": "0031_add_dynamic_section_options",
"breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1741000000000,
"tag": "0032_add_show_in_navigation",
"breakpoints": true
}
]
}
1 change: 1 addition & 0 deletions packages/db/schema/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const boards = mysqlTable("board", {
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: boolean().default(false).notNull(),
showInNavigation: boolean().default(false).notNull(),
});

export const boardUserPermissions = mysqlTable(
Expand Down
1 change: 1 addition & 0 deletions packages/db/schema/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const boards = sqliteTable("board", {
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
showInNavigation: int({ mode: "boolean" }).default(false).notNull(),
});

export const boardUserPermissions = sqliteTable(
Expand Down
4 changes: 4 additions & 0 deletions packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,10 @@
"label": "Disable app status",
"description": "Disables the status check for all apps on this board"
},
"showInNavigation": {
"label": "Show in navigation bar",
"description": "Display this board in the navigation bar at the top of the page"
},
"columnCount": {
"label": "Column count"
},
Expand Down
1 change: 1 addition & 0 deletions packages/validation/src/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const savePartialSettingsSchema = z
iconColor: hexColorNullableSchema,
itemRadius: z.union([z.literal("xs"), z.literal("sm"), z.literal("md"), z.literal("lg"), z.literal("xl")]),
disableStatus: z.boolean(),
showInNavigation: z.boolean(),
})
.partial();

Expand Down
Loading