feat: consolidate settings gui (#6043)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
Kuchenpirat
2025-09-03 17:07:06 +02:00
committed by GitHub
parent 461e51bd22
commit 581aa929bd
5 changed files with 37 additions and 74 deletions

View File

@@ -16,7 +16,6 @@
absolute absolute
:top-link="topLinks" :top-link="topLinks"
:secondary-links="cookbookLinks || []" :secondary-links="cookbookLinks || []"
:bottom-links="bottomLinks"
> >
<v-menu <v-menu
offset-y offset-y
@@ -84,25 +83,6 @@
</template> </template>
</v-list> </v-list>
</v-menu> </v-menu>
<template #bottom>
<v-list-item @click.stop="languageDialog = true">
<template #prepend>
<v-icon>{{ $globals.icons.translate }}</v-icon>
</template>
<v-list-item-title>{{ $t("sidebar.language") }}</v-list-item-title>
<LanguageDialog v-model="languageDialog" />
</v-list-item>
<v-list-item @click="toggleDark">
<template #prepend>
<v-icon>
{{ $vuetify.theme.current.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight }}
</v-icon>
</template>
<v-list-item-title>
{{ $vuetify.theme.current.dark ? $t("settings.theme.light-mode") : $t("settings.theme.dark-mode") }}
</v-list-item-title>
</v-list-item>
</template>
</AppSidebar> </AppSidebar>
<v-main class="pt-12"> <v-main class="pt-12">
<v-scroll-x-transition> <v-scroll-x-transition>
@@ -121,7 +101,6 @@ import { useAppInfo } from "~/composables/api";
import { useCookbookPreferences } from "~/composables/use-users/preferences"; import { useCookbookPreferences } from "~/composables/use-users/preferences";
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store"; import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store"; import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
import { useToggleDarkMode } from "~/composables/use-utils";
import type { ReadCookBook } from "~/lib/api/types/cookbook"; import type { ReadCookBook } from "~/lib/api/types/cookbook";
import type { HouseholdSummary } from "~/lib/api/types/household"; import type { HouseholdSummary } from "~/lib/api/types/household";
@@ -133,7 +112,6 @@ export default defineNuxtComponent({
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
const isAdmin = computed(() => $auth.user.value?.admin);
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || ""); const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
@@ -191,8 +169,6 @@ export default defineNuxtComponent({
const appInfo = useAppInfo(); const appInfo = useAppInfo();
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices); const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
const toggleDark = useToggleDarkMode();
const languageDialog = ref<boolean>(false); const languageDialog = ref<boolean>(false);
const sidebar = ref<boolean>(false); const sidebar = ref<boolean>(false);
@@ -286,19 +262,6 @@ export default defineNuxtComponent({
}, },
]); ]);
const bottomLinks = computed<SideBarLink[]>(() =>
isAdmin.value
? [
{
icon: $globals.icons.cog,
title: i18n.t("general.settings"),
to: "/admin/site-settings",
restricted: true,
},
]
: [],
);
const topLinks = computed<SideBarLink[]>(() => [ const topLinks = computed<SideBarLink[]>(() => [
{ {
icon: $globals.icons.silverwareForkKnife, icon: $globals.icons.silverwareForkKnife,
@@ -367,11 +330,9 @@ export default defineNuxtComponent({
groupSlug, groupSlug,
cookbookLinks, cookbookLinks,
createLinks, createLinks,
bottomLinks,
topLinks, topLinks,
isOwnGroup, isOwnGroup,
languageDialog, languageDialog,
toggleDark,
sidebar, sidebar,
}; };
}, },

View File

@@ -1,5 +1,6 @@
<template> <template>
<v-navigation-drawer v-model="showDrawer" class="d-flex flex-column d-print-none position-fixed" touchless> <v-navigation-drawer v-model="showDrawer" class="d-flex flex-column d-print-none position-fixed" touchless>
<LanguageDialog v-model="languageDialog" />
<!-- User Profile --> <!-- User Profile -->
<template v-if="loggedIn"> <template v-if="loggedIn">
<v-list-item lines="two" :to="userProfileLink" exact> <v-list-item lines="two" :to="userProfileLink" exact>
@@ -82,20 +83,22 @@
</template> </template>
<!-- Bottom Navigation Links --> <!-- Bottom Navigation Links -->
<template v-if="bottomLinks" #append> <template #append>
<v-list v-model:selected="bottomSelected" nav density="compact"> <v-list v-model:selected="bottomSelected" nav density="comfortable">
<template v-for="nav in bottomLinks"> <v-menu location="end bottom" :offset="15">
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title"> <template #activator="{ props }">
<v-list-item :key="nav.key || nav.title" exact link :to="nav.to" :href="nav.href" <v-list-item v-bind="props" :prepend-icon="$globals.icons.cog" :title="$t('general.settings')" />
:target="nav.href ? '_blank' : null">
<template #prepend>
<v-icon>{{ nav.icon }}</v-icon>
</template> </template>
<v-list-item-title>{{ nav.title }}</v-list-item-title> <v-list density="comfortable" color="primary">
</v-list-item> <v-list-item :prepend-icon="$globals.icons.translate" :title="$t('sidebar.language')" @click="languageDialog=true" />
</div> <v-list-item :prepend-icon="$vuetify.theme.current.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight" :title="$vuetify.theme.current.dark ? $t('settings.theme.light-mode') : $t('settings.theme.dark-mode')" @click="toggleDark" />
</template> <v-divider v-if="loggedIn" class="my-2" />
<slot name="bottom" /> <v-list-item v-if="loggedIn" :prepend-icon="$globals.icons.cog" :title="$t('profile.user-settings')" to="/user/profile" />
<v-list-item v-if="canManage" :prepend-icon="$globals.icons.manageData" :title="$t('data-pages.data-management')" to="/group/data" />
<v-divider v-if="isAdmin" class="my-2" />
<v-list-item v-if="isAdmin" :prepend-icon="$globals.icons.wrench" :title="$t('settings.admin-settings')" to="/admin/site-settings" />
</v-list>
</v-menu>
</v-list> </v-list>
</template> </template>
</v-navigation-drawer> </v-navigation-drawer>
@@ -106,6 +109,7 @@ import { useWindowSize } from "@vueuse/core";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import type { SidebarLinks } from "~/types/application-types"; import type { SidebarLinks } from "~/types/application-types";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
import { useToggleDarkMode } from "~/composables/use-utils";
export default defineNuxtComponent({ export default defineNuxtComponent({
components: { components: {
@@ -130,26 +134,26 @@ export default defineNuxtComponent({
required: false, required: false,
default: null, default: null,
}, },
bottomLinks: {
type: Array as () => SidebarLinks,
required: false,
default: () => ([]),
},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(props, context) { setup(props, context) {
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { loggedIn, isOwnGroup } = useLoggedInState(); const { loggedIn, isOwnGroup } = useLoggedInState();
const isAdmin = computed(() => $auth.user.value?.admin);
const canManage = computed(() => $auth.user.value?.canManage);
const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined); const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined);
const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined); const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined);
const toggleDark = useToggleDarkMode();
const state = reactive({ const state = reactive({
dropDowns: {} as Record<string, boolean>, dropDowns: {} as Record<string, boolean>,
topSelected: null as string[] | null, topSelected: null as string[] | null,
secondarySelected: null as string[] | null, secondarySelected: null as string[] | null,
bottomSelected: null as string[] | null, bottomSelected: null as string[] | null,
hasOpenedBefore: false as boolean, hasOpenedBefore: false as boolean,
languageDialog: false as boolean,
}); });
// model to control the drawer // model to control the drawer
const showDrawer = computed({ const showDrawer = computed({
@@ -171,7 +175,7 @@ export default defineNuxtComponent({
} }
}); });
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || []), ...(props.bottomLinks || [])]); const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || [])]);
function initDropdowns() { function initDropdowns() {
allLinks.value.forEach((link) => { allLinks.value.forEach((link) => {
state.dropDowns[link.title] = link.childrenStartExpanded || false; state.dropDowns[link.title] = link.childrenStartExpanded || false;
@@ -193,8 +197,11 @@ export default defineNuxtComponent({
userProfileLink, userProfileLink,
showDrawer, showDrawer,
loggedIn, loggedIn,
isAdmin,
canManage,
isOwnGroup, isOwnGroup,
sessionUser: $auth.user, sessionUser: $auth.user,
toggleDark,
}; };
}, },
}); });

View File

@@ -15,7 +15,6 @@
v-model="sidebar" v-model="sidebar"
absolute absolute
:top-link="topLinks" :top-link="topLinks"
:bottom-links="bottomLinks"
:user="{ data: true }" :user="{ data: true }"
:secondary-header="$t('sidebar.developer')" :secondary-header="$t('sidebar.developer')"
:secondary-links="developerLinks" :secondary-links="developerLinks"
@@ -114,13 +113,4 @@ const developerLinks: SidebarLinks = [
], ],
}, },
]; ];
const bottomLinks: SidebarLinks = [
{
icon: $globals.icons.heart,
title: i18n.t("about.support"),
href: "https://github.com/sponsors/hay-kot",
restricted: true,
},
];
</script> </script>

View File

@@ -152,6 +152,7 @@ import {
mdiCookie, mdiCookie,
mdiBellPlus, mdiBellPlus,
mdiLinkVariantPlus, mdiLinkVariantPlus,
mdiTableEdit,
} from "@mdi/js"; } from "@mdi/js";
export const icons = { export const icons = {
@@ -240,6 +241,7 @@ export const icons = {
linkVariantPlus: mdiLinkVariantPlus, linkVariantPlus: mdiLinkVariantPlus,
lock: mdiLock, lock: mdiLock,
logout: mdiLogout, logout: mdiLogout,
manageData: mdiTableEdit,
menu: mdiMenu, menu: mdiMenu,
messageText: mdiMessageText, messageText: mdiMessageText,
newBox: mdiNewBox, newBox: mdiNewBox,
@@ -324,5 +326,4 @@ export const icons = {
preserveLines: mdiText, preserveLines: mdiText,
preserveBlocks: mdiTextBoxOutline, preserveBlocks: mdiTextBoxOutline,
flatten: mdiMinus, flatten: mdiMinus,
}; };

View File

@@ -25,7 +25,8 @@ test('ldap login', async ({ page }) => {
await page.getByRole('button', { name: 'Login', exact: true }).click(); await page.getByRole('button', { name: 'Login', exact: true }).click();
await expect(page).toHaveURL(/\/g\/home/); await expect(page).toHaveURL(/\/g\/home/);
await expect(page.getByRole('navigation')).toContainText(name); await expect(page.getByRole('navigation')).toContainText(name);
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeVisible(); await page.getByText('Settings', { exact: true }).click();
await expect(page.getByRole('link', { name: 'Admin Settings' })).not.toBeVisible();
}); });
test('ldap admin login', async ({ page }) => { test('ldap admin login', async ({ page }) => {
@@ -40,7 +41,8 @@ test('ldap admin login', async ({ page }) => {
// skip admin setup page // skip admin setup page
await page.getByRole('link', { name: "I'm already set up, just bring me to the homepage" }).click(); await page.getByRole('link', { name: "I'm already set up, just bring me to the homepage" }).click();
await expect(page.getByRole('navigation')).toContainText(name); await expect(page.getByRole('navigation')).toContainText(name);
await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible(); await page.getByText('Settings', { exact: true }).click();
await expect(page.getByRole('link', { name: 'Admin Settings' })).toBeVisible();
}); });
test('oidc initial login', async ({ page }) => { test('oidc initial login', async ({ page }) => {
@@ -61,7 +63,8 @@ test('oidc initial login', async ({ page }) => {
await page.getByRole('button', { name: 'Sign-in' }).click(); await page.getByRole('button', { name: 'Sign-in' }).click();
await expect(page).toHaveURL(/\/g\/home/); await expect(page).toHaveURL(/\/g\/home/);
await expect(page.getByRole('navigation')).toContainText(name); await expect(page.getByRole('navigation')).toContainText(name);
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeVisible(); await page.getByText('Settings', { exact: true }).click();
await expect(page.getByRole('link', { name: 'Admin Settings' })).not.toBeVisible();
}); });
test('oidc login with user not in propery group', async ({ page }) => { test('oidc login with user not in propery group', async ({ page }) => {
@@ -167,5 +170,6 @@ test('oidc admin user', async ({ page }) => {
await expect(page).toHaveURL(/\/admin\/setup/, { timeout: 15000 }); await expect(page).toHaveURL(/\/admin\/setup/, { timeout: 15000 });
await page.getByRole('link', { name: "I'm already set up, just bring me to the homepage" }).click(); await page.getByRole('link', { name: "I'm already set up, just bring me to the homepage" }).click();
await expect(page.getByRole('navigation')).toContainText(name); await expect(page.getByRole('navigation')).toContainText(name);
await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible(); await page.getByText('Settings', { exact: true }).click();
await expect(page.getByRole('link', { name: 'Admin Settings' })).toBeVisible();
}); });