mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 08:14:30 -04:00
feat: Support User-Level Default Activities (#5125)
This commit is contained in:
63
frontend/composables/use-default-activity.ts
Normal file
63
frontend/composables/use-default-activity.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { Activity, I18n, TranslationResult } from "~/lib/api/types/activity";
|
||||||
|
import { ActivityKey } from "~/lib/api/types/activity";
|
||||||
|
|
||||||
|
export const DEFAULT_ACTIVITY = "/g/home" as const;
|
||||||
|
|
||||||
|
type ActivityRegistry = {
|
||||||
|
recipes: Activity;
|
||||||
|
mealplanner: Activity;
|
||||||
|
shopping_list: Activity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectableActivities: ActivityRegistry = {
|
||||||
|
recipes: {
|
||||||
|
key: ActivityKey.RECIPES,
|
||||||
|
route: groupSlug => groupSlug ? `/g/${groupSlug}` : DEFAULT_ACTIVITY,
|
||||||
|
label: i18n => i18n.t("general.recipes"),
|
||||||
|
},
|
||||||
|
mealplanner: {
|
||||||
|
key: ActivityKey.MEALPLANNER,
|
||||||
|
route: () => "/household/mealplan/planner/view",
|
||||||
|
label: i18n => i18n.t("meal-plan.meal-planner"),
|
||||||
|
},
|
||||||
|
shopping_list: {
|
||||||
|
key: ActivityKey.SHOPPING_LIST,
|
||||||
|
route: () => "/shopping-lists",
|
||||||
|
label: i18n => i18n.t("shopping-list.shopping-lists"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultActivityRoute(activityKey?: ActivityKey, groupSlug?: string): string {
|
||||||
|
if (!activityKey) {
|
||||||
|
return DEFAULT_ACTIVITY;
|
||||||
|
}
|
||||||
|
const route = selectableActivities[activityKey]?.route ?? (() => DEFAULT_ACTIVITY);
|
||||||
|
return route(groupSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultActivityLabels(i18n: I18n): TranslationResult[] {
|
||||||
|
return Object.values(selectableActivities).map(
|
||||||
|
({ label }) => label(i18n),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivityKey(i18n: I18n, target: TranslationResult = ""): ActivityKey | undefined {
|
||||||
|
return Object.values(selectableActivities)
|
||||||
|
.find(({ label }) => label(i18n) === target)?.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivityLabel(i18n: I18n, target?: ActivityKey): TranslationResult {
|
||||||
|
return Object.values(selectableActivities)
|
||||||
|
.find(({ key }) => key === target)
|
||||||
|
?.label(i18n) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useDefaultActivity() {
|
||||||
|
return {
|
||||||
|
selectableActivities,
|
||||||
|
getDefaultActivityRoute,
|
||||||
|
getDefaultActivityLabels,
|
||||||
|
getActivityKey,
|
||||||
|
getActivityLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
||||||
|
import { ActivityKey } from "~/lib/api/types/activity";
|
||||||
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
|
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
|
||||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||||
|
|
||||||
@@ -65,6 +66,10 @@ export interface UserRecipeCreatePreferences {
|
|||||||
parseRecipe: boolean;
|
parseRecipe: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserActivityPreferences {
|
||||||
|
defaultActivity: ActivityKey;
|
||||||
|
}
|
||||||
|
|
||||||
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
||||||
const fromStorage = useLocalStorage(
|
const fromStorage = useLocalStorage(
|
||||||
"meal-planner-preferences",
|
"meal-planner-preferences",
|
||||||
@@ -115,6 +120,20 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
|
|||||||
return fromStorage;
|
return fromStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUserActivityPreferences(): Ref<UserActivityPreferences> {
|
||||||
|
const fromStorage = useLocalStorage(
|
||||||
|
"activity-preferences",
|
||||||
|
{
|
||||||
|
defaultActivity: ActivityKey.RECIPES,
|
||||||
|
},
|
||||||
|
{ mergeDefaults: true },
|
||||||
|
// we cast to a Ref because by default it will return an optional type ref
|
||||||
|
// but since we pass defaults we know all properties are set.
|
||||||
|
) as Ref<UserActivityPreferences>;
|
||||||
|
|
||||||
|
return fromStorage;
|
||||||
|
}
|
||||||
|
|
||||||
export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
|
export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
|
||||||
const fromStorage = useSessionStorage(
|
const fromStorage = useSessionStorage(
|
||||||
"search-query",
|
"search-query",
|
||||||
|
|||||||
@@ -1063,7 +1063,9 @@
|
|||||||
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Don't want to see this anymore? Be sure to change your email in your user settings!",
|
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Don't want to see this anymore? Be sure to change your email in your user settings!",
|
||||||
"forgot-password": "Forgot Password",
|
"forgot-password": "Forgot Password",
|
||||||
"forgot-password-text": "Please enter your email address and we will send you a link to reset your password.",
|
"forgot-password-text": "Please enter your email address and we will send you a link to reset your password.",
|
||||||
"changes-reflected-immediately": "Changes to this user will be reflected immediately."
|
"changes-reflected-immediately": "Changes to this user will be reflected immediately.",
|
||||||
|
"default-activity": "Default Activity",
|
||||||
|
"default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device"
|
||||||
},
|
},
|
||||||
"language-dialog": {
|
"language-dialog": {
|
||||||
"translated": "translated",
|
"translated": "translated",
|
||||||
|
|||||||
18
frontend/lib/api/types/activity.ts
Normal file
18
frontend/lib/api/types/activity.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export type I18n = ReturnType<typeof useI18n>;
|
||||||
|
export type TRoute = string;
|
||||||
|
export type TranslationResult = string;
|
||||||
|
|
||||||
|
export type ActivityRoute = (groupSlug?: string) => TRoute;
|
||||||
|
export type ActivityLabel = (i18n: I18n) => TranslationResult;
|
||||||
|
|
||||||
|
export type Activity = {
|
||||||
|
key: ActivityKey;
|
||||||
|
route: ActivityRoute;
|
||||||
|
label: ActivityLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enum ActivityKey {
|
||||||
|
RECIPES = "recipes",
|
||||||
|
MEALPLANNER = "mealplanner",
|
||||||
|
SHOPPING_LIST = "shopping_list",
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import useDefaultActivity from "~/composables/use-default-activity";
|
||||||
|
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
import type { AppInfo, AppStartupInfo } from "~/lib/api/types/admin";
|
import type { AppInfo, AppStartupInfo } from "~/lib/api/types/admin";
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ export default defineNuxtComponent({
|
|||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { $axios } = useNuxtApp();
|
const { $axios } = useNuxtApp();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const activityPreferences = useUserActivityPreferences();
|
||||||
|
const { getDefaultActivityRoute } = useDefaultActivity();
|
||||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||||
|
|
||||||
async function redirectPublicUserToDefaultGroup() {
|
async function redirectPublicUserToDefaultGroup() {
|
||||||
@@ -32,9 +36,16 @@ export default defineNuxtComponent({
|
|||||||
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
|
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
|
||||||
const isDemo = data.data.isDemo;
|
const isDemo = data.data.isDemo;
|
||||||
const isFirstLogin = data.data.isFirstLogin;
|
const isFirstLogin = data.data.isFirstLogin;
|
||||||
|
const defaultActivityRoute = getDefaultActivityRoute(
|
||||||
|
activityPreferences.value.defaultActivity,
|
||||||
|
groupSlug.value,
|
||||||
|
);
|
||||||
if (!isDemo && isFirstLogin && $auth.user.value?.admin) {
|
if (!isDemo && isFirstLogin && $auth.user.value?.admin) {
|
||||||
router.push("/admin/setup");
|
router.push("/admin/setup");
|
||||||
}
|
}
|
||||||
|
else if (defaultActivityRoute) {
|
||||||
|
router.push(defaultActivityRoute);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
router.push(`/g/${groupSlug.value}`);
|
router.push(`/g/${groupSlug.value}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ import { usePasswordField } from "~/composables/use-passwords";
|
|||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
import type { AppStartupInfo } from "~/lib/api/types/admin";
|
import type { AppStartupInfo } from "~/lib/api/types/admin";
|
||||||
|
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -233,6 +234,8 @@ export default defineNuxtComponent({
|
|||||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||||
const isDemo = ref(false);
|
const isDemo = ref(false);
|
||||||
const isFirstLogin = ref(false);
|
const isFirstLogin = ref(false);
|
||||||
|
const activityPreferences = useUserActivityPreferences();
|
||||||
|
const { getDefaultActivityRoute } = useDefaultActivity();
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("user.login"),
|
title: i18n.t("user.login"),
|
||||||
@@ -253,9 +256,16 @@ export default defineNuxtComponent({
|
|||||||
whenever(
|
whenever(
|
||||||
() => loggedIn.value && groupSlug.value,
|
() => loggedIn.value && groupSlug.value,
|
||||||
() => {
|
() => {
|
||||||
|
const defaultActivityRoute = getDefaultActivityRoute(
|
||||||
|
activityPreferences.value.defaultActivity,
|
||||||
|
groupSlug.value,
|
||||||
|
);
|
||||||
if (!isDemo.value && isFirstLogin.value && $auth.user.value?.admin) {
|
if (!isDemo.value && isFirstLogin.value && $auth.user.value?.admin) {
|
||||||
router.push("/admin/setup");
|
router.push("/admin/setup");
|
||||||
}
|
}
|
||||||
|
else if (defaultActivityRoute) {
|
||||||
|
router.push(defaultActivityRoute);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
router.push(`/g/${groupSlug.value || ""}`);
|
router.push(`/g/${groupSlug.value || ""}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,13 +171,26 @@
|
|||||||
class="mt-10"
|
class="mt-10"
|
||||||
:title="$t('profile.preferences')"
|
:title="$t('profile.preferences')"
|
||||||
/>
|
/>
|
||||||
<v-checkbox
|
<v-card variant="outlined" style="border-color: lightgrey;">
|
||||||
v-model="userCopy.advanced"
|
<v-card-text>
|
||||||
class="mt-n4"
|
<v-combobox
|
||||||
:label="$t('profile.show-advanced-description')"
|
v-model="selectedDefaultActivity"
|
||||||
color="primary"
|
:label="$t('user.default-activity')"
|
||||||
@change="updateUser"
|
:items="activityOptions"
|
||||||
/>
|
:hint="$t('user.default-activity-hint')"
|
||||||
|
density="comfortable"
|
||||||
|
variant="underlined"
|
||||||
|
validate-on="blur"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="userCopy.advanced"
|
||||||
|
:label="$t('profile.show-advanced-description')"
|
||||||
|
color="primary"
|
||||||
|
@change="updateUser"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
class="mt-5 d-flex flex-column justify-center text-center"
|
class="mt-5 d-flex flex-column justify-center text-center"
|
||||||
:to="`/group`"
|
:to="`/group`"
|
||||||
@@ -207,6 +220,9 @@ import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
|||||||
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import type { VForm } from "~/types/auto-forms";
|
import type { VForm } from "~/types/auto-forms";
|
||||||
|
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
|
||||||
|
import useDefaultActivity from "~/composables/use-default-activity";
|
||||||
|
import { ActivityKey } from "~/lib/api/types/activity";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -216,12 +232,20 @@ export default defineNuxtComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
|
const { getDefaultActivityLabels, getActivityLabel, getActivityKey } = useDefaultActivity();
|
||||||
const user = computed(() => $auth.user.value);
|
const user = computed(() => $auth.user.value);
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("settings.profile"),
|
title: i18n.t("settings.profile"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activityPreferences = useUserActivityPreferences();
|
||||||
|
const activityOptions = getDefaultActivityLabels(i18n);
|
||||||
|
const selectedDefaultActivity = ref(getActivityLabel(i18n, activityPreferences.value.defaultActivity));
|
||||||
|
watch(selectedDefaultActivity, () => {
|
||||||
|
activityPreferences.value.defaultActivity = getActivityKey(i18n, selectedDefaultActivity.value) ?? ActivityKey.RECIPES;
|
||||||
|
});
|
||||||
|
|
||||||
watch(user, () => {
|
watch(user, () => {
|
||||||
userCopy.value = { ...user.value };
|
userCopy.value = { ...user.value };
|
||||||
});
|
});
|
||||||
@@ -273,6 +297,8 @@ export default defineNuxtComponent({
|
|||||||
updateUser,
|
updateUser,
|
||||||
updatePassword,
|
updatePassword,
|
||||||
userCopy,
|
userCopy,
|
||||||
|
selectedDefaultActivity,
|
||||||
|
activityOptions,
|
||||||
password,
|
password,
|
||||||
domUpdatePassword,
|
domUpdatePassword,
|
||||||
passwordsMatch,
|
passwordsMatch,
|
||||||
|
|||||||
Reference in New Issue
Block a user