mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 08:14:30 -04:00
feat: consolidate settings gui (#6043)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
<template #prepend>
|
<v-list density="comfortable" color="primary">
|
||||||
<v-icon>{{ nav.icon }}</v-icon>
|
<v-list-item :prepend-icon="$globals.icons.translate" :title="$t('sidebar.language')" @click="languageDialog=true" />
|
||||||
</template>
|
<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" />
|
||||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
<v-divider v-if="loggedIn" class="my-2" />
|
||||||
</v-list-item>
|
<v-list-item v-if="loggedIn" :prepend-icon="$globals.icons.cog" :title="$t('profile.user-settings')" to="/user/profile" />
|
||||||
</div>
|
<v-list-item v-if="canManage" :prepend-icon="$globals.icons.manageData" :title="$t('data-pages.data-management')" to="/group/data" />
|
||||||
</template>
|
<v-divider v-if="isAdmin" class="my-2" />
|
||||||
<slot name="bottom" />
|
<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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user