From 976615c330e8af91f61cd0c774c3a15a48e9d267 Mon Sep 17 00:00:00 2001 From: Ashley Date: Wed, 27 Sep 2023 01:21:57 +0100 Subject: [PATCH] feat: Add ButtonInput component - Import ButtonInput.vue component from the components/FormKit folder. - Create a new input called "inputButton" and assign the ButtonInput component to it. --- frontend/src/formkit.config.ts | 4 + frontend/src/formkit.theme.ts | 21 ++++- frontend/src/modules/setup/router/index.ts | 6 +- frontend/src/plugins/ModalWrapper.tsx | 38 +++++++- frontend/src/plugins/modal.ts | 16 +++- frontend/src/plugins/sentry.ts | 7 +- frontend/src/stores/libraries.ts | 101 +++++++++++++++++---- 7 files changed, 159 insertions(+), 34 deletions(-) diff --git a/frontend/src/formkit.config.ts b/frontend/src/formkit.config.ts index 77008176d..b2d678bb2 100644 --- a/frontend/src/formkit.config.ts +++ b/frontend/src/formkit.config.ts @@ -1,5 +1,6 @@ import { createProPlugin, inputs } from "@formkit/pro"; +import ButtonInput from "./components/FormKit/ButtonInput.vue"; import type { DefaultConfigOptions } from "@formkit/vue"; import OneTimePassword from "./components/FormKit/OneTimePassword.vue"; import { createInput } from "@formkit/vue"; @@ -35,6 +36,9 @@ const config: DefaultConfigOptions = { otp: createInput(OneTimePassword, { props: ["digits"], }), + inputButton: createInput(ButtonInput, { + type: "input", + }), }, }; diff --git a/frontend/src/formkit.theme.ts b/frontend/src/formkit.theme.ts index fc4d32a2b..31e691084 100644 --- a/frontend/src/formkit.theme.ts +++ b/frontend/src/formkit.theme.ts @@ -15,6 +15,15 @@ const theme: Record> = { suffixIcon: "peer absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none bg-gray-50 dark:bg-gray-700 rounded-r border-r border-t border-b border-gray-300 dark:border-gray-600", }, + inputButton: { + input: "peer-[.formkit-prefix-icon]:pl-9 peer-[.formkit-suffix-icon]:pr-9 mb-1 w-full bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white sm:text-sm border border-gray-300 dark:border-gray-600 rounded block w-full dark:placeholder-gray-400 focus:ring-primary focus:border-primary", + label: "block mb-2 text-sm font-medium text-gray-900 dark:text-white", + inner: "w-full relative", + outer: "mb-4 formkit-disabled:opacity-50", + prefixIcon: "peer absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none bg-gray-50 dark:bg-gray-700 rounded-l border-l border-t border-b border-gray-300 dark:border-gray-600", + suffixIcon: "peer absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none bg-gray-50 dark:bg-gray-700 rounded-r border-r border-t border-b border-gray-300 dark:border-gray-600", + }, + email: { input: "peer-[.formkit-prefix-icon]:pl-9 peer-[.formkit-suffix-icon]:pr-9 mb-1 w-full bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white sm:text-sm border border-gray-300 dark:border-gray-600 rounded block w-full dark:placeholder-gray-400 focus:ring-primary focus:border-primary", label: "block mb-2 text-sm font-medium text-gray-900 dark:text-white", @@ -120,8 +129,16 @@ const theme: Record> = { }, "family:button": { - input: "$reset inline-flex items-center bg-primary hover:bg-primary_hover focus:outline-none text-white font-medium rounded dark:bg-primary dark:hover:bg-primary_hover px-5 py-2.5 text-sm formkit-disabled:bg-gray-400 formkit-loading:before:w-4 formkit-loading:before:h-4 formkit-loading:before:mr-2 formkit-loading:before:border formkit-loading:before:border-2 formkit-loading:before:border-r-transparent formkit-loading:before:rounded-3xl formkit-loading:before:border-white formkit-loading:before:animate-spin", - // wrapper: "mb-1", + input: ` + bg-primary hover:bg-primary_hover + data-[theme=primary]:bg-primary data-[theme=primary]:hover:bg-primary_hover + data-[theme=secondary]:bg-secondary data-[theme=secondary]:hover:bg-secondary_hover + data-[theme=danger]:bg-red-600 data-[theme=danger]:hover:bg-red-500 + data-[theme=success]:bg-green-500 data-[theme=success]:hover:bg-green-600 + data-[theme=warning]:bg-yellow-500 data-[theme=warning]:hover:bg-yellow-600 + + inline-flex items-center focus:outline-none text-white font-medium rounded px-5 py-2.5 text-sm + `, prefixIcon: "$reset block w-4 mr-2 stretch", suffixIcon: "$reset block w-4 ml-2 stretch", }, diff --git a/frontend/src/modules/setup/router/index.ts b/frontend/src/modules/setup/router/index.ts index f12322d10..c0378d06d 100644 --- a/frontend/src/modules/setup/router/index.ts +++ b/frontend/src/modules/setup/router/index.ts @@ -3,12 +3,8 @@ import type { RouteRecordRaw } from "vue-router"; const routes: Readonly = [ { path: "/setup", - redirect: "/setup/welcome", - }, - { - path: "/setup/:step", name: "setup", - component: () => import("@/views/SetupViews/SetupView.vue"), + component: () => import("../views/Setup.vue"), }, ]; diff --git a/frontend/src/plugins/ModalWrapper.tsx b/frontend/src/plugins/ModalWrapper.tsx index 9ed77c074..c8d3d6161 100644 --- a/frontend/src/plugins/ModalWrapper.tsx +++ b/frontend/src/plugins/ModalWrapper.tsx @@ -1,12 +1,44 @@ -import type { CustomModalOptions } from "./modal"; +import type { CustomModalOptions, CustomModalOptionsButtons } from "./modal"; + import { FormKit } from "@formkit/vue"; import { Modal } from "jenesius-vue-modal"; import type { WrapComponent } from "jenesius-vue-modal/dist/types/types/types"; import { defineComponent } from "vue"; +import mitt from "mitt"; const ModalWrapper =

(component: P | string, props?: any, options?: Partial) => { return defineComponent({ name: "ModalWrapper", + data() { + return { + eventBus: mitt(), + }; + }, + computed: { + attrs() { + // Create a shallow copy of the props and $attrs objects + const localAttrs = { ...props, ...this.$attrs, eventBus: this.eventBus }; + + // Iterate over the button actions and add a new property to the test object for each action + options?.actions?.forEach((action) => { + const alphaKey = action.event.replace(/([A-Z])/g, "-$1").toLowerCase(); + const key = `on${alphaKey.charAt(0).toUpperCase() + alphaKey.slice(1)}`; + localAttrs[key] = action.callback ?? (() => {}); + }); + + // Add custom close action + localAttrs.onClose = () => this.$emit(Modal.EVENT_PROMPT, false); + + // Return the test object + return localAttrs; + }, + }, + methods: { + buttonTrigger(button: CustomModalOptionsButtons) { + button.onClick?.(); + if (button.emit) this.eventBus.emit(button.emit); + }, + }, render() { return (

@@ -28,13 +60,13 @@ const ModalWrapper =

(component: P | string, props?: an {/* Body */}

{/* String or Component */} - {typeof component === "string" ?

{component}

: } + {typeof component === "string" ?

{component}

: }
{/* Footer */} {!options?.disableFooter ? (
{options?.buttons?.map((button) => ( - button.onClick!} key={button.text}> + this.buttonTrigger(button)} key={button.text}> {button.text} ))} diff --git a/frontend/src/plugins/modal.ts b/frontend/src/plugins/modal.ts index 9b3ce478a..7d85761d0 100644 --- a/frontend/src/plugins/modal.ts +++ b/frontend/src/plugins/modal.ts @@ -1,3 +1,4 @@ +import type { EventType, Handler } from "mitt"; import { Modal, closeById, closeModal, config, getComponentFromStore, getCurrentModal, modalQueue, onBeforeModalClose, openModal, popModal, promptModal, pushModal, useModalRouter } from "jenesius-vue-modal"; import type { App } from "vue"; @@ -7,6 +8,18 @@ import type { ModalOptions } from "jenesius-vue-modal/dist/types/utils/Modal"; import ModalWrapper from "./ModalWrapper"; import type { WrapComponent } from "jenesius-vue-modal/dist/types/types/types"; +export declare interface CustomModalOptionsButtons { + text: string; + classes?: Record | FormKitClasses>; + onClick?: () => void; + emit?: string; +} + +export declare interface CustomModalOptionsActions { + event: string; + callback: (options: Partial) => void; +} + export declare interface CustomModalOptions extends Partial { title?: string; disableHeader?: boolean; @@ -16,7 +29,8 @@ export declare interface CustomModalOptions extends Partial { confirmButtonText?: string; disableCancelButton?: boolean; cancelButtonText?: string; - buttons?: Array | FormKitClasses>; onClick: () => void }>>; + buttons?: CustomModalOptionsButtons[]; + actions?: CustomModalOptionsActions[]; } const localOpenModal = async

(component: P | string, options?: Partial, props?: any): Promise => { diff --git a/frontend/src/plugins/sentry.ts b/frontend/src/plugins/sentry.ts index 5e41eca28..7d9d69e4e 100644 --- a/frontend/src/plugins/sentry.ts +++ b/frontend/src/plugins/sentry.ts @@ -1,8 +1,8 @@ import { BrowserTracing, Replay, init, vueRouterInstrumentation } from "@sentry/vue"; -import DefaultToast from "@/components/Toasts/DefaultToast.vue"; - import type { Options, TracingOptions } from "@sentry/vue/types/types"; + import type { App } from "vue"; +import DefaultToast from "@/components/Toasts/DefaultToast.vue"; type SentryOptions = Partial< Omit & { @@ -13,6 +13,7 @@ type SentryOptions = Partial< const vuePluginSentry = { install: (app: App, options?: SentryOptions) => { init({ + app: app, dsn: "https://d1994be8f88578e14f1a4ac06ae65e89@o4505748808400896.ingest.sentry.io/4505780347666432", integrations: [ new BrowserTracing({ @@ -27,7 +28,7 @@ const vuePluginSentry = { ], environment: process.env.NODE_ENV, tracesSampleRate: 1.0, - replaysSessionSampleRate: 1.0, + replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 1.0, beforeSend(event, hint) { if (event.exception) { diff --git a/frontend/src/stores/libraries.ts b/frontend/src/stores/libraries.ts index b7ae844ff..130f85320 100644 --- a/frontend/src/stores/libraries.ts +++ b/frontend/src/stores/libraries.ts @@ -1,6 +1,3 @@ -import axios from "@/ts/utils/axios"; - -import { errorToast } from "@/ts/utils/toasts"; import { defineStore } from "pinia"; export const useLibrariesStore = defineStore("libraries", { @@ -9,25 +6,89 @@ export const useLibrariesStore = defineStore("libraries", { }), actions: { async getLibraries() { - let libraries: Array<{ id: string; name: string; created: Date }> = []; - const response = await axios() - .get("/api/libraries") - .catch((error) => { - errorToast(error); - }); - - if (response?.data) { - libraries = response.data.map((library: { id: string; name: string; created: string }) => { - return { - id: library.id, - name: library.name, - created: new Date(library.created), - }; - }); + // Get the libraries from the API + const response = await this.$axios.get("/api/libraries"); + + // Check if the response is valid + if (!response?.data) { + this.$toast.error("Could not get libraries"); + return; + } + + // Map the libraries to the correct format + this.libraries = response.data.map((library: { id: string; name: string; created: string }) => { + return { + id: library.id, + name: library.name, + created: new Date(library.created), + }; + }); + }, + async saveLibraries(libraries: Array<{ id: string; name: string; selected: boolean }>) { + const formData = new FormData(); + const newLibraries: string[] = []; + + libraries.forEach((library) => { + if (library.selected) { + newLibraries.push(library.id); + } + }); + + formData.append("libraries", JSON.stringify(newLibraries)); + + const response = await this.$axios.post("/api/libraries", formData, { disableInfoToast: true }).catch(() => { + return; + }); + + if (!response?.data?.message) { + this.$toast.error("Could not save libraries"); + return; + } + + this.$toast.info("Successfully saved libraries"); + }, + async scanLibraries() { + // Get the libraries from the API + const libResponse = await this.$axios.get("/api/libraries"); + + // Check if the response is valid + if (!libResponse?.data) { + this.$toast.error("Could not get libraries"); + return; + } + + // Map the libraries to the correct format + const allLibraries = libResponse.data.map((library: { id: string; name: string; created: string }) => { + return { + id: library.id, + name: library.name, + created: new Date(library.created), + }; + }) as Array<{ id: string; name: string; created: Date }>; + + // Update the libraries in the store + this.libraries = allLibraries; + + // Get the libraries from the media server + const scanResponse = await this.$axios.get("/api/scan-libraries"); + + // Check if the response is valid + if (!scanResponse?.data?.libraries) { + this.$toast.error("Could not get libraries"); + return; + } + + // Map the libraries to the correct format + const libraries: [string, string][] = Object.entries(scanResponse.data.libraries); + const newLibraries: Array<{ id: string; name: string; selected: boolean }> = []; + + // Check if the library is selected + for (const [name, id] of libraries) { + const selected = allLibraries.find((library) => library.id === id) !== undefined; + newLibraries.push({ id: id, name: name, selected: selected }); } - this.libraries = libraries; - return libraries; + return newLibraries; }, }, persist: true,