1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-06 05:55:23 +02:00

feat: User Tooltip (#4319)

This commit is contained in:
Michael Genson 2024-10-11 19:36:26 -05:00 committed by GitHub
parent a2bdb02a7f
commit e06572b7ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 164 additions and 80 deletions

View file

@ -33,7 +33,7 @@
</template>
<template #item.userId="{ item }">
<v-list-item class="justify-start">
<UserAvatar :user-id="item.userId" size="40" />
<UserAvatar :user-id="item.userId" :tooltip="false" size="40" />
<v-list-item-content>
<v-list-item-title>
{{ getMember(item.userId) }}
@ -153,7 +153,7 @@ export default defineComponent({
async function refreshMembers() {
const { data } = await api.groups.fetchMembers();
if (data) {
members.value = data;
members.value = data.items;
}
}

View file

@ -9,7 +9,7 @@
<v-divider class="mx-2"></v-divider>
<div v-if="user.id" class="d-flex flex-column">
<div class="d-flex mt-3" style="gap: 10px">
<UserAvatar size="40" :user-id="user.id" />
<UserAvatar :tooltip="false" size="40" :user-id="user.id" />
<v-textarea
v-model="comment"
@ -31,7 +31,7 @@
</div>
</div>
<div v-for="comment in recipe.comments" :key="comment.id" class="d-flex my-2" style="gap: 10px">
<UserAvatar size="40" :user-id="comment.userId" />
<UserAvatar :tooltip="false" size="40" :user-id="comment.userId" />
<v-card outlined class="flex-grow-1">
<v-card-text class="pa-3 pb-0">
<p class="">{{ comment.user.username }} {{ $d(Date.parse(comment.createdAt), "medium") }}</p>

View file

@ -1,14 +1,26 @@
<template>
<v-list-item-avatar v-if="list && userId">
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
</v-list-item-avatar>
<v-avatar v-else-if="userId" :size="size">
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
</v-avatar>
<v-tooltip
v-if="userId"
:disabled="!user || !tooltip"
right
>
<template #activator="{ on, attrs }">
<v-list-item-avatar v-if="list" v-bind="attrs" v-on="on">
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
</v-list-item-avatar>
<v-avatar v-else :size="size" v-bind="attrs" v-on="on">
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
</v-avatar>
</template>
<span v-if="user">
{{ user.fullName }}
</span>
</v-tooltip>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, useContext, computed } from "@nuxtjs/composition-api";
import { useUserStore } from "~/composables/store/use-user-store";
import { UserOut } from "~/lib/api/types/user";
export default defineComponent({
@ -25,6 +37,10 @@ export default defineComponent({
type: String,
default: "42",
},
tooltip: {
type: Boolean,
default: true,
}
},
setup(props) {
const state = reactive({
@ -32,15 +48,20 @@ export default defineComponent({
});
const { $auth } = useContext();
const { store: users } = useUserStore();
const user = computed(() => {
return users.value.find((user) => user.id === props.userId);
})
const imageURL = computed(() => {
// TODO Setup correct user type for $auth.user
const user = $auth.user as unknown as UserOut | null;
const key = user?.cacheKey ?? "";
const authUser = $auth.user as unknown as UserOut | null;
const key = authUser?.cacheKey ?? "";
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
});
return {
user,
imageURL,
...toRefs(state),
};

View file

@ -3,7 +3,7 @@
<!-- User Profile -->
<template v-if="loggedIn">
<v-list-item two-line :to="userProfileLink" exact>
<UserAvatar list :user-id="$auth.user.id" />
<UserAvatar list :user-id="$auth.user.id" :tooltip="false" />
<v-list-item-content>
<v-list-item-title class="pr-2"> {{ $auth.user.fullName }}</v-list-item-title>

View file

@ -57,35 +57,30 @@ function getRequests(axiosInstance: NuxtAxiosInstance): ApiRequestInstance {
};
}
export const useAdminApi = function (): AdminAPI {
export const useRequests = function (): ApiRequestInstance {
const { $axios, i18n } = useContext();
$axios.setHeader("Accept-Language", i18n.locale);
const requests = getRequests($axios);
return getRequests($axios);
};
export const useAdminApi = function (): AdminAPI {
const requests = useRequests();
return new AdminAPI(requests);
};
export const useUserApi = function (): UserApi {
const { $axios, i18n } = useContext();
$axios.setHeader("Accept-Language", i18n.locale);
const requests = getRequests($axios);
const requests = useRequests();
return new UserApi(requests);
};
export const usePublicApi = function (): PublicApi {
const { $axios, i18n } = useContext();
$axios.setHeader("Accept-Language", i18n.locale);
const requests = getRequests($axios);
const requests = useRequests();
return new PublicApi(requests);
};
export const usePublicExploreApi = function (groupSlug: string): PublicExploreApi {
const { $axios, i18n } = useContext();
$axios.setHeader("Accept-Language", i18n.locale);
const requests = getRequests($axios);
const requests = useRequests();
return new PublicExploreApi(requests, groupSlug);
}

View file

@ -6,7 +6,7 @@ import { QueryValue } from "~/lib/api/base/route";
interface ReadOnlyStoreActions<T extends BoundT> {
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
refresh(): Promise<void>;
refresh(page?: number, perPage?: number, params?: any): Promise<void>;
}
interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
@ -50,9 +50,9 @@ export function useReadOnlyActions<T extends BoundT>(
return allItems;
}
async function refresh() {
async function refresh(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
loading.value = true;
const { data } = await api.getAll();
const { data } = await api.getAll(page, perPage, params);
if (data && data.items && allRef) {
allRef.value = data.items;
@ -101,9 +101,9 @@ export function useStoreActions<T extends BoundT>(
return allItems;
}
async function refresh() {
async function refresh(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
loading.value = true;
const { data } = await api.getAll();
const { data } = await api.getAll(page, perPage, params);
if (data && data.items && allRef) {
allRef.value = data.items;

View file

@ -2,6 +2,7 @@ import { ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useReadOnlyActions, useStoreActions } from "./use-actions-factory";
import { BoundT } from "./types";
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { QueryValue } from "~/lib/api/base/route";
export const useData = function<T extends BoundT>(defaultObject: T) {
const data = reactive({ ...defaultObject });
@ -16,16 +17,21 @@ export const useReadOnlyStore = function<T extends BoundT>(
store: Ref<T[]>,
loading: Ref<boolean>,
api: BaseCRUDAPIReadOnly<T>,
params = {} as Record<string, QueryValue>,
) {
const storeActions = useReadOnlyActions(api, store, loading);
const actions = {
...useReadOnlyActions(api, store, loading),
...storeActions,
async refresh() {
return await storeActions.refresh(1, -1, params);
},
flushStore() {
store.value = [];
},
};
if (!loading.value && (!store.value || store.value.length === 0)) {
const result = actions.getAll();
const result = actions.getAll(1, -1, params);
store.value = result.value || [];
}
@ -36,16 +42,21 @@ export const useStore = function<T extends BoundT>(
store: Ref<T[]>,
loading: Ref<boolean>,
api: BaseCRUDAPI<unknown, T, unknown>,
params = {} as Record<string, QueryValue>,
) {
const storeActions = useStoreActions(api, store, loading);
const actions = {
...useStoreActions(api, store, loading),
...storeActions,
async refresh() {
return await storeActions.refresh(1, -1, params);
},
flushStore() {
store = ref([]);
},
};
if (!loading.value && (!store.value || store.value.length === 0)) {
const result = actions.getAll();
const result = actions.getAll(1, -1, params);
store.value = result.value || [];
}

View file

@ -0,0 +1,20 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useReadOnlyStore } from "../partials/use-store-factory";
import { useRequests } from "../api/api-client";
import { UserSummary } from "~/lib/api/types/user";
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
const store: Ref<UserSummary[]> = ref([]);
const loading = ref(false);
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
baseRoute = "/api/groups/members";
itemRoute = (idOrUsername: string | number) => `/groups/members/${idOrUsername}`;
}
export const useUserStore = function () {
const requests = useRequests();
const api = new GroupUserAPIReadOnly(requests);
return useReadOnlyStore<UserSummary>(store, loading, api, {orderBy: "full_name"});
}

View file

@ -77,6 +77,7 @@ export interface ReadWebhook {
}
export interface UserSummary {
id: string;
username: string;
fullName: string;
}
export interface ReadGroupPreferences {

View file

@ -1,4 +1,6 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { PaginationData } from "../types/non-generated";
import { QueryValue } from "../base/route";
import { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
import {
GroupAdminUpdate,
@ -14,11 +16,7 @@ const routes = {
groupsSelf: `${prefix}/groups/self`,
preferences: `${prefix}/groups/preferences`,
storage: `${prefix}/groups/storage`,
membersHouseholdId: (householdId: string | number | null) => {
return householdId ?
`${prefix}/households/members?householdId=${householdId}` :
`${prefix}/groups/members`;
},
members: `${prefix}/groups/members`,
groupsId: (id: string | number) => `${prefix}/admin/groups/${id}`,
};
@ -40,8 +38,8 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
return await this.requests.put<ReadGroupPreferences, UpdateGroupPreferences>(routes.preferences, payload);
}
async fetchMembers(householdId: string | number | null = null) {
return await this.requests.get<UserSummary[]>(routes.membersHouseholdId(householdId));
async fetchMembers(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
return await this.requests.get<PaginationData<UserSummary>>(routes.members, { page, perPage, ...params });
}
async storage() {

View file

@ -1,4 +1,6 @@
import { BaseCRUDAPIReadOnly } from "../base/base-clients";
import { PaginationData } from "../types/non-generated";
import { QueryValue } from "../base/route";
import { UserOut } from "~/lib/api/types/user";
import {
HouseholdInDB,
@ -48,8 +50,8 @@ export class HouseholdAPI extends BaseCRUDAPIReadOnly<HouseholdSummary> {
return await this.requests.post<ReadInviteToken>(routes.invitation, payload);
}
async fetchMembers() {
return await this.requests.get<UserOut[]>(routes.members);
async fetchMembers(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
return await this.requests.get<PaginationData<UserOut>>(routes.members, { page, perPage, ...params });
}
async setMemberPermissions(payload: SetPermissions) {

View file

@ -26,7 +26,7 @@
disable-pagination
>
<template #item.avatar="{ item }">
<UserAvatar :user-id="item.id" />
<UserAvatar :tooltip="false" :user-id="item.id" />
</template>
<template #item.admin="{ item }">
{{ item.admin ? $t('user.admin') : $t('user.user') }}
@ -111,7 +111,7 @@ export default defineComponent({
async function refreshMembers() {
const { data } = await api.households.fetchMembers();
if (data) {
members.value = data;
members.value = data.items;
}
}

View file

@ -1025,7 +1025,7 @@ export default defineComponent({
}
// update current user
allUsers.value = data.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
allUsers.value = data.items.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
currentUserId.value = shoppingList.value?.userId;
}

View file

@ -3,7 +3,7 @@
<BasePageTitle divider>
<template #header>
<div class="d-flex flex-column align-center justify-center">
<UserAvatar size="96" :user-id="$auth.user.id" />
<UserAvatar :tooltip="false" size="96" :user-id="$auth.user.id" />
<AppButtonUpload
class="my-1"
file-name="profile"

View file

@ -1,7 +1,7 @@
<template>
<v-container v-if="user">
<section class="d-flex flex-column align-center mt-4">
<UserAvatar size="96" :user-id="user.id" />
<UserAvatar :tooltip="false" size="96" :user-id="user.id" />
<h2 class="headline">{{ $t('profile.welcome-user', [user.fullName]) }}</h2>
<p class="subtitle-1 mb-0 text-center">

View file

@ -34,6 +34,7 @@ import ReportTable from "@/components/global/ReportTable.vue";
import SafeMarkdown from "@/components/global/SafeMarkdown.vue";
import StatsCards from "@/components/global/StatsCards.vue";
import ToggleState from "@/components/global/ToggleState.vue";
import WakelockSwitch from "@/components/global/WakelockSwitch.vue";
import DefaultLayout from "@/components/layout/DefaultLayout.vue";
declare module "vue" {
@ -74,6 +75,7 @@ declare module "vue" {
SafeMarkdown: typeof SafeMarkdown;
StatsCards: typeof StatsCards;
ToggleState: typeof ToggleState;
WakelockSwitch: typeof WakelockSwitch;
// Layout Components
DefaultLayout: typeof DefaultLayout;
}