mirror of https://github.com/nocodb/nocodb
24 changed files with 963 additions and 442 deletions
@ -0,0 +1,103 @@
|
||||
import type { DefineComponent, VNode } from '@vue/runtime-dom' |
||||
import { isVNode, render } from '@vue/runtime-dom' |
||||
import type { ComponentPublicInstance } from '@vue/runtime-core' |
||||
import { isClient } from '@vueuse/core' |
||||
import { createEventHook, h, ref, toReactive, tryOnScopeDispose, useNuxtApp, watch } from '#imports' |
||||
|
||||
/** |
||||
* Programmatically create a component and attach it to the body (or a specific mount target), like a dialog or modal. |
||||
* This composable is not SSR friendly - it should be used only on the client. |
||||
* |
||||
* @param componentOrVNode The component to create and attach. Can be a VNode or a component definition. |
||||
* @param props The props to pass to the component. |
||||
* @param mountTarget The target to attach the component to. Defaults to the document body |
||||
* |
||||
* @example |
||||
* import { useDialog } from '#imports' |
||||
* import DlgQuickImport from '~/components/dlg/QuickImport.vue' |
||||
* |
||||
* function openQuickImportDialog(type: string) { |
||||
* // create a ref for showing/hiding the modal
|
||||
* const isOpen = ref(true) |
||||
* |
||||
* const { close, vNode } = useDialog(DlgQuickImport, { |
||||
* 'modelValue': isOpen, |
||||
* 'importType': type, |
||||
* 'onUpdate:modelValue': closeDialog, |
||||
* }) |
||||
* |
||||
* function closeDialog() { |
||||
* // hide the modal
|
||||
* isOpen.value = false |
||||
* |
||||
* // debounce destroying the component, so the modal transition can finish
|
||||
* close(1000) |
||||
* } |
||||
* } |
||||
*/ |
||||
export function useDialog( |
||||
componentOrVNode: DefineComponent<any, any, any> | VNode, |
||||
props: NonNullable<Parameters<typeof h>[1]> = {}, |
||||
mountTarget?: Element | ComponentPublicInstance, |
||||
) { |
||||
if (typeof document === 'undefined' || !isClient) { |
||||
console.warn('[useDialog]: Cannot use outside of browser!') |
||||
} |
||||
|
||||
const closeHook = createEventHook<void>() |
||||
const mountedHook = createEventHook<void>() |
||||
|
||||
const isMounted = $ref(false) |
||||
|
||||
const domNode = document.createElement('div') |
||||
|
||||
const vNodeRef = ref<VNode>() |
||||
|
||||
mountTarget = mountTarget ? ('$el' in mountTarget ? (mountTarget.$el as HTMLElement) : mountTarget) : document.body |
||||
|
||||
/** if specified, append vnode to mount target instead of document.body */ |
||||
mountTarget.appendChild(domNode) |
||||
|
||||
/** When props change, we want to re-render the element with the new prop values */ |
||||
const stop = watch( |
||||
toReactive(props), |
||||
(reactiveProps) => { |
||||
const vNode = isVNode(componentOrVNode) ? componentOrVNode : h(componentOrVNode, reactiveProps) |
||||
|
||||
vNode.appContext = useNuxtApp().vueApp._context |
||||
|
||||
vNodeRef.value = vNode |
||||
|
||||
render(vNode, domNode) |
||||
|
||||
if (!isMounted) mountedHook.trigger() |
||||
}, |
||||
{ deep: true, immediate: true, flush: 'post' }, |
||||
) |
||||
|
||||
/** When calling scope is disposed, destroy component */ |
||||
tryOnScopeDispose(close) |
||||
|
||||
/** destroy component, can be debounced */ |
||||
function close(debounce = 0) { |
||||
setTimeout(() => { |
||||
stop() |
||||
|
||||
render(null, domNode) |
||||
|
||||
setTimeout(() => { |
||||
;(mountTarget as HTMLElement)!.removeChild(domNode) |
||||
}, 100) |
||||
|
||||
closeHook.trigger() |
||||
}, debounce) |
||||
} |
||||
|
||||
return { |
||||
close, |
||||
onClose: closeHook.on, |
||||
onMounted: mountedHook.on, |
||||
domNode, |
||||
vNode: vNodeRef, |
||||
} |
||||
} |
@ -1,3 +1,132 @@
|
||||
<script lang="ts" setup> |
||||
import type { UploadChangeParam, UploadFile } from 'ant-design-vue' |
||||
import { message } from 'ant-design-vue' |
||||
import { ref, useDialog, useDropZone, useFileDialog, useNuxtApp, watch } from '#imports' |
||||
import DlgQuickImport from '~/components/dlg/QuickImport.vue' |
||||
|
||||
const dropZone = ref<HTMLDivElement>() |
||||
|
||||
const { isOverDropZone } = useDropZone(dropZone, onDrop) |
||||
|
||||
const { files, open, reset } = useFileDialog() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
type QuickImportTypes = 'excel' | 'json' | 'csv' |
||||
|
||||
const allowedQuickImportTypes = [ |
||||
// Excel |
||||
'.xls, .xlsx, .xlsm, .ods, .ots', |
||||
|
||||
// CSV |
||||
'.csv', |
||||
|
||||
// JSON |
||||
'.json', |
||||
] |
||||
|
||||
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles), { flush: 'post' }) |
||||
|
||||
function onFileSelect(fileList: FileList | null) { |
||||
if (!fileList) return |
||||
|
||||
const files = Array.from(fileList).map((file) => file) |
||||
|
||||
onDrop(files) |
||||
} |
||||
|
||||
function onDrop(droppedFiles: File[] | null) { |
||||
if (!droppedFiles) return |
||||
|
||||
/** we can only handle one file per drop */ |
||||
if (droppedFiles.length > 1) { |
||||
return message.error({ |
||||
content: `Only one file can be imported at a time.`, |
||||
duration: 2, |
||||
}) |
||||
} |
||||
|
||||
let fileType: QuickImportTypes | null = null |
||||
const isValid = allowedQuickImportTypes.some((type) => { |
||||
const isAllowed = droppedFiles[0].type.replace('/', '.').endsWith(type) |
||||
|
||||
if (isAllowed) { |
||||
fileType = type.replace('.', '') as QuickImportTypes |
||||
} |
||||
|
||||
return isAllowed |
||||
}) |
||||
|
||||
/** Invalid file type was dropped */ |
||||
if (!isValid) { |
||||
return message.error({ |
||||
content: 'Invalid file type', |
||||
duration: 2, |
||||
}) |
||||
} |
||||
|
||||
if (fileType && isValid) { |
||||
openQuickImportDialog(fileType, droppedFiles[0]) |
||||
} |
||||
} |
||||
|
||||
function openQuickImportDialog(type: QuickImportTypes, file: File) { |
||||
$e(`a:actions:import-${type}`) |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close, vNode } = useDialog(DlgQuickImport, { |
||||
'modelValue': isOpen, |
||||
'importType': type, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
vNode.value?.component?.exposed?.handleChange({ |
||||
file: { |
||||
uid: `${type}-${file.name}-${Math.random().toString(36).substring(2)}`, |
||||
name: file.name, |
||||
type: file.type, |
||||
status: 'done', |
||||
fileName: file.name, |
||||
lastModified: file.lastModified, |
||||
size: file.size, |
||||
originFileObj: file, |
||||
}, |
||||
event: { percent: 100 }, |
||||
} as UploadChangeParam<UploadFile<File>>) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
|
||||
reset() |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="h-full w-full prose text-3xl text-gray-400 flex items-center justify-center">Welcome to NocoDB!</div> |
||||
<div ref="dropZone" class="h-full w-full text-gray-600 flex items-center justify-center relative"> |
||||
<general-overlay |
||||
:model-value="true" |
||||
:class="[isOverDropZone ? 'bg-gray-300/75 border-primary shadow' : 'bg-gray-100/25 border-gray-500 cursor-pointer']" |
||||
inline |
||||
style="top: 20%; left: 20%; right: 20%; bottom: 20%" |
||||
class="text-3xl flex items-center justify-center gap-2 border-1 border-dashed rounded hover:border-primary" |
||||
@click="open" |
||||
> |
||||
<template v-if="isOverDropZone"> <MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here </template> |
||||
</general-overlay> |
||||
|
||||
<div class="flex flex-col gap-6 items-center justify-center md:w-1/2 mx-auto text-center"> |
||||
<div class="text-3xl">Welcome to NocoDB!</div> |
||||
|
||||
<div class="flex items-center flex-wrap justify-center gap-2 prose-lg leading-8"> |
||||
To get started, either drop a <span class="flex items-center gap-2"><PhFileCsv /> CSV</span>, |
||||
<span class="flex items-center gap-2"><BiFiletypeJson /> JSON</span> or |
||||
<span class="flex items-center gap-2"><BiFiletypeXlsx /> Excel</span> file here or click the button in the top-left of |
||||
this page. |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
@ -1,6 +1,12 @@
|
||||
import { Menu as AntMenu } from 'ant-design-vue' |
||||
import { defineNuxtPlugin } from '#imports' |
||||
import { Menu as AntMenu, ConfigProvider } from 'ant-design-vue' |
||||
import { defineNuxtPlugin, themeColors } from '#imports' |
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => { |
||||
ConfigProvider.config({ |
||||
theme: { |
||||
primaryColor: themeColors.primary, |
||||
}, |
||||
}) |
||||
|
||||
nuxtApp.vueApp.component(AntMenu.name, AntMenu) |
||||
}) |
||||
|
Loading…
Reference in new issue