mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
feat: Dialog.vue
- shift showToast to interactive.ts
This commit is contained in:
parent
1e8b1152bb
commit
903ee3e158
@ -102,7 +102,6 @@ export default defineComponent({
|
||||
WindowsTitleBar,
|
||||
},
|
||||
async mounted() {
|
||||
// setGlobalShortcuts(this.shortcuts as Shortcuts);
|
||||
this.setInitialScreen();
|
||||
},
|
||||
watch: {
|
||||
|
@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showToast } from 'src/utils/ui';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
emits: ['item-selected'],
|
||||
|
157
src/components/Dialog.vue
Normal file
157
src/components/Dialog.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition>
|
||||
<!-- Backdrop -->
|
||||
<div class="backdrop z-20 flex justify-center items-center" v-if="open">
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
border
|
||||
rounded-lg
|
||||
text-gray-900
|
||||
p-4
|
||||
shadow-2xl
|
||||
w-dialog
|
||||
flex flex-col
|
||||
gap-4
|
||||
inner
|
||||
"
|
||||
>
|
||||
<h1 class="font-semibold">{{ title }}</h1>
|
||||
<p v-if="description" class="text-base">{{ description }}</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
v-for="(b, index) of buttons"
|
||||
:ref="b.isPrimary ? 'primary' : 'secondary'"
|
||||
:key="b.label"
|
||||
class="w-20"
|
||||
:type="b.isPrimary ? 'primary' : 'secondary'"
|
||||
@click="() => handleClick(index)"
|
||||
>
|
||||
{{ b.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { DialogButton } from 'src/utils/types';
|
||||
import { defineComponent, nextTick, PropType, ref } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
primary: ref<InstanceType<typeof Button>[] | null>(null),
|
||||
secondary: ref<InstanceType<typeof Button>[] | null>(null),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { open: false };
|
||||
},
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
buttons: {
|
||||
type: Array as PropType<DialogButton[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
open(value) {
|
||||
if (value) {
|
||||
document.addEventListener('keydown', this.handleEscape);
|
||||
} else {
|
||||
document.removeEventListener('keydown', this.handleEscape);
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await nextTick(() => {
|
||||
this.open = true;
|
||||
});
|
||||
|
||||
this.focusButton();
|
||||
},
|
||||
unmounted() {
|
||||
console.log('unmounted');
|
||||
},
|
||||
deactivated() {
|
||||
console.log('deactivated');
|
||||
},
|
||||
methods: {
|
||||
focusButton() {
|
||||
let button = this.primary?.[0];
|
||||
if (!button) {
|
||||
button = this.secondary?.[0];
|
||||
}
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.$el.focus();
|
||||
},
|
||||
handleEscape(event: KeyboardEvent) {
|
||||
if (event.code !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.buttons.length === 1) {
|
||||
return this.handleClick(0);
|
||||
}
|
||||
|
||||
const index = this.buttons.findIndex(
|
||||
({ isPrimary, isEscape }) =>
|
||||
isEscape || (this.buttons.length === 2 && !isPrimary)
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.handleClick(index);
|
||||
},
|
||||
handleClick(index: number) {
|
||||
const button = this.buttons[index];
|
||||
button.handler();
|
||||
this.open = false;
|
||||
},
|
||||
},
|
||||
components: { Button },
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: all 100ms ease-out;
|
||||
}
|
||||
|
||||
.inner {
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.v-enter-from .inner,
|
||||
.v-leave-to .inner {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.v-enter-to .inner,
|
||||
.v-leave-from .inner {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
</style>
|
@ -1,22 +1,7 @@
|
||||
<template>
|
||||
<Transition>
|
||||
<div
|
||||
class="
|
||||
fixed
|
||||
top-0
|
||||
start-0
|
||||
w-screen
|
||||
h-screen
|
||||
z-20
|
||||
flex
|
||||
justify-center
|
||||
items-center
|
||||
"
|
||||
:style="
|
||||
useBackdrop
|
||||
? 'background: rgba(0, 0, 0, 0.1); backdrop-filter: blur(2px)'
|
||||
: ''
|
||||
"
|
||||
class="backdrop z-20 flex justify-center items-center"
|
||||
@click="$emit('closemodal')"
|
||||
v-if="openModal"
|
||||
>
|
||||
@ -37,17 +22,14 @@ import { defineComponent, inject } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return { shortcuts: inject(shortcutsKey) };
|
||||
const context = `Modal-` + Math.random().toString(36).slice(2, 6);
|
||||
return { shortcuts: inject(shortcutsKey), context };
|
||||
},
|
||||
props: {
|
||||
openModal: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
useBackdrop: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['closemodal'],
|
||||
watch: {
|
||||
@ -61,11 +43,6 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
context(): string {
|
||||
return `Modal-` + Math.random().toString(36).slice(2, 6);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
@ -102,6 +102,10 @@ export default defineComponent({
|
||||
t`A submittable entry is deleted only if it is in the cancelled state.`,
|
||||
].join(' '),
|
||||
},
|
||||
{
|
||||
shortcut: [ShortcutKey.pmod, 'P'],
|
||||
description: t`Open Print View if Print is available.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ import { fyo } from './initFyo';
|
||||
import router from './router';
|
||||
import { getErrorMessage, stringifyCircular } from './utils';
|
||||
import { MessageDialogOptions, ToastOptions } from './utils/types';
|
||||
import { showMessageDialog, showToast } from './utils/ui';
|
||||
import { showMessageDialog } from './utils/ui';
|
||||
|
||||
function shouldNotStore(error: Error) {
|
||||
const shouldLog = (error as BaseError).shouldStore ?? true;
|
||||
@ -90,6 +90,7 @@ export async function handleError(
|
||||
|
||||
await sendError(errorLogObj);
|
||||
const toastProps = getToastProps(errorLogObj);
|
||||
const { showToast } = await import('src/utils/interactive');
|
||||
await showToast(toastProps);
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ import { reloadWindow } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { UIGroupedFields } from 'src/utils/types';
|
||||
import { showToast } from 'src/utils/ui';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import CommonFormSection from '../CommonForm/CommonFormSection.vue';
|
||||
|
||||
|
@ -220,6 +220,7 @@ import PageHeader from 'src/components/PageHeader.vue';
|
||||
import ShortcutKeys from 'src/components/ShortcutKeys.vue';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { shortcutsKey } from 'src/utils/injectionKeys';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { getSavePath } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
@ -237,7 +238,6 @@ import {
|
||||
selectTextFile,
|
||||
ShortcutKey,
|
||||
showMessageDialog,
|
||||
showToast,
|
||||
} from 'src/utils/ui';
|
||||
import { useDocShortcuts } from 'src/utils/vueUtils';
|
||||
import { getMapFromList } from 'utils/index';
|
||||
|
@ -76,10 +76,24 @@ input[type='number']::-webkit-inner-spin-button {
|
||||
--h-app: 800px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.w-form {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.w-dialog {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.w-quick-edit {
|
||||
width: var(--w-quick-edit);
|
||||
flex-shrink: 0;
|
||||
|
88
src/utils/interactive.ts
Normal file
88
src/utils/interactive.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import Dialog from 'src/components/Dialog.vue';
|
||||
import Toast from 'src/components/Toast.vue';
|
||||
import { t } from 'fyo';
|
||||
import { App, createApp, h } from 'vue';
|
||||
import { DialogButton, DialogOptions, ToastOptions } from './types';
|
||||
|
||||
type DialogReturn<DO extends DialogOptions> = DO['buttons'] extends {
|
||||
handler: () => Promise<infer O> | infer O;
|
||||
}[]
|
||||
? O
|
||||
: void;
|
||||
|
||||
export async function showDialog<DO extends DialogOptions>(options: DO) {
|
||||
const { title, description } = options;
|
||||
|
||||
const preWrappedButtons: DialogButton[] = options.buttons ?? [
|
||||
{ label: t`Okay`, handler: () => {} },
|
||||
];
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const buttons = preWrappedButtons!.map(({ label, handler, isPrimary }) => {
|
||||
return {
|
||||
label,
|
||||
handler: async () => {
|
||||
resolve(await handler());
|
||||
},
|
||||
isPrimary,
|
||||
};
|
||||
});
|
||||
|
||||
const dialogApp = createApp({
|
||||
render() {
|
||||
return h(Dialog, { title, description, buttons });
|
||||
},
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// @ts-ignore
|
||||
dialogApp.mount(fragment);
|
||||
document.body.append(fragment);
|
||||
}) as DialogReturn<DO>;
|
||||
}
|
||||
|
||||
export async function showToast(options: ToastOptions) {
|
||||
const toastApp = createApp({
|
||||
render() {
|
||||
return h(Toast, { ...options });
|
||||
},
|
||||
});
|
||||
|
||||
replaceAndAppendMount(toastApp, 'toast-target');
|
||||
}
|
||||
|
||||
function replaceAndAppendMount(app: App<Element>, replaceId: string) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const target = document.getElementById(replaceId);
|
||||
if (target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = target.parentElement;
|
||||
const clone = target.cloneNode();
|
||||
|
||||
// @ts-ignore
|
||||
app.mount(fragment);
|
||||
target.replaceWith(fragment);
|
||||
parent!.append(clone);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.st = () => showToast({ message: 'peace' });
|
||||
|
||||
// @ts-ignore
|
||||
window.sd = async function () {
|
||||
const options = {
|
||||
title: 'Do This?',
|
||||
description: 'Give me confirmation, should I do this?',
|
||||
buttons: [
|
||||
{ label: 'Yes', handler: () => 'do it', isPrimary: true },
|
||||
{ label: 'No', handler: () => 'dont do it' },
|
||||
],
|
||||
};
|
||||
|
||||
const ret = await showDialog(options);
|
||||
console.log(ret);
|
||||
return ret;
|
||||
};
|
@ -7,8 +7,9 @@ import { BaseError } from 'fyo/utils/errors';
|
||||
import { BackendResponse } from 'utils/ipc/types';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types';
|
||||
import { showToast } from './interactive';
|
||||
import { setLanguageMap } from './language';
|
||||
import { showMessageDialog, showToast } from './ui';
|
||||
import { showMessageDialog } from './ui';
|
||||
|
||||
export function reloadWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
|
@ -5,7 +5,6 @@ import { fyo } from 'src/initFyo';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { reloadWindow } from './ipcCalls';
|
||||
import { systemLanguageRef } from './refs';
|
||||
import { showToast } from './ui';
|
||||
|
||||
// Language: Language Code in books/translations
|
||||
export const languageCodeMap: Record<string, string> = {
|
||||
@ -72,6 +71,7 @@ async function fetchAndSetLanguageMap(code: string) {
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
const { showToast } = await import('src/utils/interactive');
|
||||
showToast({ type: 'error', message });
|
||||
} else {
|
||||
setLanguageMapOnTranslationString(languageMap);
|
||||
|
@ -98,3 +98,16 @@ export type PrintValues = {
|
||||
print: Record<string, unknown>;
|
||||
doc: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export interface DialogOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
buttons?: DialogButton[];
|
||||
}
|
||||
|
||||
export type DialogButton = {
|
||||
label: string;
|
||||
handler: () => any;
|
||||
isPrimary?: boolean;
|
||||
isEscape?: boolean;
|
||||
};
|
||||
|
@ -18,10 +18,10 @@ import { fyo } from 'src/initFyo';
|
||||
import router from 'src/router';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { SelectFileOptions } from 'utils/types';
|
||||
import { App, createApp, h } from 'vue';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import { stringifyCircular } from './';
|
||||
import { evaluateHidden } from './doc';
|
||||
import { showToast } from './interactive';
|
||||
import { selectFile } from './ipcCalls';
|
||||
import { showSidebar } from './refs';
|
||||
import {
|
||||
@ -123,32 +123,6 @@ export async function showMessageDialog({
|
||||
return await button.action();
|
||||
}
|
||||
|
||||
export async function showToast(options: ToastOptions) {
|
||||
const Toast = (await import('src/components/Toast.vue')).default;
|
||||
const toast = createApp({
|
||||
render() {
|
||||
return h(Toast, { ...options });
|
||||
},
|
||||
});
|
||||
replaceAndAppendMount(toast, 'toast-target');
|
||||
}
|
||||
|
||||
function replaceAndAppendMount(app: App<Element>, replaceId: string) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const target = document.getElementById(replaceId);
|
||||
if (target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = target.parentElement;
|
||||
const clone = target.cloneNode();
|
||||
|
||||
// @ts-ignore
|
||||
app.mount(fragment);
|
||||
target.replaceWith(fragment);
|
||||
parent!.append(clone);
|
||||
}
|
||||
|
||||
export async function openSettings(tab: SettingsTab) {
|
||||
await routeTo({ path: '/settings', query: { tab } });
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user