-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add asssertConfirm and move global modal confirm code out of HoustonA…
…ppContainer.vue
- Loading branch information
1 parent
e1fdf18
commit 638e070
Showing
4 changed files
with
173 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 118 additions & 78 deletions
196
houston-common-ui/lib/components/modals/ModalConfirm.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,170 @@ | ||
<script lang="ts"> | ||
export type ConfirmOptions = { | ||
header: string; | ||
body: string; | ||
dangerous?: boolean; | ||
header: string; | ||
body: string; | ||
dangerous?: boolean; | ||
}; | ||
</script> | ||
|
||
<script setup lang="ts"> | ||
import { defineProps, defineEmits, computed, ref, watchEffect, defineExpose } from "vue"; | ||
import { ResultAsync, okAsync } from 'neverthrow'; | ||
import { | ||
defineProps, | ||
defineEmits, | ||
computed, | ||
ref, | ||
watchEffect, | ||
defineExpose, | ||
} from "vue"; | ||
import { ResultAsync, okAsync, errAsync } from "neverthrow"; | ||
import { type Action } from "@/composables/wrapActions"; | ||
import CardContainer from '@/components/CardContainer.vue'; | ||
import Modal from './Modal.vue'; | ||
import CardContainer from "@/components/CardContainer.vue"; | ||
import Modal from "./Modal.vue"; | ||
import { ExclamationCircleIcon } from "@heroicons/vue/20/solid"; | ||
const _ = cockpit.gettext; | ||
defineProps<{ | ||
clickOutsideCancels?: boolean; | ||
clickOutsideCancels?: boolean; | ||
}>(); | ||
const emit = defineEmits<{ | ||
(e: "confirm"): void; | ||
(e: "cancel"): void; | ||
(e: "confirm"): void; | ||
(e: "cancel"): void; | ||
}>(); | ||
class Confirmation { | ||
id: symbol; | ||
promise: Promise<boolean> | ||
resolve!: (value: boolean | PromiseLike<boolean>) => void; | ||
constructor(public header: string, public body: string, public dangerous: boolean) { | ||
this.promise = new Promise((r) => { | ||
this.resolve = r; | ||
}); | ||
this.id = Symbol(); | ||
} | ||
id: symbol; | ||
promise: Promise<boolean>; | ||
resolve!: (value: boolean | PromiseLike<boolean>) => void; | ||
constructor( | ||
public header: string, | ||
public body: string, | ||
public dangerous: boolean | ||
) { | ||
this.promise = new Promise((r) => { | ||
this.resolve = r; | ||
}); | ||
this.id = Symbol(); | ||
} | ||
} | ||
const confirmationStack = ref<Confirmation[]>([]); | ||
const pushConfirmation = (c: Confirmation) => { | ||
confirmationStack.value = [c, ...confirmationStack.value]; | ||
confirmationStack.value = [c, ...confirmationStack.value]; | ||
}; | ||
const popConfirmation = () => { | ||
confirmationStack.value = confirmationStack.value.slice(1); | ||
confirmationStack.value = confirmationStack.value.slice(1); | ||
}; | ||
const currentConfirmation = computed(() => confirmationStack.value.length > 0 ? confirmationStack.value[0] : undefined); | ||
const currentConfirmation = computed(() => | ||
confirmationStack.value.length > 0 ? confirmationStack.value[0] : undefined | ||
); | ||
const resolveCurrent = (value: boolean) => { | ||
if (currentConfirmation.value === undefined) { | ||
return; | ||
} | ||
currentConfirmation.value.resolve(value); | ||
popConfirmation(); | ||
if (currentConfirmation.value === undefined) { | ||
return; | ||
} | ||
currentConfirmation.value.resolve(value); | ||
popConfirmation(); | ||
}; | ||
// stage the modal contents to avoid it going blank on leave | ||
const headerText = ref<string>(""); | ||
const bodyText = ref<string>(""); | ||
const isDangerous = ref<boolean>(false); | ||
watchEffect(() => { | ||
if (currentConfirmation.value === undefined) { | ||
return; | ||
} | ||
headerText.value = currentConfirmation.value.header; | ||
bodyText.value = currentConfirmation.value.body; | ||
isDangerous.value = currentConfirmation.value.dangerous; | ||
if (currentConfirmation.value === undefined) { | ||
return; | ||
} | ||
headerText.value = currentConfirmation.value.header; | ||
bodyText.value = currentConfirmation.value.body; | ||
isDangerous.value = currentConfirmation.value.dangerous; | ||
}); | ||
const confirm = (options: ConfirmOptions): ResultAsync<boolean, never> => { | ||
const confirmation = new Confirmation(options.header, options.body, options.dangerous ?? false); | ||
pushConfirmation(confirmation); | ||
return ResultAsync.fromSafePromise(confirmation.promise); | ||
const confirmation = new Confirmation( | ||
options.header, | ||
options.body, | ||
options.dangerous ?? false | ||
); | ||
pushConfirmation(confirmation); | ||
return ResultAsync.fromSafePromise(confirmation.promise); | ||
}; | ||
const confirmBeforeAction = ( | ||
options: ConfirmOptions, | ||
action: Action<any, any, any> | ||
options: ConfirmOptions, | ||
action: Action<any, any, any> | ||
): typeof action => { | ||
return (...args: Parameters<typeof action>): ReturnType<typeof action> | ResultAsync<null, never> => { | ||
return confirm(options).andThen((confirmed) => { | ||
if (!confirmed) { | ||
return okAsync(null); | ||
} | ||
return action(...args); | ||
}); | ||
}; | ||
return ( | ||
...args: Parameters<typeof action> | ||
): ReturnType<typeof action> | ResultAsync<null, never> => { | ||
return confirm(options).andThen((confirmed) => { | ||
if (!confirmed) { | ||
return okAsync(null); | ||
} | ||
return action(...args); | ||
}); | ||
}; | ||
}; | ||
type ValueElseUndefiend<T> = T extends | ||
| string | ||
| number | ||
| boolean | ||
| symbol | ||
| object | ||
? T | ||
: undefined; | ||
const assertConfirm = <T,>( | ||
options: ConfirmOptions, | ||
resultIfConfirmed?: T | ||
): ResultAsync<ValueElseUndefiend<T>, Error> => { | ||
return confirm(options).andThen((confirmed) => | ||
confirmed | ||
? okAsync(resultIfConfirmed as any) | ||
: errAsync(new Error("Cancelled by user.")) | ||
); | ||
}; | ||
defineExpose({ | ||
confirm, | ||
confirmBeforeAction, | ||
confirm, | ||
confirmBeforeAction, | ||
assertConfirm, | ||
}); | ||
</script> | ||
|
||
<template> | ||
<Modal :show="currentConfirmation !== undefined"> | ||
<CardContainer class="sm:min-w-96"> | ||
<template #header> | ||
{{ headerText }} | ||
</template> | ||
<div class="flex flex-row items-center gap-2"> | ||
<ExclamationCircleIcon | ||
v-if="isDangerous" | ||
class="size-icon-xl icon-danger shrink-0" | ||
/> | ||
<div class="grow overflow-x-auto whitespace-pre"> | ||
{{ bodyText }} | ||
</div> | ||
</div> | ||
<template #footer> | ||
<div class="button-group-row justify-end grow"> | ||
<button | ||
class="btn btn-secondary" | ||
@click="resolveCurrent(false)" | ||
>{{ _("Cancel") }}</button> | ||
<button | ||
class="btn" | ||
:class="isDangerous ? 'btn-danger' : 'btn-primary'" | ||
@click="resolveCurrent(true)" | ||
>{{ _("Confirm") }}</button> | ||
</div> | ||
</template> | ||
</CardContainer> | ||
</Modal> | ||
<Modal :show="currentConfirmation !== undefined"> | ||
<CardContainer class="sm:min-w-96"> | ||
<template #header> | ||
{{ headerText }} | ||
</template> | ||
<div class="flex flex-row items-center gap-2"> | ||
<ExclamationCircleIcon | ||
v-if="isDangerous" | ||
class="size-icon-xl icon-danger shrink-0" | ||
/> | ||
<div class="grow overflow-x-auto whitespace-pre"> | ||
{{ bodyText }} | ||
</div> | ||
</div> | ||
<template #footer> | ||
<div class="button-group-row justify-end grow"> | ||
<button class="btn btn-secondary" @click="resolveCurrent(false)"> | ||
{{ _("Cancel") }} | ||
</button> | ||
<button | ||
class="btn" | ||
:class="isDangerous ? 'btn-danger' : 'btn-primary'" | ||
@click="resolveCurrent(true)" | ||
> | ||
{{ _("Confirm") }} | ||
</button> | ||
</div> | ||
</template> | ||
</CardContainer> | ||
</Modal> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { type ConfirmOptions } from "@/components/modals"; | ||
import { ResultAsync } from "neverthrow"; | ||
import { type Action } from "@/composables/wrapActions"; | ||
import { ref } from "vue"; | ||
|
||
export type GlobalModalConfirmFunctions = { | ||
confirm: (options: ConfirmOptions) => ResultAsync<boolean, never>; | ||
confirmBeforeAction: ( | ||
options: ConfirmOptions, | ||
action: Action<any, any, any> | ||
) => typeof action; | ||
assertConfirm: ( | ||
options: ConfirmOptions, | ||
resultIfConfirmed?: unknown | ||
) => ResultAsync<typeof resultIfConfirmed, Error>; | ||
}; | ||
|
||
const globalModalConfirmFuncs = ref<GlobalModalConfirmFunctions>(); | ||
|
||
export namespace _internal { | ||
export const provideGlobalModalFuncs = ( | ||
funcs: GlobalModalConfirmFunctions | ||
) => { | ||
globalModalConfirmFuncs.value = funcs; | ||
}; | ||
} | ||
|
||
const getGlobalModalConfirmFuncs = () => { | ||
if (globalModalConfirmFuncs.value === undefined) { | ||
throw new Error("Global ModalConfirm methods not provided!"); | ||
} | ||
return globalModalConfirmFuncs.value; | ||
}; | ||
|
||
export const confirm = ( | ||
...args: Parameters<GlobalModalConfirmFunctions["confirm"]> | ||
): ReturnType<GlobalModalConfirmFunctions["confirm"]> => | ||
getGlobalModalConfirmFuncs().confirm(...args); | ||
export const confirmBeforeAction = ( | ||
...args: Parameters<GlobalModalConfirmFunctions["confirmBeforeAction"]> | ||
): ReturnType<GlobalModalConfirmFunctions["confirmBeforeAction"]> => | ||
getGlobalModalConfirmFuncs().confirmBeforeAction(...args); | ||
export const assertConfirm = ( | ||
...args: Parameters<GlobalModalConfirmFunctions["assertConfirm"]> | ||
): ReturnType<GlobalModalConfirmFunctions["assertConfirm"]> => | ||
getGlobalModalConfirmFuncs().assertConfirm(...args); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters