Browse Source

Merge pull request #2885 from nocodb/fix/gui-v2-quick-import

fix(gui-v2): quick import
pull/2961/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
75683fcd7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  2. 29
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  3. 42
      packages/nc-gui-v2/components/dlg/QuickImport.vue
  4. 54
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  5. 1
      packages/nc-gui-v2/components/dlg/TableRename.vue
  6. 28
      packages/nc-gui-v2/components/template/Editor.vue
  7. 66
      packages/nc-gui-v2/composables/useTableCreate.ts
  8. 10
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

2
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -239,7 +239,7 @@ const addTableTab = (table: TableType) => {
</div>
<SettingsModal :show="settingsDlg" @closed="settingsDlg = false" />
<DlgTableCreate v-model="tableCreateDlg" />
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>
</template>

29
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -2,6 +2,7 @@
import io from 'socket.io-client'
import type { Socket } from 'socket.io-client'
import { Form } from 'ant-design-vue'
import type { Card as AntCard } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { fieldRequiredValidator } from '~/utils/validation'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
@ -14,18 +15,30 @@ interface Props {
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
// TODO: handle baseURL
const baseURL = 'http://localhost:8080' // this.$axios.defaults.baseURL
const { $state } = useNuxtApp()
const toast = useToast()
const { sqlUi, project, loadTables } = useProject()
const loading = ref(false)
const showGoToDashboardButton = ref(false)
const step = ref(1)
const progress = ref<Record<string, any>[]>([])
const logRef = ref<typeof AntCard>()
let socket: Socket | null
const syncSource = ref({
id: '',
type: 'Airtable',
@ -64,6 +77,7 @@ const dialogShow = computed({
})
const useForm = Form.useForm
const { resetFields, validate, validateInfos } = useForm(syncSource, validators)
const disableImportButton = computed(() => {
@ -194,7 +208,14 @@ onMounted(async () => {
socket.on('progress', async (d: Record<string, any>) => {
progress.value.push(d)
// FIXME: this doesn't work
nextTick(() => {
;(logRef.value?.$el as HTMLDivElement).scrollTo()
})
if (d.status === 'COMPLETED') {
showGoToDashboardButton.value = true
await loadTables()
// TODO: add tab of the first table
}
@ -210,7 +231,7 @@ onBeforeUnmount(() => {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" @keydown.esc="dialogShow = false">
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" :mask-closable="false" @keydown.esc="dialogShow = false">
<template #footer>
<div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -291,7 +312,7 @@ onBeforeUnmount(() => {
</div>
<div v-if="step === 2">
<div class="mb-4 prose-xl font-bold">Logs</div>
<a-card body-style="background-color: #000000; height:400px; overflow: auto;">
<a-card ref="logRef" body-style="background-color: #000000; height:400px; overflow: auto;">
<div v-for="({ msg, status }, i) in progress" :key="i">
<div v-if="status === 'FAILED'" class="flex items-center">
<MdiCloseCircleOutlineIcon class="text-red-500" />
@ -314,6 +335,10 @@ onBeforeUnmount(() => {
<span class="text-green-500 ml-2"> Importing</span>
</div>
</a-card>
<div class="flex justify-center items-center">
<a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false">Go to Dashboard</a-button
>
</div>
</div>
</div>
</a-modal>

42
packages/nc-gui-v2/components/dlg/QuickImport.vue

@ -18,7 +18,7 @@ interface Props {
importType: 'csv' | 'json' | 'excel'
}
const { modelValue, importType } = defineProps<Props>()
const { importType, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -97,14 +97,7 @@ const importMeta = computed(() => {
return {}
})
const dialogShow = computed({
get() {
return modelValue
},
set(v) {
emit('update:modelValue', v)
},
})
const dialogShow = useVModel(rest, 'modelValue', emit)
const disablePreImportButton = computed(() => {
if (activeKey.value === 'uploadTab') {
@ -123,6 +116,13 @@ const disableImportButton = computed(() => {
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
const modalWidth = computed(() => {
if (importType === 'excel' && templateEditorModal.value) {
return 'max(90vw, 600px)'
}
return 'max(60vw, 600px)'
})
async function handlePreImport() {
loading.value = true
if (activeKey.value === 'uploadTab') {
@ -141,9 +141,14 @@ async function handlePreImport() {
}
async function handleImport() {
loading.value = true
await templateEditorRef.value.importTemplate()
loading.value = false
try {
loading.value = true
await templateEditorRef.value.importTemplate()
} catch (e: any) {
return toast.error(await extractSdkResponseErrorMsg(e))
} finally {
loading.value = false
}
dialogShow.value = false
}
@ -228,12 +233,16 @@ function getAdapter(name: string, val: any) {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(60vw, 600px)" @keydown.esc="dialogShow = false">
<a-modal v-model:visible="dialogShow" :width="modalWidth" :mask-closable="false" @keydown.esc="dialogShow = false">
<a-typography-title class="ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</a-typography-title>
<template #footer>
<a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button>
<a-button v-else key="cancel" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button v-if="activeKey === 'jsonEditorTab'" key="format" :disabled="disableFormatJsonButton" @click="formatJson"
<a-button
v-if="activeKey === 'jsonEditorTab' && !templateEditorModal"
key="format"
:disabled="disableFormatJsonButton"
@click="formatJson"
>Format JSON</a-button
>
<a-button
@ -257,6 +266,7 @@ function getAdapter(name: string, val: any) {
:project-template="templateData"
:import-data="importData"
:quick-import-type="importType"
@import="handleImport"
/>
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" :tab-position="top">
<a-tab-pane key="uploadTab" :closable="false">
@ -289,7 +299,7 @@ function getAdapter(name: string, val: any) {
<template #tab>
<span class="flex items-center gap-2">
<MdiCodeJSONIcon />
Json Editor
JSON Editor
</span>
</template>
<div class="pb-3 pt-3">
@ -300,7 +310,7 @@ function getAdapter(name: string, val: any) {
<template #tab>
<span class="flex items-center gap-2">
<MdiLinkVariantIcon />
Url
URL
</span>
</template>
<div class="pr-10 pt-5">

54
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -1,5 +1,4 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from '@vue/runtime-core'
import { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { onMounted, useProject, useTable, useTabs } from '#imports'
@ -12,17 +11,16 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'create'])
const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const idTypes = [
{ value: 'AI', text: 'Auto increment number' },
{ value: 'AG', text: 'Auto generated string' },
]
const toast = useToast()
const valid = ref(false)
const isIdToggleAllowed = ref(false)
const isAdvanceOptVisible = ref(false)
const { addTab } = useTabs()
@ -52,50 +50,32 @@ const validateDuplicate = (v: string) => {
return (tables?.value || []).every((t) => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name'
}
const inputEl = ref<ComponentPublicInstance>()
const inputEl = ref<HTMLInputElement>()
const useForm = Form.useForm
const formState = reactive({
title: '',
table_name: '',
columns: {
id: true,
title: true,
created_at: true,
updated_at: true,
},
})
const validators = computed(() => {
return {
title: [validateTableName, validateDuplicateAlias],
table_name: [validateTableName],
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const { resetFields, validate, validateInfos } = useForm(table, validators)
onMounted(() => {
generateUniqueTitle()
nextTick(() => {
const el = inputEl.value?.$el
el?.querySelector('input')?.focus()
el?.querySelector('input')?.select()
})
inputEl.value?.focus()
})
</script>
<template>
<a-modal
v-model:visible="dialogShow"
width="max(30vw, 600px)"
@keydown.esc="dialogShow = false"
@keydown.enter="$emit('create', table)"
>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" :mask-closable="false" @keydown.esc="dialogShow = false">
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" @click="createTable">{{ $t('general.submit') }}</a-button>
<a-button key="submit" size="large" type="primary" @click="createTable()">{{ $t('general.submit') }}</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<a-form :model="formState" name="create-new-table-form">
<a-form :model="table" name="create-new-table-form" @keydown.enter="createTable">
<!-- Create A New Table -->
<div class="prose-xl font-bold self-center my-4">{{ $t('activity.createTable') }}</div>
<!-- hint="Enter table name" -->
@ -119,7 +99,7 @@ onMounted(() => {
</div>
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<!-- hint="Table name as saved in database" -->
<div class="mb-2">{{ $t('msg.info.tableNameInDb') }}</div>
<div v-if="!project.prefix" class="mb-2">{{ $t('msg.info.tableNameInDb') }}</div>
<a-form-item v-if="!project.prefix" v-bind="validateInfos.table_name">
<a-input v-model:value="table.table_name" size="large" hide-details :placeholder="$t('msg.info.tableNameInDb')" />
</a-form-item>
@ -134,17 +114,17 @@ onMounted(() => {
<template #title>
<span>ID column is required, you can rename this later if required.</span>
</template>
<a-checkbox v-model:checked="formState.columns.id" disabled>ID</a-checkbox>
<a-checkbox v-model:checked="table.columns.id" disabled>ID</a-checkbox>
</a-tooltip>
</a-col>
<a-col :span="6">
<a-checkbox v-model:checked="formState.columns.title"> title </a-checkbox>
<a-checkbox v-model:checked="table.columns.title"> title </a-checkbox>
</a-col>
<a-col :span="6">
<a-checkbox v-model:checked="formState.columns.created_at"> created_at </a-checkbox>
<a-checkbox v-model:checked="table.columns.created_at"> created_at </a-checkbox>
</a-col>
<a-col :span="6">
<a-checkbox v-model:checked="formState.columns.updated_at"> updated_at </a-checkbox>
<a-checkbox v-model:checked="table.columns.updated_at"> updated_at </a-checkbox>
</a-col>
</a-row>
</div>

1
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -95,6 +95,7 @@ const renameTable = async () => {
<a-modal
v-model:visible="dialogShow"
:title="$t('activity.renameTable')"
:mask-closable="false"
@keydown.esc="dialogShow = false"
@finish="renameTable"
>

28
packages/nc-gui-v2/components/template/Editor.vue

@ -28,6 +28,8 @@ interface Option {
const { quickImportType, projectTemplate, importData } = defineProps<Props>()
const emit = defineEmits(['import'])
const useForm = Form.useForm
const { $api } = useNuxtApp()
@ -73,6 +75,9 @@ const { sqlUi, project, loadTables } = useProject()
onMounted(() => {
parseAndLoadTemplate()
nextTick(() => {
inputRefs.value[0]?.focus()
})
})
const validators = computed(() =>
@ -174,9 +179,8 @@ async function importTemplate() {
try {
await validate()
} catch (errorInfo) {
toast.error('Please fill all the required values')
isImporting.value = false
return
throw new Error('Please fill all the required values')
}
try {
@ -283,7 +287,7 @@ defineExpose({
<template>
<a-spin :spinning="isImporting" :tip="importingTip" size="large">
<a-card>
<a-form :model="data" name="template-editor-form">
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center">
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import
@ -358,16 +362,15 @@ defineExpose({
}
"
v-model:value="record.column_name"
size="large"
/>
</a-form-item>
</template>
<template v-else-if="column.key === 'uidt'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-auto-complete
<a-select
v-model:value="record.uidt"
class="w-52"
size="large"
show-search
:options="uiTypeOptions"
:filter-option="filterOption"
/>
@ -376,7 +379,7 @@ defineExpose({
<template v-else-if="column.key === 'dtxp'">
<a-form-item v-if="isSelect(record)">
<a-input v-model:value="record.dtxp" size="large" />
<a-input v-model:value="record.dtxp" />
</a-form-item>
</template>
@ -386,9 +389,9 @@ defineExpose({
<!-- TODO: i18n -->
<span>Primary Value</span>
</template>
<span class="mr-3">
<div class="flex items-center float-right mr-4">
<MdiKeyStarIcon class="text-lg" />
</span>
</div>
</a-tooltip>
<a-tooltip v-else>
<template #title>
@ -448,7 +451,7 @@ defineExpose({
<!-- TODO: i18n -->
<span>Add Other Column</span>
</template>
<a-button @click="addNewColumnRow(table)">
<a-button @click="addNewColumnRow(table, 'SingleLineText')">
<div class="flex items-center">
<MdiPlusIcon class="text-lg" />
Column
@ -473,7 +476,10 @@ defineExpose({
@apply bg-white;
}
:deep(.template-form-row) > td {
@apply !pb-0;
@apply pa-0 mb-0;
.ant-form-item {
@apply mb-0;
}
}
}
</style>

66
packages/nc-gui-v2/composables/useTableCreate.ts

@ -0,0 +1,66 @@
import type { TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useProject } from './useProject'
import { useNuxtApp } from '#app'
import { useToast } from 'vue-toastification'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
export function useTableCreate(onTableCreate?: (tableMeta: TableType) => void) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '',
table_name: '',
columns: {
id: true,
title: true,
created_at: true,
updated_at: true,
},
})
const { sqlUi, project, tables } = useProject()
const toast = useToast()
const { $api } = useNuxtApp()
const createTable = async () => {
try {
if (!sqlUi?.value) return
const columns = sqlUi?.value?.getNewTableColumns().filter((col) => {
if (col.column_name === 'id' && table.columns.id_ag) {
Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG'))
col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt)
col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt)
return true
}
return !!table.columns[col.column_name]
})
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
...table,
columns,
})
onTableCreate?.(tableMeta)
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
}
watch(
() => table.title,
(title) => {
table.table_name = `${project?.value?.prefix || ''}${title}`
},
)
const generateUniqueTitle = () => {
let c = 1
while (tables?.value?.some((t) => t.title === `Sheet${c}`)) {
c++
}
table.title = `Sheet${c}`
}
return { table, createTable, generateUniqueTitle, tables, project }
}

10
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

@ -152,4 +152,14 @@ function openQuickImportDialog(type: string) {
:deep(.ant-tabs-nav) {
@apply !mb-0;
}
:deep(.ant-menu-item-selected) {
@apply text-inherit !bg-inherit;
}
:deep(.ant-menu-horizontal),
:deep(.ant-menu-item::after),
:deep(.ant-menu-submenu::after) {
@apply !border-none;
}
</style>

Loading…
Cancel
Save