wip: notifiers UI

This commit is contained in:
Hayden 2023-03-05 11:07:16 -09:00
parent 30b1879c35
commit b6f44dfe58
No known key found for this signature in database
GPG key ID: 17CF79474E257545
5 changed files with 298 additions and 3 deletions

View 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);
}
});
});

View 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 } });
}
}

View file

@ -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;

View file

@ -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);
} }

View file

@ -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>