mirror of
https://github.com/hay-kot/homebox.git
synced 2024-11-26 02:25:41 +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;
|
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 {
|
export interface PaginationResultItemSummary {
|
||||||
items: ItemSummary[];
|
items: ItemSummary[];
|
||||||
page: number;
|
page: number;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ActionsAPI } from "./classes/actions";
|
||||||
import { StatsAPI } from "./classes/stats";
|
import { StatsAPI } from "./classes/stats";
|
||||||
import { AssetsApi } from "./classes/assets";
|
import { AssetsApi } from "./classes/assets";
|
||||||
import { ReportsAPI } from "./classes/reports";
|
import { ReportsAPI } from "./classes/reports";
|
||||||
|
import { NotifiersAPI } from "./classes/notifiers";
|
||||||
import { Requests } from "~~/lib/requests";
|
import { Requests } from "~~/lib/requests";
|
||||||
|
|
||||||
export class UserClient extends BaseAPI {
|
export class UserClient extends BaseAPI {
|
||||||
|
@ -20,6 +21,7 @@ export class UserClient extends BaseAPI {
|
||||||
stats: StatsAPI;
|
stats: StatsAPI;
|
||||||
assets: AssetsApi;
|
assets: AssetsApi;
|
||||||
reports: ReportsAPI;
|
reports: ReportsAPI;
|
||||||
|
notifiers: NotifiersAPI;
|
||||||
|
|
||||||
constructor(requests: Requests, attachmentToken: string) {
|
constructor(requests: Requests, attachmentToken: string) {
|
||||||
super(requests, attachmentToken);
|
super(requests, attachmentToken);
|
||||||
|
@ -33,6 +35,7 @@ export class UserClient extends BaseAPI {
|
||||||
this.stats = new StatsAPI(requests);
|
this.stats = new StatsAPI(requests);
|
||||||
this.assets = new AssetsApi(requests);
|
this.assets = new AssetsApi(requests);
|
||||||
this.reports = new ReportsAPI(requests);
|
this.reports = new ReportsAPI(requests);
|
||||||
|
this.notifiers = new NotifiersAPI(requests);
|
||||||
|
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Detail } from "~~/components/global/DetailsSection/types";
|
import { Detail } from "~~/components/global/DetailsSection/types";
|
||||||
import { themes } from "~~/lib/data/themes";
|
import { themes } from "~~/lib/data/themes";
|
||||||
import { currencies, Currency } from "~~/lib/data/currency";
|
import { currencies, Currency } from "~~/lib/data/currency";
|
||||||
|
import { NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth"],
|
middleware: ["auth"],
|
||||||
|
@ -167,6 +168,118 @@
|
||||||
passwordChange.current = "";
|
passwordChange.current = "";
|
||||||
passwordChange.loading = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -190,6 +303,24 @@
|
||||||
</div>
|
</div>
|
||||||
</BaseModal>
|
</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">
|
<BaseContainer class="flex flex-col gap-4 mb-6">
|
||||||
<BaseCard>
|
<BaseCard>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
@ -218,6 +349,50 @@
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</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>
|
<BaseCard>
|
||||||
<template #title>
|
<template #title>
|
||||||
<BaseSectionHeader class="pb-0">
|
<BaseSectionHeader class="pb-0">
|
||||||
|
@ -233,8 +408,8 @@
|
||||||
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
|
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
|
||||||
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4">
|
||||||
<BaseButton @click="updateGroup"> Update Group </BaseButton>
|
<BaseButton size="sm" @click="updateGroup"> Update Group </BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
@ -299,7 +474,7 @@
|
||||||
</BaseSectionHeader>
|
</BaseSectionHeader>
|
||||||
|
|
||||||
<div class="py-4 border-t-2 border-gray-300">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
Loading…
Reference in a new issue