From b6f44dfe58969255274a4201dd41252a1ff6572c Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 5 Mar 2023 11:07:16 -0900 Subject: [PATCH] wip: notifiers UI --- .../lib/api/__test__/user/notifier.test.ts | 59 ++++++ frontend/lib/api/classes/notifiers.ts | 28 +++ frontend/lib/api/types/data-contracts.ts | 30 +++ frontend/lib/api/user.ts | 3 + frontend/pages/profile.vue | 181 +++++++++++++++++- 5 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 frontend/lib/api/__test__/user/notifier.test.ts create mode 100644 frontend/lib/api/classes/notifiers.ts diff --git a/frontend/lib/api/__test__/user/notifier.test.ts b/frontend/lib/api/__test__/user/notifier.test.ts new file mode 100644 index 0000000..717c03a --- /dev/null +++ b/frontend/lib/api/__test__/user/notifier.test.ts @@ -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); + } + }); +}); diff --git a/frontend/lib/api/classes/notifiers.ts b/frontend/lib/api/classes/notifiers.ts new file mode 100644 index 0000000..c9e4a25 --- /dev/null +++ b/frontend/lib/api/classes/notifiers.ts @@ -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({ url: route("/notifiers") }); + } + + create(body: NotifierCreate) { + return this.http.post({ url: route("/notifiers"), body }); + } + + update(id: string, body: NotifierUpdate) { + if (body.url === "") { + body.url = null; + } + + return this.http.put({ url: route(`/notifiers/${id}`), body }); + } + + delete(id: string) { + return this.http.delete({ url: route(`/notifiers/${id}`) }); + } + + test(url: string) { + return this.http.post<{ url: string }, null>({ url: route(`/notifiers/test`), body: { url } }); + } +} diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 2555e10..adee10d 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -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; diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index fa08258..10a082b 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -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); } diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index f085e2a..b744008 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -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(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."); + }