replaced vue3-promise-dialog for internal component
This commit is contained in:
@@ -95,3 +95,24 @@ code {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SM Dialog */
|
||||
.sm-dialog-outer {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sm-dialog-outer:last-of-type {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
114
resources/js/components/SMDialog.ts
Normal file
114
resources/js/components/SMDialog.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
AllowedComponentProps,
|
||||
Component,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
VNodeProps,
|
||||
} from "vue";
|
||||
|
||||
export interface DialogInstance {
|
||||
comp?: any;
|
||||
dialog: Component;
|
||||
wrapper: string;
|
||||
props: unknown;
|
||||
resolve: (data: unknown) => void;
|
||||
}
|
||||
const dialogRefs = shallowReactive<DialogInstance[]>([]);
|
||||
|
||||
export default defineComponent({
|
||||
name: "SMDialogList",
|
||||
template: `
|
||||
<div class="sm-dialog-list">
|
||||
<div v-for="(dialogRef, index) in dialogRefList" :key="index" class="sm-dialog-outer">
|
||||
<component
|
||||
:is="dialogRef.dialog"
|
||||
v-if="dialogRef && dialogRef.wrapper === name"
|
||||
v-bind="dialogRef.props"
|
||||
:ref="(ref) => (dialogRef.comp = ref)"></component>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
const dialogRefList = dialogRefs;
|
||||
|
||||
return {
|
||||
name: "default",
|
||||
transitionAttrs: {},
|
||||
dialogRefList,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Closes last opened dialog, resolving the promise with the return value of the dialog, or with the given
|
||||
* data if any.
|
||||
*
|
||||
* @param {unknown} data The dialog return value.
|
||||
*/
|
||||
export function closeDialog(data?: unknown) {
|
||||
if (dialogRefs.length <= 1) {
|
||||
document.getElementsByTagName("html")[0].style.overflow = "";
|
||||
document.getElementsByTagName("body")[0].style.overflow = "";
|
||||
}
|
||||
|
||||
const lastDialog = dialogRefs.pop();
|
||||
if (data === undefined) {
|
||||
data = lastDialog.comp.returnValue();
|
||||
}
|
||||
lastDialog.resolve(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the type of props from a component definition.
|
||||
*/
|
||||
type PropsType<C extends Component> = C extends new (...args: any) => any
|
||||
? Omit<
|
||||
InstanceType<C>["$props"],
|
||||
keyof VNodeProps | keyof AllowedComponentProps
|
||||
>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts the return type of the dialog from the setup function.
|
||||
*/
|
||||
type BindingReturnType<C extends Component> = C extends new (
|
||||
...args: any
|
||||
) => any
|
||||
? InstanceType<C> extends { returnValue: () => infer Y }
|
||||
? Y
|
||||
: never
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts the return type of the dialog either from the setup method or from the methods.
|
||||
*/
|
||||
type ReturnType<C extends Component> = BindingReturnType<C>;
|
||||
|
||||
/**
|
||||
* Opens a dialog.
|
||||
*
|
||||
* @param {Component} dialog The dialog you want to open.
|
||||
* @param {PropsType} props The props to be passed to the dialog.
|
||||
* @param {string} wrapper The dialog wrapper you want the dialog to open into.
|
||||
* @returns {Promise} A promise that resolves when the dialog is closed
|
||||
*/
|
||||
export function openDialog<C extends Component>(
|
||||
dialog: C,
|
||||
props?: PropsType<C>,
|
||||
wrapper: string = "default"
|
||||
): Promise<ReturnType<C>> {
|
||||
if (dialogRefs.length === 0) {
|
||||
document.getElementsByTagName("html")[0].style.overflow = "hidden";
|
||||
document.getElementsByTagName("body")[0].style.overflow = "hidden";
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
dialogRefs.push({
|
||||
dialog,
|
||||
props,
|
||||
wrapper,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, useSlots, watch } from "vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import { api } from "../helpers/api";
|
||||
import { MediaResponse } from "../helpers/api.types";
|
||||
import { imageMedium } from "../helpers/image";
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, watch } from "vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../components/SMDialog";
|
||||
import { api } from "../helpers/api";
|
||||
import { Media, MediaResponse } from "../helpers/api.types";
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div class="sm-modal">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
|
||||
onMounted(() => {
|
||||
document.getElementsByTagName("body")[0].style.overflow = "hidden";
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.getElementsByTagName("body")[0].style.overflow = "auto";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-modal {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,27 @@
|
||||
<template>
|
||||
<SMModal>
|
||||
<SMFormCard :loading="dialogLoading">
|
||||
<h1>Change Password</h1>
|
||||
<p class="text-center">Enter your new password below</p>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMInput
|
||||
control="password"
|
||||
type="password"
|
||||
label="New Password" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="secondary"
|
||||
label="Cancel"
|
||||
@click="handleClickCancel" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton type="submit" label="Update" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
</SMFormCard>
|
||||
</SMModal>
|
||||
<SMFormCard :loading="dialogLoading">
|
||||
<h1>Change Password</h1>
|
||||
<p class="text-center">Enter your new password below</p>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMInput control="password" type="password" label="New Password" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="secondary"
|
||||
label="Cancel"
|
||||
@click="handleClickCancel" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton type="submit" label="Update" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
</SMFormCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl, FormObject } from "../../helpers/form";
|
||||
import { And, Password, Required } from "../../helpers/validate";
|
||||
@@ -38,7 +33,6 @@ import SMFormCard from "../SMFormCard.vue";
|
||||
import SMForm from "../SMForm.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMInput from "../SMInput.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
|
||||
const form: FormObject = reactive(
|
||||
Form({
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<SMModal>
|
||||
<SMFormCard>
|
||||
<h1>{{ props.title }}</h1>
|
||||
<p v-html="computedSanitizedText"></p>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
:type="props.cancel.type"
|
||||
:label="props.cancel.label"
|
||||
@click="handleClickCancel()" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
:type="props.confirm.type"
|
||||
:label="props.confirm.label"
|
||||
@click="handleClickConfirm()" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMFormCard>
|
||||
</SMModal>
|
||||
<SMFormCard>
|
||||
<h1>{{ props.title }}</h1>
|
||||
<p v-html="computedSanitizedText"></p>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
:type="props.cancel.type"
|
||||
:label="props.cancel.label"
|
||||
@click="handleClickCancel()" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
:type="props.confirm.type"
|
||||
:label="props.confirm.label"
|
||||
@click="handleClickConfirm()" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMFormCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { computed, onMounted, onUnmounted } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { useApplicationStore } from "../../store/ApplicationStore";
|
||||
import SMButton from "../SMButton.vue";
|
||||
import SMFormCard from "../SMFormCard.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
|
||||
@@ -1,108 +1,106 @@
|
||||
<template>
|
||||
<SMModal>
|
||||
<SMFormCard
|
||||
:loading="dialogLoading"
|
||||
full
|
||||
:loading-message="dialogLoadingMessage"
|
||||
class="sm-dialog-media">
|
||||
<h1>Insert Media</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage"
|
||||
icon="alert-circle-outline"
|
||||
type="error"
|
||||
:message="formMessage"
|
||||
class="d-flex" />
|
||||
<div class="media-browser" :class="mediaBrowserClasses">
|
||||
<div class="media-browser-content">
|
||||
<SMLoadingIcon v-if="mediaLoading" />
|
||||
<div
|
||||
v-if="!mediaLoading && mediaItems.length == 0"
|
||||
class="media-none">
|
||||
<ion-icon name="sad-outline"></ion-icon>
|
||||
<p>No media found</p>
|
||||
</div>
|
||||
<ul v-if="!mediaLoading && mediaItems.length > 0">
|
||||
<li
|
||||
v-for="item in mediaItems"
|
||||
:key="item.id"
|
||||
:class="[{ selected: item.id == selected }]"
|
||||
@click="handleClickItem(item.id)"
|
||||
@dblclick="handleDblClickItem(item.id)">
|
||||
<div
|
||||
:style="{
|
||||
backgroundImage: `url('${getFilePreview(
|
||||
item.url
|
||||
)}')`,
|
||||
}"
|
||||
class="media-image"></div>
|
||||
<span class="media-title">{{ item.title }}</span>
|
||||
<span class="media-size">{{
|
||||
bytesReadable(item.size)
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<SMFormCard
|
||||
:loading="dialogLoading"
|
||||
full
|
||||
:loading-message="dialogLoadingMessage"
|
||||
class="sm-dialog-media">
|
||||
<h1>Insert Media</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage"
|
||||
icon="alert-circle-outline"
|
||||
type="error"
|
||||
:message="formMessage"
|
||||
class="d-flex" />
|
||||
<div class="media-browser" :class="mediaBrowserClasses">
|
||||
<div class="media-browser-content">
|
||||
<SMLoadingIcon v-if="mediaLoading" />
|
||||
<div
|
||||
v-if="!mediaLoading && mediaItems.length == 0"
|
||||
class="media-none">
|
||||
<ion-icon name="sad-outline"></ion-icon>
|
||||
<p>No media found</p>
|
||||
</div>
|
||||
<div class="media-browser-toolbar">
|
||||
<div class="layout-buttons">
|
||||
<ion-icon
|
||||
name="grid-outline"
|
||||
class="layout-button-grid"
|
||||
@click="handleClickGridLayout"></ion-icon>
|
||||
<ion-icon
|
||||
name="list-outline"
|
||||
class="layout-button-list"
|
||||
@click="handleClickListLayout"></ion-icon>
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
<ion-icon
|
||||
name="chevron-back-outline"
|
||||
:class="[{ disabled: computedDisablePrevButton }]"
|
||||
@click="handleClickPrev" />
|
||||
<span class="pagination-info">{{
|
||||
computedPaginationInfo
|
||||
<ul v-if="!mediaLoading && mediaItems.length > 0">
|
||||
<li
|
||||
v-for="item in mediaItems"
|
||||
:key="item.id"
|
||||
:class="[{ selected: item.id == selected }]"
|
||||
@click="handleClickItem(item.id)"
|
||||
@dblclick="handleDblClickItem(item.id)">
|
||||
<div
|
||||
:style="{
|
||||
backgroundImage: `url('${getFilePreview(
|
||||
item.url
|
||||
)}')`,
|
||||
}"
|
||||
class="media-image"></div>
|
||||
<span class="media-title">{{ item.title }}</span>
|
||||
<span class="media-size">{{
|
||||
bytesReadable(item.size)
|
||||
}}</span>
|
||||
<ion-icon
|
||||
name="chevron-forward-outline"
|
||||
:class="[{ disabled: computedDisableNextButton }]"
|
||||
@click="handleClickNext" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="media-browser-toolbar">
|
||||
<div class="layout-buttons">
|
||||
<ion-icon
|
||||
name="grid-outline"
|
||||
class="layout-button-grid"
|
||||
@click="handleClickGridLayout"></ion-icon>
|
||||
<ion-icon
|
||||
name="list-outline"
|
||||
class="layout-button-list"
|
||||
@click="handleClickListLayout"></ion-icon>
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
<ion-icon
|
||||
name="chevron-back-outline"
|
||||
:class="[{ disabled: computedDisablePrevButton }]"
|
||||
@click="handleClickPrev" />
|
||||
<span class="pagination-info">{{
|
||||
computedPaginationInfo
|
||||
}}</span>
|
||||
<ion-icon
|
||||
name="chevron-forward-outline"
|
||||
:class="[{ disabled: computedDisableNextButton }]"
|
||||
@click="handleClickNext" />
|
||||
</div>
|
||||
</div>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
@click="handleClickCancel" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
v-if="props.allowUpload"
|
||||
type="button"
|
||||
label="Upload"
|
||||
@click="handleClickUpload" />
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Insert"
|
||||
:disabled="selected.length == 0"
|
||||
@click="handleClickInsert" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
<input
|
||||
v-if="props.allowUpload"
|
||||
id="file"
|
||||
ref="refUploadInput"
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="computedAccepts"
|
||||
@change="handleChangeUpload" />
|
||||
</SMFormCard>
|
||||
</SMModal>
|
||||
</div>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
@click="handleClickCancel" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
v-if="props.allowUpload"
|
||||
type="button"
|
||||
label="Upload"
|
||||
@click="handleClickUpload" />
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Insert"
|
||||
:disabled="selected.length == 0"
|
||||
@click="handleClickInsert" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
<input
|
||||
v-if="props.allowUpload"
|
||||
id="file"
|
||||
ref="refUploadInput"
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="computedAccepts"
|
||||
@change="handleChangeUpload" />
|
||||
</SMFormCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, Ref, watch } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { api } from "../../helpers/api";
|
||||
import { Media, MediaCollection, MediaResponse } from "../../helpers/api.types";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
@@ -113,7 +111,6 @@ import SMFormCard from "../SMFormCard.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMLoadingIcon from "../SMLoadingIcon.vue";
|
||||
import SMMessage from "../SMMessage.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
mime: {
|
||||
@@ -356,7 +353,7 @@ const handleClickNext = ($event: MouseEvent): void => {
|
||||
/**
|
||||
* When the user clicks the upload button
|
||||
*/
|
||||
const handleClickUpload = () => {
|
||||
const handleClickUpload = async () => {
|
||||
if (refUploadInput.value != null) {
|
||||
refUploadInput.value.click();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import { createApp } from "vue";
|
||||
import { VueReCaptcha } from "vue-recaptcha-v3";
|
||||
import { PromiseDialog } from "vue3-promise-dialog";
|
||||
import "../css/app.scss";
|
||||
import "./bootstrap";
|
||||
import SMColumn from "./components/SMColumn.vue";
|
||||
@@ -20,7 +19,6 @@ pinia.use(piniaPluginPersistedstate);
|
||||
createApp(App)
|
||||
.use(pinia)
|
||||
.use(Router)
|
||||
.use(PromiseDialog)
|
||||
.use(VueReCaptcha, {
|
||||
siteKey: import.meta.env.GOOGLE_RECAPTCHA_SITE_KEY,
|
||||
loaderOptions: {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<SMProgress />
|
||||
<SMToastList />
|
||||
<DialogWrapper :transition-attrs="{ name: 'fade' }" />
|
||||
<SMDialogList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -18,7 +19,7 @@ import SMNavbar from "../components/SMNavbar.vue";
|
||||
import SMFooter from "../components/SMFooter.vue";
|
||||
import SMProgress from "../components/SMProgress.vue";
|
||||
import SMToastList from "../components/SMToastList.vue";
|
||||
import { DialogWrapper } from "vue3-promise-dialog";
|
||||
import SMDialogList from "../components/SMDialog";
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMFileLink from "../../components/SMFileLink.vue";
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
Reference in New Issue
Block a user