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> </div>
<SettingsModal :show="settingsDlg" @closed="settingsDlg = false" /> <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" /> <DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div> </div>
</template> </template>

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save