feat: Add file uploads to composer

This commit is contained in:
Jesse Wierzbinski 2024-12-01 17:20:21 +01:00
parent 294740c97f
commit 027847aa03
No known key found for this signature in database
27 changed files with 718 additions and 31 deletions

View file

@ -1,25 +1,34 @@
import {
confirmModalService,
confirmModalWithInputService,
} from "./service.ts";
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
export type ConfirmModalOptions = {
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
inputType?: "none" | "text" | "textarea";
defaultValue?: string;
};
export function useConfirmModal() {
const confirm = (
options: ConfirmModalOptions,
): Promise<ConfirmModalResult> => {
return confirmModalService.confirm(options);
};
export type ConfirmModalResult = {
confirmed: boolean;
value?: string;
};
const confirmWithInput = (
options: ConfirmModalOptions,
placeholder?: string,
): Promise<ConfirmModalResult> => {
return confirmModalWithInputService.confirm(options, placeholder);
};
class ConfirmModalService {
private modalRef = ref<{
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
} | null>(null);
return {
confirm,
confirmWithInput,
};
register(modal: {
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
}) {
this.modalRef.value = modal;
}
confirm(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
if (!this.modalRef.value) {
throw new Error("Confirmation modal not initialized");
}
return this.modalRef.value.open(options);
}
}
export const confirmModalService = new ConfirmModalService();

View file

@ -0,0 +1,69 @@
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { ConfirmModalOptions, ConfirmModalResult } from "./composable.ts";
defineProps<{
modalOptions: ConfirmModalOptions;
}>();
defineEmits<{
confirm: (result: ConfirmModalResult) => void;
cancel: () => void;
}>();
const inputValue = ref<string>("");
</script>
<template>
<Dialog>
<DialogTrigger :as-child="true">
<slot />
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{{ modalOptions.title }}</DialogTitle>
<DialogDescription>
{{ modalOptions.message }}
</DialogDescription>
</DialogHeader>
<div v-if="modalOptions.inputType === 'text'" class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="confirmInput" class="text-right">Value</Label>
<Input id="confirmInput" v-model="inputValue" class="col-span-3" />
</div>
</div>
<div v-else-if="modalOptions.inputType === 'textarea'" class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="confirmTextarea" class="text-right">Value</Label>
<Textarea id="confirmTextarea" v-model="inputValue" class="col-span-3" />
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="() => $emit('cancel')">
{{ modalOptions.cancelText }}
</Button>
<Button @click="() => $emit('confirm', {
confirmed: true,
value: inputValue,
})">
{{ modalOptions.confirmText }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,102 @@
<script setup lang="ts">
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ref } from "vue";
import {
type ConfirmModalOptions,
type ConfirmModalResult,
confirmModalService,
} from "./composable.ts";
const isOpen = ref(false);
const modalOptions = ref<ConfirmModalOptions>({
title: "Confirm",
message: "",
inputType: "none",
confirmText: "Confirm",
cancelText: "Cancel",
});
const inputValue = ref("");
const resolvePromise = ref<((result: ConfirmModalResult) => void) | null>(null);
function open(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
isOpen.value = true;
modalOptions.value = {
title: options.title || "Confirm",
message: options.message,
inputType: options.inputType || "none",
confirmText: options.confirmText || "Confirm",
cancelText: options.cancelText || "Cancel",
};
inputValue.value = options.defaultValue || "";
return new Promise((resolve) => {
resolvePromise.value = resolve;
});
}
function handleConfirm() {
if (resolvePromise.value) {
resolvePromise.value({
confirmed: true,
value: inputValue.value,
});
}
isOpen.value = false;
}
function handleCancel() {
if (resolvePromise.value) {
resolvePromise.value({
confirmed: false,
});
}
isOpen.value = false;
}
confirmModalService.register({
open,
});
</script>
<template>
<AlertDialog :key="String(isOpen)" :open="isOpen" @update:open="isOpen = false">
<AlertDialogContent class="sm:max-w-[425px] flex flex-col">
<AlertDialogHeader>
<AlertDialogTitle>{{ modalOptions.title }}</AlertDialogTitle>
<AlertDialogDescription v-if="modalOptions.message">
{{ modalOptions.message }}
</AlertDialogDescription>
</AlertDialogHeader>
<Input v-if="modalOptions.inputType === 'text'" v-model="inputValue" />
<Textarea v-else-if="modalOptions.inputType === 'textarea'" v-model="inputValue" rows="6" />
<AlertDialogFooter class="w-full">
<AlertDialogCancel :as-child="true">
<Button variant="outline" @click="handleCancel">
{{ modalOptions.cancelText }}
</Button>
</AlertDialogCancel>
<AlertDialogAction :as-child="true">
<Button @click="handleConfirm">
{{ modalOptions.confirmText }}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View file

@ -1,124 +0,0 @@
<template>
<HeadlessTransitionRoot as="template" :show="isOpen">
<Dialog.Root :open="isOpen" @update:open="handleOpenChange" :close-on-escape="true"
:close-on-interact-outside="true">
<Teleport to="body">
<Dialog.Positioner class="fixed inset-0 z-50 flex items-end md:items-center justify-center md:p-4">
<HeadlessTransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0"
enter-to="opacity-100" leave="ease-in duration-300" leave-from="opacity-100"
leave-to="opacity-0">
<Dialog.Backdrop class="fixed inset-0 bg-black/70 backdrop-blur-sm" />
</HeadlessTransitionChild>
<HeadlessTransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100" leave="ease-in duration-300" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95">
<Dialog.Content class="relative w-full md:max-w-md p-6 rounded bg-dark-800 ring-1 ring-white/10 shadow-xl">
<Dialog.Title class="mb-4 text-lg font-bold tracking-tight text-gray-100 sm:text-xl">
{{ modalOptions.title || 'Confirm Action' }}
</Dialog.Title>
<div class="mb-6 text-gray-300">
{{ modalOptions.message }}
</div>
<div v-if="withInput" class="mb-4">
<input v-model="inputValue" type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
:placeholder="inputPlaceholder" />
</div>
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-3 *:!py-2">
<Button @click="handleCancel"
theme="outline">
{{ modalOptions.cancelText || 'Cancel' }}
</button>
<Button @click="handleConfirm"
theme="primary">
{{ modalOptions.confirmText || 'Confirm' }}
</button>
</div>
</Dialog.Content>
</HeadlessTransitionChild>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</HeadlessTransitionRoot>
</template>
<script setup lang="ts">
import { Dialog } from "@ark-ui/vue";
import Button from "~/packages/ui/components/buttons/button.vue";
import {
confirmModalService,
confirmModalWithInputService,
} from "./service.ts";
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
const isOpen = ref(false);
const modalOptions = ref<ConfirmModalOptions>({ message: "" });
const resolvePromise = ref<((result: ConfirmModalResult) => void) | null>(null);
const inputValue = ref("");
const withInput = ref(false);
const inputPlaceholder = ref("");
const open = async (
options: ConfirmModalOptions,
): Promise<ConfirmModalResult> => {
modalOptions.value = options;
isOpen.value = true;
withInput.value = false;
inputValue.value = "";
return new Promise((resolve) => {
resolvePromise.value = resolve;
});
};
const openWithInput = async (
options: ConfirmModalOptions,
placeholder = "Enter value",
): Promise<ConfirmModalResult> => {
modalOptions.value = options;
isOpen.value = true;
withInput.value = true;
inputValue.value = "";
inputPlaceholder.value = placeholder;
return new Promise((resolve) => {
resolvePromise.value = resolve;
});
};
const handleConfirm = () => {
if (resolvePromise.value) {
resolvePromise.value({
confirmed: true,
value: withInput.value ? inputValue.value : undefined,
});
isOpen.value = false;
}
};
const handleCancel = () => {
if (resolvePromise.value) {
resolvePromise.value({ confirmed: false });
isOpen.value = false;
}
};
const handleOpenChange = (open: boolean) => {
if (!open && resolvePromise.value) {
resolvePromise.value({ confirmed: false });
isOpen.value = false;
}
};
// Register the component with the service
confirmModalService.register({
open,
});
confirmModalWithInputService.register({
open: openWithInput,
});
</script>

View file

@ -1,52 +0,0 @@
import { ref } from "vue";
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
class ConfirmModalService {
private modalRef = ref<{
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
} | null>(null);
register(modal: {
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
}) {
this.modalRef.value = modal;
}
confirm(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
if (!this.modalRef.value) {
throw new Error("Confirmation modal not initialized");
}
return this.modalRef.value.open(options);
}
}
class ConfirmModalWithInputService {
private modalRef = ref<{
open: (
options: ConfirmModalOptions,
placeholder?: string,
) => Promise<ConfirmModalResult>;
} | null>(null);
register(modal: {
open: (
options: ConfirmModalOptions,
placeholder?: string,
) => Promise<ConfirmModalResult>;
}) {
this.modalRef.value = modal;
}
confirm(
options: ConfirmModalOptions,
placeholder?: string,
): Promise<ConfirmModalResult> {
if (!this.modalRef.value) {
throw new Error("Confirmation modal not initialized");
}
return this.modalRef.value.open(options, placeholder);
}
}
export const confirmModalService = new ConfirmModalService();
export const confirmModalWithInputService = new ConfirmModalWithInputService();

View file

@ -1,11 +0,0 @@
export interface ConfirmModalOptions {
title?: string;
message: string;
confirmText?: string;
cancelText?: string;
}
export interface ConfirmModalResult {
confirmed: boolean;
value?: string;
}