fix(frontend): cleanup error handling even more

This commit is contained in:
MiniDigger | Martin 2022-12-28 20:33:04 +01:00
parent 077df236fd
commit 88e0a41469
26 changed files with 156 additions and 146 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@ -20,7 +20,7 @@ const props = defineProps<{
const i18n = useI18n();
const route = useRoute();
const flags = await (props.resolved ? useResolvedFlags() : useUnresolvedFlags()).catch((e) => handleRequestError(e));
const flags = await (props.resolved ? useResolvedFlags() : useUnresolvedFlags());
const loading = ref<{ [key: number]: boolean }>({});
function resolve(flag: Flag) {
@ -45,6 +45,7 @@ function resolve(flag: Flag) {
// TODO: bake into hangarflag?
const notifications = ref<HangarFlagNotification[]>([]);
const currentId = ref(-1);
async function getNotifications(flag: Flag) {
if (currentId.value === flag.id) {
return;
@ -83,7 +84,10 @@ async function getNotifications(flag: Flag) {
<template v-if="resolved">
<Button v-if="currentId !== item.id" @click="getNotifications(item)">Load notifications</Button>
<Button :disabled="loading[item.id]" @click="resolve(item)"><IconMdiCheck class="mr-1" /> {{ i18n.t("flagReview.markUnresolved") }}</Button>
<Button :disabled="loading[item.id]" @click="resolve(item)">
<IconMdiCheck class="mr-1" />
{{ i18n.t("flagReview.markUnresolved") }}
</Button>
</template>
<template v-else>
<div class="flex flex-col space-y-1">
@ -91,7 +95,10 @@ async function getNotifications(flag: Flag) {
<ReportNotificationModal :flag="item" :send-to-reporter="true" />
</div>
<VisibilityChangerModal :prop-visibility="item.projectVisibility" type="project" :post-url="`projects/visibility/${item.projectId}`" />
<Button :disabled="loading[item.id]" @click="resolve(item)"><IconMdiCheck class="mr-1" /> {{ i18n.t("flagReview.markResolved") }}</Button>
<Button :disabled="loading[item.id]" @click="resolve(item)">
<IconMdiCheck class="mr-1" />
{{ i18n.t("flagReview.markResolved") }}
</Button>
</template>
</div>

View File

@ -122,25 +122,23 @@ function updateNotifications() {
unreadNotifications.value = v.value;
}
});
useRecentNotifications(30)
.then((v) => {
if (v && v.value) {
// Only show notifications that are recent or unread (from the last 30 notifications)
let filteredAmount = 0;
notifications.value = v.value.filter((notification: HangarNotification) => {
if (filteredAmount < 8 && (!notification.read || isRecent(notification.createdAt))) {
if (!notification.read) {
loadedUnreadNotifications.value++;
}
filteredAmount++;
return true;
useRecentNotifications(30).then((v) => {
if (v && v.value) {
// Only show notifications that are recent or unread (from the last 30 notifications)
let filteredAmount = 0;
notifications.value = v.value.filter((notification: HangarNotification) => {
if (filteredAmount < 8 && (!notification.read || isRecent(notification.createdAt))) {
if (!notification.read) {
loadedUnreadNotifications.value++;
}
return false;
});
}
})
.catch((e) => handleRequestError(e));
filteredAmount++;
return true;
}
return false;
});
}
});
}
function isRecent(date: string): boolean {

View File

@ -16,160 +16,179 @@ import {
RoleTable,
} from "hangar-internal";
import { AsyncData } from "nuxt/app";
import { ref, Ref } from "vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import { useAsyncData } from "#imports";
import { handleRequestError } from "~/composables/useErrorHandling";
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }) {
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }): Promise<Ref<PaginatedResult<Project> | null>> {
return extract(await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", "get", params)));
}
export async function useUser(user: string) {
export async function useUser(user: string): Promise<Ref<User | null>> {
return extract(await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user)));
}
export async function useOrganization(user: string) {
export async function useOrganization(user: string): Promise<Ref<Organization | null>> {
return extract(await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`)));
}
export async function useProject(user: string, project: string) {
export async function useProject(user: string, project: string): Promise<Ref<HangarProject | null>> {
return extract(await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project)));
}
export async function useStargazers(user: string, project: string) {
export async function useStargazers(user: string, project: string): Promise<Ref<PaginatedResult<User> | null>> {
return extract(await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`)));
}
export async function useWatchers(user: string, project: string) {
export async function useWatchers(user: string, project: string): Promise<Ref<PaginatedResult<User> | null>> {
return extract(await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`)));
}
export async function useStaff(params?: { offset?: number; limit?: number; sort?: string[] }) {
export async function useStaff(params?: { offset?: number; limit?: number; sort?: string[] }): Promise<Ref<PaginatedResult<User> | null>> {
return extract(await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", "GET", params)));
}
export async function useAuthors(params?: { offset?: number; limit?: number; sort?: string[] }) {
export async function useAuthors(params?: { offset?: number; limit?: number; sort?: string[] }): Promise<Ref<PaginatedResult<User> | null>> {
return extract(await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", "GET", params)));
}
export async function useUsers() {
export async function useUsers(): Promise<Ref<PaginatedResult<User> | null>> {
return extract(await useAsyncData("useUsers", () => useApi<PaginatedResult<User>>("users")));
}
export async function useInvites() {
export async function useInvites(): Promise<Ref<Invites | null>> {
return extract(await useAsyncData("useInvites", () => useInternalApi<Invites>("invites")));
}
export async function useNotifications() {
export async function useNotifications(): Promise<Ref<PaginatedResult<HangarNotification> | null>> {
return extract(await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications")));
}
export async function useUnreadNotifications() {
export async function useUnreadNotifications(): Promise<Ref<PaginatedResult<HangarNotification> | null>> {
return extract(await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications")));
}
export async function useReadNotifications() {
export async function useReadNotifications(): Promise<Ref<PaginatedResult<HangarNotification> | null>> {
return extract(await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications")));
}
export async function useRecentNotifications(amount: number) {
export async function useRecentNotifications(amount: number): Promise<Ref<HangarNotification[] | null>> {
return extract(await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount)));
}
export async function useUnreadNotificationsCount() {
export async function useUnreadNotificationsCount(): Promise<Ref<number | null>> {
return extract(await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount")));
}
export async function useResolvedFlags() {
export async function useResolvedFlags(): Promise<Ref<PaginatedResult<Flag> | null>> {
return extract(await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved")));
}
export async function useUnresolvedFlags() {
export async function useUnresolvedFlags(): Promise<Ref<PaginatedResult<Flag> | null>> {
return extract(await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved")));
}
export async function useProjectFlags(projectId: number) {
export async function useProjectFlags(projectId: number): Promise<Ref<Flag[] | null>> {
return extract(await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId)));
}
export async function useProjectNotes(projectId: number) {
export async function useProjectNotes(projectId: number): Promise<Ref<Note[] | null>> {
return extract(await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId)));
}
export async function useProjectChannels(user: string, project: string) {
export async function useProjectChannels(user: string, project: string): Promise<Ref<ProjectChannel[] | null>> {
return extract(await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`)));
}
export async function useProjectVersions(user: string, project: string) {
export async function useProjectVersions(user: string, project: string): Promise<Ref<PaginatedResult<Version> | null>> {
return extract(
await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`))
);
}
export async function useProjectVersionsInternal(user: string, project: string, version: string) {
return await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
export async function useProjectVersionsInternal(user: string, project: string, version: string): Promise<Ref<HangarVersion | null>> {
return extract(
await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
)
);
}
export async function usePage(user: string, project: string, path?: string) {
return await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
export async function usePage(user: string, project: string, path?: string): Promise<Ref<ProjectPage | null>> {
return extract(
await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
)
);
}
export async function useHealthReport() {
export async function useHealthReport(): Promise<Ref<HealthReport | null>> {
return extract(await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health")));
}
export async function useActionLogs() {
export async function useActionLogs(): Promise<Ref<PaginatedResult<LoggedAction> | null>> {
return extract(await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/")));
}
export async function useVersionApprovals() {
return await useAsyncData("useVersionApprovals", () =>
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
export async function useVersionApprovals(): Promise<Ref<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] } | null>> {
return extract(
await useAsyncData("useVersionApprovals", () =>
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
)
);
}
export async function usePossibleOwners() {
export async function usePossibleOwners(): Promise<Ref<ProjectOwner[] | null>> {
return extract(await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners")));
}
export async function useOrgVisibility(user: string) {
export async function useOrgVisibility(user: string): Promise<Ref<{ [key: string]: boolean } | null>> {
return extract(
await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`))
);
}
export async function useVersionInfo() {
export async function useVersionInfo(): Promise<Ref<VersionInfo | null>> {
return extract(await useAsyncData("useVersionInfo", () => useInternalApi<VersionInfo>(`data/version-info`)));
}
export async function useUserData(user: string) {
return await useAsyncData("useUserData:" + user, async () => {
// noinspection ES6MissingAwait
const data = await Promise.all([
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
useApi<PaginatedResult<Project>>(`projects`, "get", {
owner: user,
}),
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
useApi<ProjectCompact[]>(`users/${user}/pinned`),
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
watching: data[1] as PaginatedResult<ProjectCompact>,
projects: data[2] as PaginatedResult<Project>,
organizations: data[3] as { [key: string]: RoleTable },
pinned: data[4] as ProjectCompact[],
};
});
export async function useUserData(user: string): Promise<
Ref<{
starred: PaginatedResult<ProjectCompact>;
watching: PaginatedResult<ProjectCompact>;
projects: PaginatedResult<Project>;
organizations: { [key: string]: RoleTable };
pinned: ProjectCompact[];
} | null>
> {
return extract(
await useAsyncData("useUserData:" + user, async () => {
// noinspection ES6MissingAwait
const data = await Promise.all([
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
useApi<PaginatedResult<Project>>(`projects`, "get", {
owner: user,
}),
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
useApi<ProjectCompact[]>(`users/${user}/pinned`),
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
watching: data[1] as PaginatedResult<ProjectCompact>,
projects: data[2] as PaginatedResult<Project>,
organizations: data[3] as { [key: string]: RoleTable },
pinned: data[4] as ProjectCompact[],
};
})
);
}
function extract<T, E>(asyncData: AsyncData<T, E>) {
function extract<T, E>(asyncData: AsyncData<T, E>): Ref<T | null> {
if (asyncData.error?.value) {
throw asyncData.error.value;
handleRequestError(asyncData.error.value);
return ref(null);
} else {
return asyncData.data;
}

View File

@ -6,14 +6,13 @@ import { useNotificationStore } from "~/lib/store/notification";
import { I18n } from "~/lib/i18n";
import { createError } from "#imports";
export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value, msg: string | undefined = undefined) {
export function handleRequestError(err: AxiosError | unknown, i18n: Composer = I18n.value, msg: string | undefined = undefined) {
if (import.meta.env.SSR) {
_handleRequestError(err, i18n);
return ref();
}
const notfication = useNotificationStore();
const transformed = transformAxiosError(err);
if (!err.isAxiosError) {
if (!axios.isAxiosError(err)) {
// everything should be an AxiosError
console.log("no axios request error", transformed);
notfication.error(transformed.message?.toString() || "Unknown error");
@ -38,10 +37,9 @@ export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value,
console.log("unknown error", transformed);
notfication.error(transformed.message?.toString() || "Unknown error");
}
return ref();
}
function _handleRequestError(err: AxiosError, i18n: Composer) {
function _handleRequestError(err: AxiosError | unknown, i18n: Composer) {
function writeResponse(object: unknown) {
console.log("writeResponse", object);
// throw new Error("TODO: Implement me"); // TODO
@ -49,7 +47,7 @@ function _handleRequestError(err: AxiosError, i18n: Composer) {
}
const transformed = transformAxiosError(err);
if (!err.isAxiosError) {
if (!axios.isAxiosError(err)) {
// everything should be an AxiosError
writeResponse({
status: 500,

View File

@ -8,7 +8,7 @@ import { usePage } from "~/composables/useApiHelper";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
export async function useProjectPage(route: RouteLocationNormalizedLoaded, router: Router, i18n: ReturnType<typeof useI18n>, project: HangarProject) {
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string).catch((e) => handleRequestError(e));
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string);
if (!page) {
throw useErrorRedirect(route, 404, "Not found");
}

View File

@ -2,17 +2,16 @@
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useOrganization, useUser } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
const i18n = useI18n();
const route = useRoute();
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e));
const user = await useUser(route.params.user as string);
let organization = null;
if (!user || !user.value) {
throw useErrorRedirect(useRoute(), 404, "Not found");
} else if (user.value?.isOrganization) {
organization = await useOrganization(route.params.user as string).catch((e) => handleRequestError(e));
organization = await useOrganization(route.params.user as string);
}
</script>

View File

@ -5,7 +5,6 @@ import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { HangarProjectPage } from "hangar-internal";
import { useProject } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import ProjectHeader from "~/components/projects/ProjectHeader.vue";
import ProjectNav from "~/components/projects/ProjectNav.vue";
@ -19,7 +18,7 @@ defineProps({
const i18n = useI18n();
const route = useRoute();
const project = await useProject(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e));
const project = await useProject(route.params.user as string, route.params.project as string);
if (!project || !project.value) {
throw useErrorRedirect(route, 404, "Not found");
}

View File

@ -30,7 +30,7 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const route = useRoute();
const channels = await useProjectChannels(props.project.namespace.owner, props.project.namespace.slug).catch((e) => handleRequestError(e));
const channels = await useProjectChannels(props.project.namespace.owner, props.project.namespace.slug);
const validations = useBackendData.validations;
const notifications = useNotificationStore();
@ -42,7 +42,7 @@ async function refreshChannels() {
const newChannels = await useInternalApi<ProjectChannel[]>(`channels/${props.project.namespace.owner}/${props.project.namespace.slug}`).catch((e) =>
handleRequestError(e)
);
if (channels && newChannels) {
if (channels?.value && newChannels) {
channels.value = newChannels;
}
}

View File

@ -9,7 +9,6 @@ import Link from "~/lib/components/design/Link.vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useProjectFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { definePageMeta } from "#imports";
@ -24,7 +23,7 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const route = useRoute();
const flags = await useProjectFlags(props.project.id).catch((e) => handleRequestError(e));
const flags = await useProjectFlags(props.project.id);
const headers = [
{ title: "Submitter", name: "user" },

View File

@ -28,7 +28,7 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const route = useRoute();
const notes = await useProjectNotes(props.project.id).catch((e) => handleRequestError(e));
const notes = await useProjectNotes(props.project.id);
const text = ref("");
const loading = ref(false);
@ -50,7 +50,7 @@ async function addNote() {
}).catch((e) => handleRequestError(e));
text.value = "";
const newNotes = await useInternalApi<Note[]>("projects/notes/" + props.project.id).catch((e) => handleRequestError(e));
if (notes && newNotes) {
if (notes?.value && newNotes) {
notes.value = newNotes;
}
loading.value = false;

View File

@ -3,7 +3,6 @@ import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import UserAvatar from "~/components/UserAvatar.vue";
@ -15,7 +14,7 @@ import { useSeo } from "~/composables/useSeo";
const route = useRoute();
const i18n = useI18n();
const stargazers = await useStargazers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e));
const stargazers = await useStargazers(route.params.user as string, route.params.project as string);
const props = defineProps<{
project: HangarProject;
@ -37,8 +36,8 @@ useHead(
<PageTitle>{{ i18n.t("project.stargazers") }}</PageTitle>
</template>
<div v-if="stargazers.result && stargazers.result.length > 0" class="flex flex-wrap gap-4">
<div v-for="stargazer in stargazers.result" :key="stargazer.name">
<div v-if="stargazers?.result?.length > 0" class="flex flex-wrap gap-4">
<div v-for="stargazer in stargazers?.result" :key="stargazer.name">
<UserAvatar size="xs" :username="stargazer.name" :avatar-url="avatarUrl(stargazer.name)" />
<Link :to="'/' + stargazer.name">{{ stargazer.name }}</Link>
</div>

View File

@ -2,7 +2,6 @@
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useProjectVersionsInternal } from "~/composables/useApiHelper";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import { Platform } from "~/types/enums";
@ -14,10 +13,8 @@ const props = defineProps<{
project: HangarProject;
}>();
const version = await useProjectVersionsInternal(route.params.user as string, route.params.project as string, route.params.version as string).catch((e) =>
handleRequestError(e)
);
if (!version || !version.value) {
const version = await useProjectVersionsInternal(route.params.user as string, route.params.project as string, route.params.version as string);
if (!version?.value) {
throw useErrorRedirect(route, 404, "Not found");
}

View File

@ -48,10 +48,10 @@ const requestOptions = computed(() => {
};
});
const channels = await useProjectChannels(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e));
const versions = await useProjectVersions(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e));
const channels = await useProjectChannels(route.params.user as string, route.params.project as string);
const versions = await useProjectVersions(route.params.user as string, route.params.project as string);
if (channels) {
if (channels.value) {
filter.channels.push(...(channels.value?.map((c) => c.name) || []));
filter.platforms.push(...platforms.value.map((p) => p.enumName));
}

View File

@ -3,7 +3,6 @@ import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import UserAvatar from "~/components/UserAvatar.vue";
@ -15,7 +14,7 @@ import { useSeo } from "~/composables/useSeo";
const route = useRoute();
const i18n = useI18n();
const watchers = await useWatchers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e));
const watchers = await useWatchers(route.params.user as string, route.params.project as string);
const props = defineProps<{
project: HangarProject;
@ -37,8 +36,8 @@ useHead(
<PageTitle>{{ i18n.t("project.watchers") }}</PageTitle>
</template>
<div v-if="watchers.result && watchers.result.length > 0" class="flex flex-wrap gap-4">
<div v-for="watcher in watchers.result" :key="watcher.name">
<div v-if="watchers?.result?.length > 0" class="flex flex-wrap gap-4">
<div v-for="watcher in watchers?.result" :key="watcher.name">
<UserAvatar size="xs" :username="watcher.name" :avatar-url="avatarUrl(watcher.name)" />
<Link :to="'/' + watcher.name">{{ watcher.name }}</Link>
</div>

View File

@ -37,7 +37,8 @@ const props = defineProps<{
const i18n = useI18n();
const route = useRoute();
const { starred, watching, projects, organizations, pinned } = (await useUserData(props.user.name)).value || {};
const userData = await useUserData(props.user.name);
const { starred, watching, projects, organizations, pinned } = userData.value || { starred: null };
let organizationVisibility = null;
if (props.user.name === useAuthStore().user?.name) {
organizationVisibility = await useOrgVisibility(props.user.name);

View File

@ -6,7 +6,6 @@ import { useRoute } from "vue-router";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import { ReviewAction } from "~/types/enums";
import { useVersionApprovals } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import Tag from "~/components/Tag.vue";
@ -20,7 +19,7 @@ definePageMeta({
const i18n = useI18n();
const route = useRoute();
const data = await useVersionApprovals().catch((e) => handleRequestError(e));
const data = await useVersionApprovals();
const actions = {
ongoing: [ReviewAction.START, ReviewAction.MESSAGE, ReviewAction.UNDO_APPROVAL, ReviewAction.REOPEN],

View File

@ -3,8 +3,6 @@ import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref } from "vue";
import { useHead } from "@vueuse/head";
import { useUnresolvedFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useSeo } from "~/composables/useSeo";
import Flags from "~/components/Flags.vue";
@ -17,7 +15,6 @@ definePageMeta({
const i18n = useI18n();
const route = useRoute();
const flags = await useUnresolvedFlags().catch((e) => handleRequestError(e));
const loading = ref<{ [key: number]: boolean }>({});
const selectedTab = ref("unresolved");

View File

@ -3,7 +3,6 @@ import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { useHealthReport } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
@ -16,7 +15,7 @@ definePageMeta({
const i18n = useI18n();
const route = useRoute();
const healthReport = await useHealthReport().catch((e) => handleRequestError(e));
const healthReport = await useHealthReport();
useHead(useSeo(i18n.t("health.title"), null, route, null));
</script>

View File

@ -8,7 +8,6 @@ import { LoggedAction, LoggedActionType } from "hangar-internal";
import { debounce } from "lodash-es";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useActionLogs } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Link from "~/lib/components/design/Link.vue";
@ -27,7 +26,7 @@ definePageMeta({
const i18n = useI18n();
const route = useRoute();
const loggedActions = await useActionLogs().catch((e) => handleRequestError(e));
const loggedActions = await useActionLogs();
// TODO add support for sorting
const headers = [

View File

@ -15,7 +15,7 @@ import SortableTable, { Header } from "~/components/SortableTable.vue";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import { useSeo } from "~/composables/useSeo";
import { authUrl, forumUserUrl } from "~/composables/useUrlHelper";
import { useUser } from "~/composables/useApiHelper";
import { useProjects, useUser } from "~/composables/useApiHelper";
import Tag from "~/components/Tag.vue";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
import { useBackendData } from "~/store/backendData";
@ -30,13 +30,11 @@ const i18n = useI18n();
const route = useRoute();
const router = useRouter();
const projects = await useApi<PaginatedResult<Project>>("projects", "get", {
owner: route.params.user,
}).catch((e) => handleRequestError(e));
const projects = await useProjects({ owner: route.params.user });
const orgs = await useInternalApi<{ [key: string]: OrganizationRoleTable }>(`organizations/${route.params.user}/userOrganizations`).catch((e) =>
handleRequestError(e)
);
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e));
const user = await useUser(route.params.user as string);
const projectsConfig = [
{ title: i18n.t("userAdmin.project"), name: "name" },
@ -67,7 +65,7 @@ const selectedRole = ref();
async function processRole(add: boolean) {
try {
await useInternalApi("/admin/user/" + route.params.user + "/" + selectedRole.value, add ? "POST" : "DELETE");
if (user) {
if (user?.value) {
user.value = await useApi<User>(("users/" + route.params.user) as string);
}
} catch (e) {
@ -90,7 +88,7 @@ useHead(useSeo(i18n.t("userAdmin.title") + " " + route.params.user, null, route,
<Card class="basis-full md:basis-8/12">
<template #header>{{ i18n.t("userAdmin.roles") }}</template>
<div class="space-x-1">
<Tag v-for="role in user.roles" :key="role.value" :color="{ background: role.color }" :name="role.title" />
<Tag v-for="role in user?.roles" :key="role.value" :color="{ background: role.color }" :name="role.title" />
</div>
<div class="flex mt-2">
@ -98,12 +96,12 @@ useHead(useSeo(i18n.t("userAdmin.title") + " " + route.params.user, null, route,
<InputSelect v-model="selectedRole" :values="useBackendData.globalRoles" item-text="title" item-value="value"></InputSelect>
</div>
<div>
<Button size="medium" :disabled="!selectedRole || user.roles.some((r) => r.value === selectedRole)" @click="processRole(true)">
<Button size="medium" :disabled="!selectedRole || user?.roles.some((r) => r.value === selectedRole)" @click="processRole(true)">
{{ i18n.t("general.add") }}
</Button>
</div>
<div class="ml-2">
<Button size="medium" :disabled="!selectedRole || !user.roles.some((r) => r.value === selectedRole)" @click="processRole(false)">
<Button size="medium" :disabled="!selectedRole || !user?.roles.some((r) => r.value === selectedRole)" @click="processRole(false)">
{{ i18n.t("general.delete") }}
</Button>
</div>

View File

@ -10,7 +10,7 @@ import Tag from "~/components/Tag.vue";
import { useApi } from "~/composables/useApi";
import { Header } from "~/components/SortableTable.vue";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta, handleRequestError, watch } from "#imports";
import { definePageMeta, watch } from "#imports";
import { useUsers } from "~/composables/useApiHelper";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import InputText from "~/lib/components/ui/InputText.vue";
@ -33,7 +33,7 @@ const headers = [
{ name: "org", title: i18n.t("pages.headers.organization"), sortable: true },
] as Header[];
const users = await useUsers().catch((e) => handleRequestError(e));
const users = await useUsers();
const page = ref(0);
const sort = ref<string[]>([]);
const query = ref();

View File

@ -32,7 +32,7 @@ const requestParams = computed(() => {
sort: sort.value,
};
});
const authors = await useAuthors(requestParams.value).catch((e) => handleRequestError(e));
const authors = await useAuthors(requestParams.value);
async function updateSort(col: string, sorter: Record<string, number>) {
sort.value = [...Object.keys(sorter)]

View File

@ -9,7 +9,6 @@ import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import { useBackendData, useVisibleCategories, useVisiblePlatforms } from "~/store/backendData";
import ProjectList from "~/components/projects/ProjectList.vue";
import { useProjects } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import Container from "~/lib/components/design/Container.vue";
import { useSeo } from "~/composables/useSeo";
@ -67,7 +66,7 @@ const requestParams = computed(() => {
return params;
});
const p = await useProjects(requestParams.value).catch((e) => handleRequestError(e));
const p = await useProjects(requestParams.value);
if (p && p.value) {
projects.value = p.value;
await checkOffsetLargerCount();
@ -141,7 +140,7 @@ const script = {
if (isRef(meta.script)) {
meta.script.value.push(script);
} else {
meta.script = meta.script || [];
meta.script = (meta.script || []) as [];
meta.script.push(script);
}
useHead(meta);

View File

@ -30,11 +30,11 @@ const route = useRoute();
const notificationStore = useNotificationStore();
// TODO send in one
const unreadNotifications = (await useUnreadNotifications().catch((e) => handleRequestError(e))) as Ref<PaginatedResult<HangarNotification>>;
const readNotifications = (await useReadNotifications().catch((e) => handleRequestError(e))) as Ref<PaginatedResult<HangarNotification>>;
const allNotifications = (await useNotifications().catch((e) => handleRequestError(e))) as Ref<PaginatedResult<HangarNotification>>;
const notifications: Ref<PaginatedResult<HangarNotification>> = ref(unreadNotifications.value);
const invites = (await useInvites().catch((e) => handleRequestError(e))) as Ref<Invites>;
const unreadNotifications = await useUnreadNotifications();
const readNotifications = await useReadNotifications();
const allNotifications = await useNotifications();
const notifications: Ref<PaginatedResult<HangarNotification> | null> = ref(unreadNotifications.value);
const invites = await useInvites();
const selectedTab = ref("unread");
const selectedTabs: Tab[] = [
@ -66,6 +66,7 @@ useHead(useSeo("Notifications", null, route, null));
async function markAllAsRead() {
await useInternalApi(`markallread`, "post").catch((e) => handleRequestError(e));
if (!unreadNotifications.value) return;
unreadNotifications.value.result = [];
unreadNotifications.value.pagination.limit = 0;
unreadNotifications.value.pagination.offset = 0;
@ -75,6 +76,7 @@ async function markAllAsRead() {
async function markNotificationRead(notification: HangarNotification, router = true) {
await useInternalApi(`notifications/${notification.id}`, "post").catch((e) => handleRequestError(e));
notification.read = true;
if (!notifications.value) return;
notifications.value.result = notifications.value.result.filter((n) => n !== notification);
if (notification.action && router) {
await useRouter().push(notification.action);
@ -86,6 +88,7 @@ async function updateInvite(invite: Invite, status: "accept" | "decline") {
if (status === "accept") {
invite.accepted = true;
} else {
if (!invites.value) return;
invites.value[invite.type] = invites.value[invite.type].filter((i) => i.roleTableId !== invite.roleTableId);
}
notificationStore.success(i18n.t(`notifications.invite.msgs.${status}`, [invite.name]));
@ -112,11 +115,11 @@ function updateSelectedNotifications() {
<!-- Abuse tabs a little -->
<Tabs v-model="selectedTab" :tabs="selectedTabs" :vertical="false" @click="updateSelectedNotifications()" />
<div v-if="notifications.result.length === 0" class="text-lg">
<div v-if="notifications?.result.length === 0" class="text-lg">
{{ i18n.t(`notifications.empty.${selectedTab}`) }}
</div>
<Pagination v-if="notifications.result.length !== 0" :items="notifications.result">
<Pagination v-if="notifications?.result.length !== 0" :items="notifications?.result">
<template #default="{ item }">
<div class="p-1 mb-1 flex items-center">
<div class="inline-flex items-center flex-grow">
@ -139,7 +142,7 @@ function updateSelectedNotifications() {
</div>
</template>
</Pagination>
<Button v-if="notifications.result.length > 1 && selectedTab === 'unread'" size="small" @click="markAllAsRead">
<Button v-if="notifications?.result.length > 1 && selectedTab === 'unread'" size="small" @click="markAllAsRead">
{{ i18n.t("notifications.readAll") }}
</Button>
</Card>

View File

@ -34,7 +34,7 @@ const requestParams = computed(() => {
sort: sort.value,
};
});
const staff = await useStaff(requestParams.value).catch((e) => handleRequestError(e));
const staff = await useStaff(requestParams.value);
async function updateSort(col: string, sorter: Record<string, number>) {
sort.value = [...Object.keys(sorter)]