Skip to content

Commit

Permalink
feat: Update ESLint settings and add new modal functionality
Browse files Browse the repository at this point in the history
- Updated ESLint settings to include additional file types and validate specific languages.
- Added new modal functionality to display modals with customizable title, body, footer, and buttons.
- The modal can be closed by clicking on the overlay or pressing the escape key.
- The modal component can be created and destroyed programmatically using the `$modal.create` and `$modal.destroy` methods.
- Added a confirm modal method that prompts the user with a title and message and returns a promise that resolves to a boolean value.
  • Loading branch information
realashleybailey committed Sep 18, 2023
1 parent d840642 commit b1f61db
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 31 deletions.
21 changes: 14 additions & 7 deletions frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@
"source.fixAll.eslint": true,
"source.organizeImports": false
},
"vetur.validation.template": false,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"[nunjucks]": {
"editor.defaultFormatter": "okitavera.vscode-nunjucks-formatter"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"files.associations": {
"*.vue": "vue"
},
"prettier.configPath": ".prettierrc.js",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"editor.wordWrap": "off",
"[xml]": {
"editor.defaultFormatter": "fabianlauer.vs-code-xml-format"
},
// hide node_modules folder
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
}
}
}
18 changes: 17 additions & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<RouterView v-else />
<Offline />
<ReloadPrompt />
<!-- <ServerURLModal v-model:visible="serverURLModalVisible" /> -->
<ModalContainer />
</template>

<script lang="ts">
Expand Down Expand Up @@ -129,6 +129,22 @@ export default defineComponent({
this.$toast.info(UpdateAvailable, { timeout: false, closeButton: false, draggable: false, closeOnClick: false });
}
// this.$modal.create({
// modal: {
// title: "Server URL",
// body: "Please enter the URL of your server.",
// buttons: [
// {
// disabled: false,
// text: "Cancel",
// onClick: () => {
// this.$modal.destroy();
// },
// },
// ],
// },
// });
// Set the server data
this.setServerData(serverData);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/InvitationList/InvitationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export default defineComponent({
this.$toast.info(this.__("Copied to clipboard"));
},
async deleteLocalInvitation() {
await this.deleteInvitation(this.invite.id);
if (await this.$modal.confirm(this.__("Are you sure?"), this.__("Do you really want to delete this invitation?"))) {
await this.deleteInvitation(this.invite.id);
}
},
...mapActions(useInvitationStore, ["deleteInvitation"]),
},
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/UserList/UserItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export default defineComponent({
},
async localDeleteUser() {
this.disabled.delete = true;
await this.deleteUser(this.user.id);
if (await this.$modal.confirm(this.__("Are you sure?"), this.__("Do you really want to delete this user from your media server?"))) {
await this.deleteUser(this.user.id);
}
},
...mapActions(useUsersStore, ["deleteUser"]),
},
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/UserList/UserList.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<template>
<Draggable v-model="users" tag="ul" group="users" ghost-class="moving-card" :animation="200" item-key="id">
<Draggable v-if="users && users.length > 0" v-model="users" tag="ul" group="users" ghost-class="moving-card" :animation="200" item-key="id">
<template #item="{ element }">
<li class="mb-2">
<UserItem :user="element" />
</li>
</template>
</Draggable>
<div v-else class="flex flex-col justify-center items-center space-y-1">
<i class="fa-solid fa-info-circle text-3xl text-gray-400"></i>
<span class="text-gray-400">{{ __("No Users found") }}</span>
</div>
</template>

<script lang="ts">
Expand Down
29 changes: 15 additions & 14 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import "./assets/scss/main.scss";

import { createPinia } from "pinia";
import { createApp } from "vue";
import Axios, { piniaPluginAxios } from "./plugins/axios";
import Filters, { piniaPluginFilters } from "./plugins/filters";
import Socket, { piniaPluginSocketIO } from "./plugins/socket";
import Toast, { piniaPluginToast } from "./plugins/toasts";
import { defaultConfig, plugin } from "@formkit/vue";

import Analytics from "./plugins/analytics";
import App from "./App.vue";
import ToastPlugin from "vue-toastification";
import FloatingVue from "floating-vue";
import Modal from "./plugins/modal";
import Sentry from "./plugins/sentry";
import ToastOptions from "./assets/configs/DefaultToasts";
import ToastPlugin from "vue-toastification";
import VueProgressBar from "@aacassandra/vue3-progressbar";
import { createApp } from "vue";
import { createPinia } from "pinia";
import formkitConfig from "./formkit.config";
import i18n from "./i18n";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import VueProgressBar from "@aacassandra/vue3-progressbar";
import FloatingVue from "floating-vue";
import router from "./router";
import Sentry from "./plugins/sentry";
import Analytics from "./plugins/analytics";

import Toast, { piniaPluginToast } from "./plugins/toasts";
import Axios, { piniaPluginAxios } from "./plugins/axios";
import Socket, { piniaPluginSocketIO } from "./plugins/socket";
import Filters, { piniaPluginFilters } from "./plugins/filters";

import formkitConfig from "./formkit.config";
import { plugin, defaultConfig } from "@formkit/vue";
// import Plugin from "@flavorly/vanilla-components";

const app = createApp(App);
Expand All @@ -44,6 +44,7 @@ app.use(Socket, { uri: window.location.origin });
app.use(Filters);
app.use(Sentry);
app.use(Analytics);
app.use(Modal);

pinia.use(piniaPluginPersistedstate);
pinia.use(piniaPluginToast);
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/modules/admin/pages/Invitations.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
<template>
<AdminTemplate :header="__('Invitations')" :subheader="__('Manage your invitations')" :box-view="boxView">
<template #header>
<FormKit type="button" @click="inviteModal = true" :classes="{ input: '!bg-secondary' }">
<FormKit type="button" @click="openInviteModal" :classes="{ input: '!bg-secondary' }">
{{ __("Create Invitation") }}
</FormKit>
</template>
<template #default>
<InvitationList />
</template>
</AdminTemplate>
<DefaultModal title-string="Create Invite" :visible="inviteModal" @close="inviteModal = false" modal-class="md:w-[500px] lg:w-[600px]">
<div>
<InviteForm />
</div>
</DefaultModal>
</template>

<script lang="ts">
Expand Down Expand Up @@ -41,6 +36,14 @@ export default defineComponent({
inviteModal: false,
};
},
methods: {
openInviteModal() {
this.$modal.create({
modal: { title: this.__("Create Invite"), body: InviteForm },
options: { showFooter: false },
});
},
},
computed: {
...mapState(useThemeStore, ["boxView"]),
},
Expand Down
186 changes: 186 additions & 0 deletions frontend/src/plugins/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import mitt, { type Emitter, type EventType } from "mitt";
import type { App, Component, VNodeProps } from "vue";
import { Transition, createVNode, defineComponent, render } from "vue";

declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
$modal: {
eventBus: Emitter<Record<EventType, any>>;
create: (modal: Partial<ModalOptions>) => void;
destroy: () => void;
confirm: (title?: string, message?: string) => Promise<boolean>;
[key: string]: any;
};
}
}

export type ModalOptions = {
modal: {
title?: string | Component;
body?: string | Component;
footer?: string | Component;
buttons?: Array<Partial<HTMLButtonElement & { text: string; onClick: () => void }>>;
};
options: {
showCloseButton?: boolean;
showHeader?: boolean;
showBody?: boolean;
showFooter?: boolean;
showOverlay?: boolean;
hideOnOverlayClick?: boolean;
hideOnEscapeKey?: boolean;
closeButtonText?: string;
disableAnimation?: boolean;
};
isVisible?: boolean;
};

const defaultModalOptions: ModalOptions["options"] = {
showCloseButton: true,
showHeader: true,
showBody: true,
showFooter: true,
showOverlay: true,
hideOnOverlayClick: false,
hideOnEscapeKey: false,
closeButtonText: "Cancel",
disableAnimation: false,
};

const DefaultModal = defineComponent({
name: "DefaultModal",
data() {
return {
isVisible: false,
modal: {},
options: defaultModalOptions,
} as ModalOptions;
},
methods: {
onDestroy() {
this.modal = {};
this.options = defaultModalOptions;
this.isVisible = false;
},
updateModal(modal: Partial<ModalOptions>) {
this.modal = { ...this.modal, ...modal.modal };
this.options = { ...this.options, ...modal.options };
this.isVisible = true;
},
},
mounted() {
addEventListener("keydown", (e: KeyboardEvent) => {
if (this.options.hideOnEscapeKey && e.key === "Escape") {
this.isVisible = false;
}
});

this.$modal.eventBus.on("create-modal", (modal: Partial<ModalOptions>) => {
this.updateModal(modal);
});

this.$modal.eventBus.on("destroy-modal", () => {
this.onDestroy();
});
},
render() {
return (
<Transition name={this.options.disableAnimation ? "" : "fade"} onAfterLeave={() => this.onDestroy()} mode="out-in">
{this.isVisible ? (
<div class="fixed top-0 bottom-0 left-0 right-0 flex z-30 items-center justify-center">
{this.options.showOverlay ? <div onClick={() => (this.isVisible = !this.options.hideOnOverlayClick)} class="fixed inset-0 bg-gray-700 bg-opacity-75 transition-opacity"></div> : null}
<div class="min-w-[800px] flex flex-col fixed top-0 bottom-0 left-0 right-0 h-full w-full md:h-auto md:w-auto transform text-left shadow-xl transition-all md:relative md:min-w-[30%] md:max-w-2xl md:shadow-none md:transform-none sm:align-middle text-gray-900 dark:text-white">
{/* Modal Title */}
<div class="flex items-center bg-white pl-6 p-3 dark:bg-gray-800 justify-between p-4 border-b dark:border-gray-600 rounded-t">
{this.modal?.title && typeof this.modal.title === "object" ? createVNode(this.modal.title) : null}
<h3 class="text-xl align-center font-semibold text-gray-900 dark:text-white">{this.modal?.title && typeof this.modal.title === "string" ? this.modal.title : null}</h3>
<button onClick={() => (this.isVisible = false)} type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<i class="fa-solid fa-times text-xl"></i>
</button>
</div>

{/* Modal Body */}
<div class="bg-white p-6 dark:bg-gray-800 p-6 space-y-6 flex-grow">
{this.modal?.body && typeof this.modal.body === "object" ? createVNode(this.modal.body) : null}
{this.modal?.body && typeof this.modal.body === "string" ? <p>{this.modal.body}</p> : null}
</div>

{/* Modal Footer */}
{this.options.showFooter ? (
<div class="flex items-center justify-end bg-white p-6 dark:bg-gray-800 p-6 space-x-2 border-t border-gray-200 dark:border-gray-600 rounded-b">
{this.modal?.buttons?.map((button) => (
// @ts-ignore
<button onClick={button.onClick} {...button} type="button" class="px-5 py-2.5 text-sm whitespace-nowrap overflow-hidden overflow-ellipsis flex items-center justify-center relative bg-secondary hover:bg-secondary_hover focus:outline-none text-white font-medium rounded dark:bg-secondary dark:hover:bg-secondary_hover" key={button.text}>
<span>{button.text}</span>
</button>
))}
{this.modal?.footer && typeof this.modal.footer === "object" ? createVNode(this.modal.footer) : null}
{this.modal?.footer && typeof this.modal.footer === "string" ? <p>{this.modal.footer}</p> : null}
{this.options.showCloseButton ? (
<button onClick={() => (this.isVisible = false)} type="button" class="px-5 py-2.5 text-sm whitespace-nowrap overflow-hidden overflow-ellipsis flex items-center justify-center relative bg-secondary hover:bg-secondary_hover focus:outline-none text-white font-medium rounded dark:bg-secondary dark:hover:bg-secondary_hover">
<span>{this.options.closeButtonText}</span>
</button>
) : null}
</div>
) : null}
</div>
</div>
) : null}
</Transition>
);
},
});

const create = (eventBus: Emitter<Record<EventType, any>>, modal: Partial<ModalOptions>) => eventBus.emit("create-modal", modal) as unknown as void;
const destroy = (eventBus: Emitter<Record<EventType, any>>) => eventBus.emit("destroy-modal") as unknown as void;
const confirm = async (eventBus: Emitter<Record<EventType, any>>, title?: string, message?: string): Promise<boolean> => {
return new Promise((resolve) => {
eventBus.emit("create-modal", {
options: {
showCloseButton: false,
},
modal: {
title: title ?? "Are you sure?",
body: message ?? "Are you sure you want to continue?",
buttons: [
{
class: "!bg-primary",
text: "Confirm",
onClick: () => {
eventBus.emit("destroy-modal");
resolve(true);
},
},
{
text: "Cancel",
onClick: () => {
eventBus.emit("destroy-modal");
resolve(false);
},
},
],
},
});
});
};

/**
* Vue Plugin
*
* This plugin will add a $modal function to the vue instance
* allowing you to create modals from anywhere in your app
*/
const vuePluginModal = {
install(vue: App) {
const eventBus = mitt();
vue.component("ModalContainer", DefaultModal);
vue.config.globalProperties.$modal = {
eventBus: eventBus,
create: (modal: Partial<ModalOptions>) => create(eventBus, modal),
destroy: () => destroy(eventBus),
confirm: (title?: string, message?: string) => confirm(eventBus, title, message),
};
},
};

export default vuePluginModal;

0 comments on commit b1f61db

Please sign in to comment.