mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
34 changed files with 1062 additions and 486 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> |
<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> |
</template> |
||||||
|
@ -1,6 +1,12 @@ |
|||||||
import { Menu as AntMenu } from 'ant-design-vue' |
import { Menu as AntMenu, ConfigProvider } from 'ant-design-vue' |
||||||
import { defineNuxtPlugin } from '#imports' |
import { defineNuxtPlugin, themeColors } from '#imports' |
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => { |
export default defineNuxtPlugin((nuxtApp) => { |
||||||
|
ConfigProvider.config({ |
||||||
|
theme: { |
||||||
|
primaryColor: themeColors.primary, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
nuxtApp.vueApp.component(AntMenu.name, AntMenu) |
nuxtApp.vueApp.component(AntMenu.name, AntMenu) |
||||||
}) |
}) |
||||||
|
Loading…
Reference in new issue