2
0
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:
18alantom 2023-03-24 13:02:22 +05:30 committed by Alan
parent 1e8b1152bb
commit 903ee3e158
14 changed files with 288 additions and 60 deletions

View File

@ -102,7 +102,6 @@ export default defineComponent({
WindowsTitleBar,
},
async mounted() {
// setGlobalShortcuts(this.shortcuts as Shortcuts);
this.setInitialScreen();
},
watch: {

View File

@ -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
View 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>

View File

@ -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>

View File

@ -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.`,
},
],
},
{

View File

@ -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);
}

View File

@ -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';

View File

@ -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';

View File

@ -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
View 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;
};

View File

@ -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);

View File

@ -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);

View File

@ -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;
};

View File

@ -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 } });
}