mirror of
https://github.com/hay-kot/homebox.git
synced 2024-11-22 08:35:43 +00:00
wip: notifiers UI
This commit is contained in:
parent
30b1879c35
commit
b6f44dfe58
5 changed files with 298 additions and 3 deletions
59
frontend/lib/api/__test__/user/notifier.test.ts
Normal file
59
frontend/lib/api/__test__/user/notifier.test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { factories } from "../factories";
|
||||
|
||||
describe("basic notifier workflows", () => {
|
||||
test("user should be able to create, update, and delete a notifier", async () => {
|
||||
const { client } = await factories.client.singleUse();
|
||||
|
||||
// Create Notifier
|
||||
const result = await client.notifiers.create({
|
||||
name: faker.name.firstName(),
|
||||
url: faker.internet.url(),
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
expect(result.error).toBeFalsy();
|
||||
expect(result.status).toBe(201);
|
||||
expect(result.data).toBeTruthy();
|
||||
|
||||
const notifier = result.data;
|
||||
|
||||
// Update Notifier with new URL
|
||||
{
|
||||
const updateData = {
|
||||
name: faker.name.firstName(),
|
||||
url: faker.internet.url(),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const updateResult = await client.notifiers.update(notifier.id, updateData);
|
||||
expect(updateResult.error).toBeFalsy();
|
||||
expect(updateResult.status).toBe(200);
|
||||
expect(updateResult.data).toBeTruthy();
|
||||
expect(updateResult.data.name).not.toBe(notifier.name);
|
||||
}
|
||||
|
||||
// Update Notifier with empty URL
|
||||
{
|
||||
const updateData = {
|
||||
name: faker.name.firstName(),
|
||||
url: "",
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const updateResult = await client.notifiers.update(notifier.id, updateData);
|
||||
expect(updateResult.error).toBeFalsy();
|
||||
expect(updateResult.status).toBe(200);
|
||||
expect(updateResult.data).toBeTruthy();
|
||||
expect(updateResult.data.name).not.toBe(notifier.name);
|
||||
}
|
||||
|
||||
// Delete Notifier
|
||||
{
|
||||
const deleteResult = await client.notifiers.delete(notifier.id);
|
||||
expect(deleteResult.error).toBeFalsy();
|
||||
expect(deleteResult.status).toBe(204);
|
||||
}
|
||||
});
|
||||
});
|
28
frontend/lib/api/classes/notifiers.ts
Normal file
28
frontend/lib/api/classes/notifiers.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { NotifierCreate, NotifierOut, NotifierUpdate } from "../types/data-contracts";
|
||||
|
||||
export class NotifiersAPI extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<NotifierOut[]>({ url: route("/notifiers") });
|
||||
}
|
||||
|
||||
create(body: NotifierCreate) {
|
||||
return this.http.post<NotifierCreate, NotifierOut>({ url: route("/notifiers"), body });
|
||||
}
|
||||
|
||||
update(id: string, body: NotifierUpdate) {
|
||||
if (body.url === "") {
|
||||
body.url = null;
|
||||
}
|
||||
|
||||
return this.http.put<NotifierUpdate, NotifierOut>({ url: route(`/notifiers/${id}`), body });
|
||||
}
|
||||
|
||||
delete(id: string) {
|
||||
return this.http.delete<void>({ url: route(`/notifiers/${id}`) });
|
||||
}
|
||||
|
||||
test(url: string) {
|
||||
return this.http.post<{ url: string }, null>({ url: route(`/notifiers/test`), body: { url } });
|
||||
}
|
||||
}
|
|
@ -267,6 +267,36 @@ export interface MaintenanceLog {
|
|||
itemId: string;
|
||||
}
|
||||
|
||||
export interface NotifierCreate {
|
||||
isActive: boolean;
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface NotifierOut {
|
||||
createdAt: Date | string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
updatedAt: Date | string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface NotifierUpdate {
|
||||
isActive: boolean;
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface PaginationResultItemSummary {
|
||||
items: ItemSummary[];
|
||||
page: number;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ActionsAPI } from "./classes/actions";
|
|||
import { StatsAPI } from "./classes/stats";
|
||||
import { AssetsApi } from "./classes/assets";
|
||||
import { ReportsAPI } from "./classes/reports";
|
||||
import { NotifiersAPI } from "./classes/notifiers";
|
||||
import { Requests } from "~~/lib/requests";
|
||||
|
||||
export class UserClient extends BaseAPI {
|
||||
|
@ -20,6 +21,7 @@ export class UserClient extends BaseAPI {
|
|||
stats: StatsAPI;
|
||||
assets: AssetsApi;
|
||||
reports: ReportsAPI;
|
||||
notifiers: NotifiersAPI;
|
||||
|
||||
constructor(requests: Requests, attachmentToken: string) {
|
||||
super(requests, attachmentToken);
|
||||
|
@ -33,6 +35,7 @@ export class UserClient extends BaseAPI {
|
|||
this.stats = new StatsAPI(requests);
|
||||
this.assets = new AssetsApi(requests);
|
||||
this.reports = new ReportsAPI(requests);
|
||||
this.notifiers = new NotifiersAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { themes } from "~~/lib/data/themes";
|
||||
import { currencies, Currency } from "~~/lib/data/currency";
|
||||
import { NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
|
@ -167,6 +168,118 @@
|
|||
passwordChange.current = "";
|
||||
passwordChange.loading = false;
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Notifiers
|
||||
|
||||
const notifiers = useAsyncData(async () => {
|
||||
const { data } = await api.notifiers.getAll();
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const targetID = ref("");
|
||||
const notifier = ref<NotifierCreate | null>(null);
|
||||
const notifierDialog = ref(false);
|
||||
|
||||
function openNotifierDialog(v: NotifierOut | null) {
|
||||
if (v) {
|
||||
targetID.value = v.id;
|
||||
notifier.value = {
|
||||
name: v.name,
|
||||
url: "",
|
||||
isActive: v.isActive,
|
||||
};
|
||||
} else {
|
||||
notifier.value = {
|
||||
name: "",
|
||||
url: "",
|
||||
isActive: true,
|
||||
};
|
||||
}
|
||||
|
||||
notifierDialog.value = true;
|
||||
}
|
||||
|
||||
async function createNotifier() {
|
||||
if (!notifier.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetID.value) {
|
||||
await editNotifier();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.notifiers.create({
|
||||
name: notifier.value.name,
|
||||
url: notifier.value.url || "",
|
||||
isActive: notifier.value.isActive,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
notify.error("Failed to create notifier.");
|
||||
}
|
||||
|
||||
notifier.value = null;
|
||||
notifierDialog.value = false;
|
||||
|
||||
await notifiers.refresh();
|
||||
}
|
||||
|
||||
async function editNotifier() {
|
||||
if (!notifier.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.notifiers.update(targetID.value, {
|
||||
name: notifier.value.name,
|
||||
url: notifier.value.url || "",
|
||||
isActive: notifier.value.isActive,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
notify.error("Failed to update notifier.");
|
||||
}
|
||||
|
||||
notifier.value = null;
|
||||
notifierDialog.value = false;
|
||||
targetID.value = "";
|
||||
|
||||
await notifiers.refresh();
|
||||
}
|
||||
|
||||
async function deleteNotifier(id: string) {
|
||||
const result = await confirm.open("Are you sure you want to delete this notifier?");
|
||||
|
||||
if (result.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.notifiers.delete(id);
|
||||
|
||||
if (error) {
|
||||
notify.error("Failed to delete notifier.");
|
||||
return;
|
||||
}
|
||||
|
||||
await notifiers.refresh();
|
||||
}
|
||||
|
||||
async function testNotifier() {
|
||||
if (!notifier.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.notifiers.test(notifier.value.url);
|
||||
|
||||
if (error) {
|
||||
notify.error("Failed to test notifier.");
|
||||
return;
|
||||
}
|
||||
|
||||
notify.success("Notifier test successful.");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -190,6 +303,24 @@
|
|||
</div>
|
||||
</BaseModal>
|
||||
|
||||
<BaseModal v-model="notifierDialog">
|
||||
<template #title> {{ notifier ? "Edit" : "Create" }} Notifier </template>
|
||||
|
||||
<form @submit.prevent="createNotifier">
|
||||
<template v-if="notifier">
|
||||
<FormTextField v-model="notifier.name" label="Name" />
|
||||
<FormTextField v-model="notifier.url" label="URL" />
|
||||
<div class="max-w-[100px]">
|
||||
<FormCheckbox v-model="notifier.isActive" label="Enabled" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex gap-2 justify-between mt-4">
|
||||
<BaseButton :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier"> Test </BaseButton>
|
||||
<BaseButton type="submit"> Submit </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
|
||||
<BaseContainer class="flex flex-col gap-4 mb-6">
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
|
@ -218,6 +349,50 @@
|
|||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-megaphone" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Notifiers </span>
|
||||
<template #description> Get notifications for up coming maintenance reminders </template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="notifiers.data.value" class="mx-4 divide-y divide-gray-400 rounded-md border border-gray-400">
|
||||
<article v-for="n in notifiers.data.value" :key="n.id" class="p-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="mr-auto text-xl">{{ n.name }}</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<div class="tooltip" data-tip="Delete">
|
||||
<button class="btn btn-sm btn-square" @click="deleteNotifier(n.id)">
|
||||
<Icon name="mdi-delete" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip" data-tip="Edit">
|
||||
<button class="btn btn-sm btn-square" @click="openNotifierDialog(n)">
|
||||
<Icon name="mdi-pencil" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between py-1 flex-wrap text-sm">
|
||||
<p>
|
||||
<span v-if="n.isActive" class="text-success font-bold"> Active </span>
|
||||
<span v-else class="text-error font-bold"> Inactive</span>
|
||||
</p>
|
||||
<p>
|
||||
Created
|
||||
<DateTime format="relative" datetime-type="time" :date="n.createdAt" />
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<BaseButton size="sm" @click="openNotifierDialog"> Create </BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader class="pb-0">
|
||||
|
@ -233,8 +408,8 @@
|
|||
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
|
||||
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<BaseButton @click="updateGroup"> Update Group </BaseButton>
|
||||
<div class="mt-4">
|
||||
<BaseButton size="sm" @click="updateGroup"> Update Group </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
@ -299,7 +474,7 @@
|
|||
</BaseSectionHeader>
|
||||
|
||||
<div class="py-4 border-t-2 border-gray-300">
|
||||
<BaseButton class="btn-error" @click="deleteProfile"> Delete Account </BaseButton>
|
||||
<BaseButton size="sm" class="btn-error" @click="deleteProfile"> Delete Account </BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCard>
|
||||
|
|
Loading…
Reference in a new issue