Skip to content
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

feat: User Tooltip #4319

Merged
merged 14 commits into from
Oct 12, 2024
1 change: 1 addition & 0 deletions docs/docs/contributors/developers-guide/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ These endpoints have moved, but are otherwise unchanged:
- `/explore/recipes/{group_slug}` -> `/explore/groups/{group_slug}/recipes`

`/groups/members` previously returned a `UserOut` object, but now returns a `UserSummary`. Should you need the full user information (username, email, etc.), rather than just the summary, see `/households/members` instead for the household members.
`/groups/members` previously returned a list of users, but now returns paginated users (similar to all other list endpoints).

These endpoints have been completely removed:

Expand Down
4 changes: 2 additions & 2 deletions frontend/components/Domain/Recipe/RecipeDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}
Expand Down Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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>
Expand Down
37 changes: 29 additions & 8 deletions frontend/components/Domain/User/UserAvatar.vue
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -25,22 +37,31 @@ export default defineComponent({
type: String,
default: "42",
},
tooltip: {
type: Boolean,
default: true,
}
},
setup(props) {
const state = reactive({
error: false,
});

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),
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Layout/LayoutParts/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
23 changes: 9 additions & 14 deletions frontend/composables/api/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 5 additions & 5 deletions frontend/composables/partials/use-actions-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 15 additions & 4 deletions frontend/composables/partials/use-store-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 || [];
}

Expand All @@ -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 || [];
}

Expand Down
20 changes: 20 additions & 0 deletions frontend/composables/store/use-user-store.ts
Original file line number Diff line number Diff line change
@@ -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"});
}
1 change: 1 addition & 0 deletions frontend/lib/api/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface ReadWebhook {
}
export interface UserSummary {
id: string;
username: string;
fullName: string;
}
export interface ReadGroupPreferences {
Expand Down
12 changes: 5 additions & 7 deletions frontend/lib/api/user/groups.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}`,
};

Expand All @@ -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() {
Expand Down
6 changes: 4 additions & 2 deletions frontend/lib/api/user/households.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/pages/household/members.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down Expand Up @@ -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;
}
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/shopping-lists/_id.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/user/profile/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/user/profile/index.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
2 changes: 2 additions & 0 deletions frontend/types/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -74,6 +75,7 @@ declare module "vue" {
SafeMarkdown: typeof SafeMarkdown;
StatsCards: typeof StatsCards;
ToggleState: typeof ToggleState;
WakelockSwitch: typeof WakelockSwitch;
// Layout Components
DefaultLayout: typeof DefaultLayout;
}
Expand Down
Loading
Loading