mirror of https://github.com/nocodb/nocodb
19 changed files with 30000 additions and 16736 deletions
@ -0,0 +1,311 @@
|
||||
<script setup lang="ts"> |
||||
import io from 'socket.io-client' |
||||
import type { Socket } from 'socket.io-client' |
||||
import { Form } from 'ant-design-vue' |
||||
import { useToast } from 'vue-toastification' |
||||
import { fieldRequiredValidator } from '~/utils/validation' |
||||
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' |
||||
import MdiCloseCircleOutlineIcon from '~icons/mdi/close-circle-outline' |
||||
import MdiCurrencyUsdIcon from '~icons/mdi/currency-usd' |
||||
import MdiLoadingIcon from '~icons/mdi/loading' |
||||
|
||||
interface Props { |
||||
modelValue: boolean |
||||
} |
||||
|
||||
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 step = ref(1) |
||||
const progress = ref<Record<string, any>[]>([]) |
||||
let socket: Socket | null |
||||
const syncSource = ref({ |
||||
id: '', |
||||
type: 'Airtable', |
||||
details: { |
||||
syncInterval: '15mins', |
||||
syncDirection: 'Airtable to NocoDB', |
||||
syncRetryCount: 1, |
||||
apiKey: '', |
||||
shareId: '', |
||||
syncSourceUrlOrId: '', |
||||
options: { |
||||
syncViews: true, |
||||
syncData: true, |
||||
syncRollup: false, |
||||
syncLookup: true, |
||||
syncFormula: false, |
||||
syncAttachment: true, |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
const validators = computed(() => { |
||||
return { |
||||
'details.apiKey': [fieldRequiredValidator], |
||||
'details.syncSourceUrlOrId': [fieldRequiredValidator], |
||||
} |
||||
}) |
||||
|
||||
const dialogShow = computed({ |
||||
get() { |
||||
return modelValue |
||||
}, |
||||
set(v) { |
||||
emit('update:modelValue', v) |
||||
}, |
||||
}) |
||||
|
||||
const useForm = Form.useForm |
||||
const { resetFields, validate, validateInfos } = useForm(syncSource, validators) |
||||
|
||||
const disableImportButton = computed(() => { |
||||
return !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId |
||||
}) |
||||
|
||||
async function saveAndSync() { |
||||
await createOrUpdate() |
||||
await sync() |
||||
} |
||||
|
||||
async function createOrUpdate() { |
||||
try { |
||||
const { id, ...payload } = syncSource.value |
||||
if (id !== '') { |
||||
await $fetch(`/api/v1/db/meta/syncs/${id}`, { |
||||
baseURL, |
||||
method: 'PATCH', |
||||
headers: { 'xc-auth': $state.token.value as string }, |
||||
body: payload, |
||||
}) |
||||
} else { |
||||
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, { |
||||
baseURL, |
||||
method: 'POST', |
||||
headers: { 'xc-auth': $state.token.value as string }, |
||||
body: payload, |
||||
}) |
||||
syncSource.value = data |
||||
} |
||||
} catch (e: any) { |
||||
toast.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
async function loadSyncSrc() { |
||||
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, { |
||||
baseURL, |
||||
method: 'GET', |
||||
headers: { 'xc-auth': $state.token.value as string }, |
||||
}) |
||||
const { list: srcs } = data |
||||
if (srcs && srcs[0]) { |
||||
srcs[0].details = srcs[0].details || {} |
||||
syncSource.value = migrateSync(srcs[0]) |
||||
syncSource.value.details.syncSourceUrlOrId = srcs[0].details.shareId |
||||
} else { |
||||
syncSource.value = { |
||||
id: '', |
||||
type: 'Airtable', |
||||
details: { |
||||
syncInterval: '15mins', |
||||
syncDirection: 'Airtable to NocoDB', |
||||
syncRetryCount: 1, |
||||
apiKey: '', |
||||
shareId: '', |
||||
syncSourceUrlOrId: '', |
||||
options: { |
||||
syncViews: true, |
||||
syncData: true, |
||||
syncRollup: false, |
||||
syncLookup: true, |
||||
syncFormula: false, |
||||
syncAttachment: true, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function sync() { |
||||
step.value = 2 |
||||
try { |
||||
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, { |
||||
baseURL, |
||||
method: 'POST', |
||||
headers: { 'xc-auth': $state.token.value as string }, |
||||
params: { |
||||
id: socket.id, |
||||
}, |
||||
}) |
||||
} catch (e: any) { |
||||
console.log(e) |
||||
toast.error(e) |
||||
} |
||||
} |
||||
|
||||
function migrateSync(src: any) { |
||||
if (!src.details?.options) { |
||||
src.details.options = { |
||||
syncViews: false, |
||||
syncData: true, |
||||
syncRollup: false, |
||||
syncLookup: true, |
||||
syncFormula: false, |
||||
syncAttachment: true, |
||||
} |
||||
src.details.options.syncViews = src.syncViews |
||||
delete src.syncViews |
||||
} |
||||
return src |
||||
} |
||||
|
||||
watch( |
||||
() => syncSource.value.details.syncSourceUrlOrId, |
||||
(v) => { |
||||
if (syncSource.value.details) { |
||||
const m = v && v.match(/(exp|shr).{14}/g) |
||||
syncSource.value.details.shareId = m ? m[0] : '' |
||||
} |
||||
}, |
||||
) |
||||
|
||||
onMounted(async () => { |
||||
socket = io(new URL(baseURL, window.location.href.split(/[?#]/)[0]).href, { |
||||
extraHeaders: { 'xc-auth': $state.token.value as string }, |
||||
}) |
||||
socket.on('connect_error', () => { |
||||
socket?.disconnect() |
||||
socket = null |
||||
}) |
||||
|
||||
// connect event does not provide data |
||||
socket.on('connect', () => { |
||||
console.log(socket?.id) |
||||
console.log('socket connected') |
||||
}) |
||||
|
||||
socket.on('progress', async (d: Record<string, any>) => { |
||||
progress.value.push(d) |
||||
if (d.status === 'COMPLETED') { |
||||
await loadTables() |
||||
// TODO: add tab of the first table |
||||
} |
||||
}) |
||||
await loadSyncSrc() |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
if (socket) { |
||||
socket.disconnect() |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<a-modal v-model:visible="dialogShow" width="max(90vw, 600px)" @keydown.esc="dialogShow = false"> |
||||
<template #footer> |
||||
<div v-if="step === 1"> |
||||
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> |
||||
<a-button |
||||
key="submit" |
||||
v-t="['c:sync-airtable:save-and-sync']" |
||||
type="primary" |
||||
:disabled="disableImportButton" |
||||
@click="saveAndSync" |
||||
>Import |
||||
</a-button> |
||||
</div> |
||||
</template> |
||||
<a-typography-title class="ml-5 mt-5" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</a-typography-title> |
||||
<div class="ml-5 mr-5"> |
||||
<a-divider /> |
||||
<div v-if="step === 1"> |
||||
<div class="mb-4"> |
||||
<span class="prose-xl font-bold mr-3">Credentials</span> |
||||
<a |
||||
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials" |
||||
class="prose-sm underline text-grey" |
||||
target="_blank" |
||||
>Where to find this? |
||||
</a> |
||||
</div> |
||||
<a-form ref="form" :model="syncSource" name="quick-import-airtable-form" layout="horizontal" class="ma-0"> |
||||
<a-form-item v-bind="validateInfos['details.apiKey']"> |
||||
<a-input-password v-model:value="syncSource.details.apiKey" placeholder="Api Key" size="large" /> |
||||
</a-form-item> |
||||
<a-form-item v-bind="validateInfos['details.syncSourceUrlOrId']"> |
||||
<a-input v-model:value="syncSource.details.syncSourceUrlOrId" placeholder="Shared Base ID / URL" size="large" /> |
||||
</a-form-item> |
||||
<span class="prose-xl font-bold self-center my-4">Advanced Settings</span> |
||||
<a-divider class="mt-2 mb-5" /> |
||||
<div class="mt-0 my-2"> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncData">Import Data</a-checkbox> |
||||
</div> |
||||
<div class="my-2"> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncViews">Import Secondary Views</a-checkbox> |
||||
</div> |
||||
<div class="my-2"> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncRollup">Import Rollup Columns</a-checkbox> |
||||
</div> |
||||
<div class="my-2"> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncLookup">Import Lookup Columns</a-checkbox> |
||||
</div> |
||||
<div class="my-2"> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncAttachment">Import Attachment Columns</a-checkbox> |
||||
</div> |
||||
<a-tooltip placement="top"> |
||||
<template #title> |
||||
<span>Coming Soon!</span> |
||||
</template> |
||||
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>Import Formula Columns</a-checkbox> |
||||
</a-tooltip> |
||||
</a-form> |
||||
<a-divider /> |
||||
<div> |
||||
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions / Help - Reach out here</a> |
||||
<br /> |
||||
<div> |
||||
This feature is currently in beta and more information can be found |
||||
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">here</a>. |
||||
</div> |
||||
</div> |
||||
</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;"> |
||||
<div v-for="({ msg, status }, i) in progress" :key="i"> |
||||
<div v-if="status === 'FAILED'" class="flex items-center"> |
||||
<MdiCloseCircleOutlineIcon class="text-red-500" /> |
||||
<span class="text-red-500 ml-2">{{ msg }}</span> |
||||
</div> |
||||
<div v-else class="flex items-center"> |
||||
<MdiCurrencyUsdIcon class="text-green-500" /> |
||||
<span class="text-green-500 ml-2">{{ msg }}</span> |
||||
</div> |
||||
</div> |
||||
<div |
||||
v-if=" |
||||
!progress || |
||||
!progress.length || |
||||
(progress[progress.length - 1].status !== 'COMPLETED' && progress[progress.length - 1].status !== 'FAILED') |
||||
" |
||||
class="flex items-center" |
||||
> |
||||
<MdiLoadingIcon class="text-green-500 animate-spin" /> |
||||
<span class="text-green-500 ml-2"> Importing</span> |
||||
</div> |
||||
</a-card> |
||||
</div> |
||||
</div> |
||||
</a-modal> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,338 @@
|
||||
<script setup lang="ts"> |
||||
import { useToast } from 'vue-toastification' |
||||
import { Form } from 'ant-design-vue' |
||||
import type { TableType } from 'nocodb-sdk' |
||||
import type { UploadChangeParam } from 'ant-design-vue' |
||||
import { useI18n } from 'vue-i18n' |
||||
import MdiFileIcon from '~icons/mdi/file-plus-outline' |
||||
import MdiFileUploadOutlineIcon from '~icons/mdi/file-upload-outline' |
||||
import MdiLinkVariantIcon from '~icons/mdi/link-variant' |
||||
import MdiCodeJSONIcon from '~icons/mdi/code-json' |
||||
import { fieldRequiredValidator, importCsvUrlValidator, importExcelUrlValidator, importUrlValidator } from '~/utils/validation' |
||||
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' |
||||
import { ExcelTemplateAdapter, ExcelUrlTemplateAdapter, JSONTemplateAdapter, JSONUrlTemplateAdapter } from '~/utils/parsers' |
||||
import { useProject } from '#imports' |
||||
|
||||
interface Props { |
||||
modelValue: boolean |
||||
importType: 'csv' | 'json' | 'excel' |
||||
} |
||||
|
||||
const { modelValue, importType } = defineProps<Props>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { tables } = useProject() |
||||
|
||||
const toast = useToast() |
||||
|
||||
const activeKey = ref('uploadTab') |
||||
|
||||
const jsonEditorRef = ref() |
||||
|
||||
const templateEditorRef = ref() |
||||
|
||||
const loading = ref(false) |
||||
|
||||
const templateData = ref() |
||||
|
||||
const importData = ref() |
||||
|
||||
const templateEditorModal = ref(false) |
||||
|
||||
const useForm = Form.useForm |
||||
|
||||
const importState = reactive({ |
||||
fileList: [] as Record<string, any>, |
||||
url: '', |
||||
jsonEditor: {}, |
||||
parserConfig: { |
||||
maxRowsToParse: 500, |
||||
normalizeNested: true, |
||||
importData: true, |
||||
}, |
||||
}) |
||||
|
||||
const isImportTypeJson = computed(() => importType === 'json') |
||||
|
||||
const isImportTypeCsv = computed(() => importType === 'csv') |
||||
|
||||
const IsImportTypeExcel = computed(() => importType === 'excel') |
||||
|
||||
const validators = computed(() => { |
||||
return { |
||||
url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], |
||||
maxRowsToParse: [fieldRequiredValidator], |
||||
} |
||||
}) |
||||
|
||||
const { resetFields, validate, validateInfos } = useForm(importState, validators) |
||||
|
||||
const importMeta = computed(() => { |
||||
if (IsImportTypeExcel.value) { |
||||
return { |
||||
header: 'QUICK IMPORT - EXCEL', |
||||
uploadHint: t('msg.info.excelSupport'), |
||||
urlInputLabel: t('msg.info.excelURL'), |
||||
loadUrlDirective: ['c:quick-import:excel:load-url'], |
||||
acceptTypes: '.xls, .xlsx, .xlsm, .ods, .ots', |
||||
} |
||||
} else if (isImportTypeCsv.value) { |
||||
return { |
||||
header: 'QUICK IMPORT - CSV', |
||||
uploadHint: '', |
||||
urlInputLabel: t('msg.info.csvURL'), |
||||
loadUrlDirective: ['c:quick-import:csv:load-url'], |
||||
acceptTypes: '.csv', |
||||
} |
||||
} else if (isImportTypeJson.value) { |
||||
return { |
||||
header: 'QUICK IMPORT - JSON', |
||||
uploadHint: '', |
||||
acceptTypes: '.json', |
||||
} |
||||
} |
||||
return {} |
||||
}) |
||||
|
||||
const dialogShow = computed({ |
||||
get() { |
||||
return modelValue |
||||
}, |
||||
set(v) { |
||||
emit('update:modelValue', v) |
||||
}, |
||||
}) |
||||
|
||||
const disablePreImportButton = computed(() => { |
||||
if (activeKey.value === 'uploadTab') { |
||||
return !(importState.fileList.length > 0) |
||||
} else if (activeKey.value === 'urlTab') { |
||||
if (!validateInfos.url.validateStatus) return true |
||||
return validateInfos.url.validateStatus === 'error' |
||||
} else if (activeKey.value === 'jsonEditorTab') { |
||||
return !jsonEditorRef.value?.isValid |
||||
} |
||||
}) |
||||
|
||||
const disableImportButton = computed(() => { |
||||
return !templateEditorRef.value?.isValid |
||||
}) |
||||
|
||||
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid) |
||||
|
||||
async function handlePreImport() { |
||||
loading.value = true |
||||
if (activeKey.value === 'uploadTab') { |
||||
await parseAndExtractData(importState.fileList[0].data, importState.fileList[0].name) |
||||
} else if (activeKey.value === 'urlTab') { |
||||
try { |
||||
await validate() |
||||
await parseAndExtractData(importState.url, '') |
||||
} catch (e: any) { |
||||
toast.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} else if (activeKey.value === 'jsonEditorTab') { |
||||
await parseAndExtractData(JSON.stringify(importState.jsonEditor), '') |
||||
} |
||||
loading.value = false |
||||
} |
||||
|
||||
async function handleImport() { |
||||
loading.value = true |
||||
await templateEditorRef.value.importTemplate() |
||||
loading.value = false |
||||
dialogShow.value = false |
||||
} |
||||
|
||||
async function parseAndExtractData(val: any, name: string) { |
||||
try { |
||||
templateData.value = null |
||||
importData.value = null |
||||
const templateGenerator: any = getAdapter(name, val) |
||||
if (!templateGenerator) { |
||||
toast.error('Template Generator cannot be found!') |
||||
return |
||||
} |
||||
await templateGenerator.init() |
||||
templateGenerator.parse() |
||||
templateData.value = templateGenerator.getTemplate() |
||||
templateData.value.tables[0].table_name = populateUniqueTableName() |
||||
importData.value = templateGenerator.getData() |
||||
templateEditorModal.value = true |
||||
} catch (e: any) { |
||||
console.log(e) |
||||
toast.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
function rejectDrop(fileList: any[]) { |
||||
fileList.map((file) => { |
||||
return toast.error(`Failed to upload file ${file.name}`) |
||||
}) |
||||
} |
||||
|
||||
function handleChange(info: UploadChangeParam) { |
||||
const status = info.file.status |
||||
if (status !== 'uploading') { |
||||
const reader: any = new FileReader() |
||||
reader.onload = (e: any) => { |
||||
const target: any = importState.fileList.find((f: any) => f?.uid === info.file.uid) |
||||
if (target) { |
||||
target.data = e.target.result |
||||
} |
||||
} |
||||
reader.readAsArrayBuffer(info.file.originFileObj) |
||||
} |
||||
if (status === 'done') { |
||||
toast.success(`Uploaded file ${info.file.name} successfully`) |
||||
} else if (status === 'error') { |
||||
toast.error(`Failed to upload file ${info.file.name}`) |
||||
} |
||||
} |
||||
|
||||
function formatJson() { |
||||
jsonEditorRef.value?.format() |
||||
} |
||||
|
||||
function populateUniqueTableName() { |
||||
let c = 1 |
||||
while (tables.value.some((t: TableType) => t.title === `Sheet${c}`)) { |
||||
c++ |
||||
} |
||||
return `Sheet${c}` |
||||
} |
||||
|
||||
function getAdapter(name: string, val: any) { |
||||
if (IsImportTypeExcel.value || isImportTypeCsv.value) { |
||||
switch (activeKey.value) { |
||||
case 'uploadTab': |
||||
return new ExcelTemplateAdapter(name, val, importState.parserConfig) |
||||
case 'urlTab': |
||||
return new ExcelUrlTemplateAdapter(val, importState.parserConfig) |
||||
} |
||||
} else if (isImportTypeJson.value) { |
||||
switch (activeKey.value) { |
||||
case 'uploadTab': |
||||
return new JSONTemplateAdapter(name, val, importState.parserConfig) |
||||
case 'urlTab': |
||||
return new JSONUrlTemplateAdapter(val, importState.parserConfig) |
||||
case 'jsonEditorTab': |
||||
return new JSONTemplateAdapter(name, val, importState.parserConfig) |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-modal v-model:visible="dialogShow" width="max(90vw, 600px)" @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" |
||||
>Format JSON</a-button |
||||
> |
||||
<a-button |
||||
v-if="!templateEditorModal" |
||||
key="pre-import" |
||||
type="primary" |
||||
:loading="loading" |
||||
:disabled="disablePreImportButton" |
||||
@click="handlePreImport" |
||||
>{{ $t('activity.import') }} |
||||
</a-button> |
||||
<a-button v-else key="import" type="primary" :loading="loading" :disabled="disableImportButton" @click="handleImport">{{ |
||||
$t('activity.import') |
||||
}}</a-button> |
||||
</template> |
||||
<div class="ml-5 mr-5"> |
||||
<TemplateEditor |
||||
v-if="templateEditorModal" |
||||
ref="templateEditorRef" |
||||
:project-template="templateData" |
||||
:import-data="importData" |
||||
:quick-import-type="importType" |
||||
/> |
||||
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" :tab-position="top"> |
||||
<a-tab-pane key="uploadTab" :closable="false"> |
||||
<template #tab> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiFileUploadOutlineIcon /> |
||||
Upload |
||||
</span> |
||||
</template> |
||||
<div class="pr-10 pb-0 pt-5"> |
||||
<a-upload-dragger |
||||
v-model:fileList="importState.fileList" |
||||
name="file" |
||||
:accept="importMeta.acceptTypes" |
||||
:max-count="1" |
||||
list-type="picture" |
||||
@change="handleChange" |
||||
@reject="rejectDrop" |
||||
> |
||||
<MdiFileIcon size="large" /> |
||||
<p class="ant-upload-text">Click or drag file to this area to upload</p> |
||||
<p class="ant-upload-hint"> |
||||
{{ importMeta.uploadHint }} |
||||
</p> |
||||
</a-upload-dragger> |
||||
</div> |
||||
</a-tab-pane> |
||||
<a-tab-pane v-if="isImportTypeJson" key="jsonEditorTab" :closable="false"> |
||||
<template #tab> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiCodeJSONIcon /> |
||||
Json Editor |
||||
</span> |
||||
</template> |
||||
<div class="pb-3 pt-3"> |
||||
<MonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" /> |
||||
</div> |
||||
</a-tab-pane> |
||||
<a-tab-pane v-else key="urlTab" :closable="false"> |
||||
<template #tab> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiLinkVariantIcon /> |
||||
Url |
||||
</span> |
||||
</template> |
||||
<div class="pr-10 pt-5"> |
||||
<a-form :model="importState" name="quick-import-url-form" layout="horizontal" class="mb-0"> |
||||
<a-form-item :label="importMeta.urlInputLabel" v-bind="validateInfos.url"> |
||||
<a-input v-model:value="importState.url" size="large" /> |
||||
</a-form-item> |
||||
</a-form> |
||||
</div> |
||||
</a-tab-pane> |
||||
</a-tabs> |
||||
</div> |
||||
<div v-if="!templateEditorModal" class="ml-5 mr-5"> |
||||
<a-divider /> |
||||
<div class="mb-4"> |
||||
<span class="prose-xl font-bold">Advanced Settings</span> |
||||
<a-form-item class="mt-4 mb-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse"> |
||||
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" /> |
||||
</a-form-item> |
||||
<div v-if="isImportTypeJson" class="mt-3"> |
||||
<a-checkbox v-model:checked="importState.parserConfig.normalizeNested"> |
||||
<span class="caption">Flatten nested</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div v-if="isImportTypeJson" class="mt-4"> |
||||
<a-checkbox v-model:checked="importState.parserConfig.importData">Import data</a-checkbox> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</a-modal> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
:deep(.ant-upload-list) { |
||||
@apply max-h-80 overflow-auto; |
||||
} |
||||
</style> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,42 @@
|
||||
import Papaparse from 'papaparse' |
||||
import TemplateGenerator from './TemplateGenerator' |
||||
|
||||
export default class CSVTemplateAdapter extends TemplateGenerator { |
||||
fileName: string |
||||
project: object |
||||
data: object |
||||
csv: any |
||||
csvData: any |
||||
columns: object |
||||
|
||||
constructor(name: string, data: object) { |
||||
super() |
||||
this.fileName = name |
||||
this.csvData = data |
||||
this.project = { |
||||
title: this.fileName, |
||||
tables: [], |
||||
} |
||||
this.data = {} |
||||
this.csv = {} |
||||
this.columns = {} |
||||
this.csvData = {} |
||||
} |
||||
|
||||
async init() { |
||||
this.csv = Papaparse.parse(this.csvData, { header: true }) |
||||
} |
||||
|
||||
parseData() { |
||||
this.columns = this.csv.meta.fields |
||||
this.data = this.csv.data |
||||
} |
||||
|
||||
getColumns() { |
||||
return this.columns |
||||
} |
||||
|
||||
getData() { |
||||
return this.data |
||||
} |
||||
} |
@ -0,0 +1,240 @@
|
||||
import XLSX from 'xlsx' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
import TemplateGenerator from './TemplateGenerator' |
||||
import { getCheckboxValue, isCheckboxType } from './parserHelpers' |
||||
|
||||
const excelTypeToUidt: Record<any, any> = { |
||||
d: UITypes.DateTime, |
||||
b: UITypes.Checkbox, |
||||
n: UITypes.Number, |
||||
s: UITypes.SingleLineText, |
||||
} |
||||
|
||||
export default class ExcelTemplateAdapter extends TemplateGenerator { |
||||
config: Record<string, any> |
||||
name: string |
||||
excelData: any |
||||
project: Record<string, any> |
||||
data: Record<string, any> |
||||
wb: any |
||||
constructor(name = '', data = {}, parserConfig = {}) { |
||||
super() |
||||
this.config = { |
||||
maxRowsToParse: 500, |
||||
...parserConfig, |
||||
} |
||||
this.name = name |
||||
this.excelData = data |
||||
this.project = { |
||||
title: this.name, |
||||
tables: [], |
||||
} |
||||
this.data = {} |
||||
} |
||||
|
||||
async init() { |
||||
const options: Record<any, boolean> = { |
||||
cellText: true, |
||||
cellDates: true, |
||||
} |
||||
if (this.name.slice(-3) === 'csv') { |
||||
this.wb = XLSX.read(new TextDecoder().decode(new Uint8Array(this.excelData)), { |
||||
type: 'string', |
||||
...options, |
||||
}) |
||||
} else { |
||||
this.wb = XLSX.read(new Uint8Array(this.excelData), { |
||||
type: 'array', |
||||
...options, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
parse() { |
||||
const tableNamePrefixRef: any = {} |
||||
for (let i = 0; i < this.wb.SheetNames.length; i++) { |
||||
const columnNamePrefixRef: Record<any, any> = { id: 0 } |
||||
const sheet: any = this.wb.SheetNames[i] |
||||
let tn: string = (sheet || 'table').replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_').trim() |
||||
|
||||
while (tn in tableNamePrefixRef) { |
||||
tn = `${tn}${++tableNamePrefixRef[tn]}` |
||||
} |
||||
tableNamePrefixRef[tn] = 0 |
||||
|
||||
const table: Record<string, any> = { table_name: tn, ref_table_name: tn, columns: [] } |
||||
this.data[tn] = [] |
||||
const ws: any = this.wb.Sheets[sheet] |
||||
const range = XLSX.utils.decode_range(ws['!ref']) |
||||
let rows: any = XLSX.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: null }) |
||||
|
||||
if (this.name.slice(-3) !== 'csv') { |
||||
// fix precision bug & timezone offset issues introduced by xlsx
|
||||
const basedate = new Date(1899, 11, 30, 0, 0, 0) |
||||
// number of milliseconds since base date
|
||||
const dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000 |
||||
// number of milliseconds in a day
|
||||
const day_ms = 24 * 60 * 60 * 1000 |
||||
// handle date1904 property
|
||||
const fixImportedDate = (date: Date) => { |
||||
const parsed = XLSX.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, { |
||||
date1904: this.wb.Workbook.WBProps.date1904, |
||||
}) |
||||
return new Date(parsed.y, parsed.m, parsed.d, parsed.H, parsed.M, parsed.S) |
||||
} |
||||
// fix imported date
|
||||
rows = rows.map((r: any) => |
||||
r.map((v: any) => { |
||||
return v instanceof Date ? fixImportedDate(v) : v |
||||
}), |
||||
) |
||||
} |
||||
|
||||
const columnNameRowExist = +rows[0].every((v: any) => v === null || typeof v === 'string') |
||||
|
||||
// const colLen = Math.max()
|
||||
for (let col = 0; col < rows[0].length; col++) { |
||||
let cn: string = ((columnNameRowExist && rows[0] && rows[0][col] && rows[0][col].toString().trim()) || `field_${col + 1}`) |
||||
.replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_') |
||||
.trim() |
||||
|
||||
while (cn in columnNamePrefixRef) { |
||||
cn = `${cn}${++columnNamePrefixRef[cn]}` |
||||
} |
||||
columnNamePrefixRef[cn] = 0 |
||||
|
||||
const column: Record<string, any> = { |
||||
column_name: cn, |
||||
ref_column_name: cn, |
||||
} |
||||
|
||||
table.columns.push(column) |
||||
|
||||
// const cellId = `${col.toString(26).split('').map(s => (parseInt(s, 26) + 10).toString(36).toUpperCase())}2`;
|
||||
const cellId = XLSX.utils.encode_cell({ |
||||
c: range.s.c + col, |
||||
r: columnNameRowExist, |
||||
}) |
||||
const cellProps = ws[cellId] || {} |
||||
column.uidt = excelTypeToUidt[cellProps.t] || UITypes.SingleLineText |
||||
|
||||
// todo: optimize
|
||||
if (column.uidt === UITypes.SingleLineText) { |
||||
// check for long text
|
||||
if (rows.some((r: any) => (r[col] || '').toString().match(/[\r\n]/) || (r[col] || '').toString().length > 255)) { |
||||
column.uidt = UITypes.LongText |
||||
} else { |
||||
const vals = rows |
||||
.slice(columnNameRowExist ? 1 : 0) |
||||
.map((r: any) => r[col]) |
||||
.filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '') |
||||
|
||||
const checkboxType = isCheckboxType(vals) |
||||
if (checkboxType.length === 1) { |
||||
column.uidt = UITypes.Checkbox |
||||
} else { |
||||
// todo: optimize
|
||||
// check column is multi or single select by comparing unique values
|
||||
// todo:
|
||||
if (vals.some((v: any) => v && v.toString().includes(','))) { |
||||
let flattenedVals = vals.flatMap((v: any) => |
||||
v |
||||
? v |
||||
.toString() |
||||
.trim() |
||||
.split(/\s*,\s*/) |
||||
: [], |
||||
) |
||||
const uniqueVals = (flattenedVals = flattenedVals.filter( |
||||
(v: any, i: any, arr: any) => i === arr.findIndex((v1: any) => v.toLowerCase() === v1.toLowerCase()), |
||||
)) |
||||
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) { |
||||
column.uidt = UITypes.MultiSelect |
||||
column.dtxp = `'${uniqueVals.join("','")}'` |
||||
} |
||||
} else { |
||||
const uniqueVals = vals |
||||
.map((v: any) => v.toString().trim()) |
||||
.filter((v: any, i: any, arr: any) => i === arr.findIndex((v1: any) => v.toLowerCase() === v1.toLowerCase())) |
||||
if (vals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(vals.length / 2)) { |
||||
column.uidt = UITypes.SingleSelect |
||||
column.dtxp = `'${uniqueVals.join("','")}'` |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} else if (column.uidt === UITypes.Number) { |
||||
if ( |
||||
rows.slice(1, this.config.maxRowsToParse).some((v: any) => { |
||||
return v && v[col] && parseInt(v[col]) !== +v[col] |
||||
}) |
||||
) { |
||||
column.uidt = UITypes.Decimal |
||||
} |
||||
if ( |
||||
rows.slice(1, this.config.maxRowsToParse).every((v: any, i: any) => { |
||||
const cellId = XLSX.utils.encode_cell({ |
||||
c: range.s.c + col, |
||||
r: i + columnNameRowExist, |
||||
}) |
||||
|
||||
const cellObj = ws[cellId] |
||||
|
||||
return !cellObj || (cellObj.w && cellObj.w.startsWith('$')) |
||||
}) |
||||
) { |
||||
column.uidt = UITypes.Currency |
||||
} |
||||
} else if (column.uidt === UITypes.DateTime) { |
||||
if ( |
||||
rows.slice(1, this.config.maxRowsToParse).every((v: any, i: any) => { |
||||
const cellId = XLSX.utils.encode_cell({ |
||||
c: range.s.c + col, |
||||
r: i + columnNameRowExist, |
||||
}) |
||||
|
||||
const cellObj = ws[cellId] |
||||
return !cellObj || (cellObj.w && cellObj.w.split(' ').length === 1) |
||||
}) |
||||
) { |
||||
column.uidt = UITypes.Date |
||||
} |
||||
} |
||||
} |
||||
|
||||
let rowIndex = 0 |
||||
for (const row of rows.slice(1)) { |
||||
const rowData: Record<string, any> = {} |
||||
for (let i = 0; i < table.columns.length; i++) { |
||||
if (table.columns[i].uidt === UITypes.Checkbox) { |
||||
rowData[table.columns[i].column_name] = getCheckboxValue(row[i]) |
||||
} else if (table.columns[i].uidt === UITypes.Currency) { |
||||
const cellId = XLSX.utils.encode_cell({ |
||||
c: range.s.c + i, |
||||
r: rowIndex + columnNameRowExist, |
||||
}) |
||||
|
||||
const cellObj = ws[cellId] |
||||
rowData[table.columns[i].column_name] = (cellObj && cellObj.w && cellObj.w.replace(/[^\d.]+/g, '')) || row[i] |
||||
} else if (table.columns[i].uidt === UITypes.SingleSelect || table.columns[i].uidt === UITypes.MultiSelect) { |
||||
rowData[table.columns[i].column_name] = (row[i] || '').toString().trim() || null |
||||
} else { |
||||
// toto: do parsing if necessary based on type
|
||||
rowData[table.columns[i].column_name] = row[i] |
||||
} |
||||
} |
||||
this.data[tn].push(rowData) |
||||
rowIndex++ |
||||
} |
||||
this.project.tables.push(table) |
||||
} |
||||
} |
||||
|
||||
getTemplate() { |
||||
return this.project |
||||
} |
||||
|
||||
getData() { |
||||
return this.data |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
import ExcelTemplateAdapter from './ExcelTemplateAdapter' |
||||
import { useNuxtApp } from '#imports' |
||||
|
||||
export default class ExcelUrlTemplateAdapter extends ExcelTemplateAdapter { |
||||
url: string |
||||
excelData: any |
||||
$api: any |
||||
|
||||
constructor(url: string, parserConfig: Record<string, any>) { |
||||
const { $api } = useNuxtApp() |
||||
const name = url?.split('/').pop() |
||||
super(name, parserConfig) |
||||
this.url = url |
||||
this.excelData = null |
||||
this.$api = $api |
||||
} |
||||
|
||||
async init() { |
||||
const data: any = await this.$api.utils.axiosRequestMake({ |
||||
apiMeta: { |
||||
url: this.url, |
||||
}, |
||||
}) |
||||
this.excelData = data.data |
||||
await super.init() |
||||
} |
||||
} |
@ -0,0 +1,168 @@
|
||||
import { UITypes } from 'nocodb-sdk' |
||||
import { |
||||
extractMultiOrSingleSelectProps, |
||||
getCheckboxValue, |
||||
isCheckboxType, |
||||
isDecimalType, |
||||
isEmailType, |
||||
isMultiLineTextType, |
||||
isUrlType, |
||||
} from './parserHelpers' |
||||
import TemplateGenerator from './TemplateGenerator' |
||||
|
||||
const jsonTypeToUidt: Record<string, string> = { |
||||
number: UITypes.Number, |
||||
string: UITypes.SingleLineText, |
||||
date: UITypes.DateTime, |
||||
boolean: UITypes.Checkbox, |
||||
object: UITypes.JSON, |
||||
} |
||||
|
||||
const extractNestedData: any = (obj: any, path: any) => path.reduce((val: any, key: any) => val && val[key], obj) |
||||
|
||||
export default class JSONTemplateAdapter extends TemplateGenerator { |
||||
config: Record<string, any> |
||||
name: string |
||||
data: Record<string, any> |
||||
_jsonData: string | Record<string, any> |
||||
jsonData: Record<string, any> |
||||
project: Record<string, any> |
||||
columns: object |
||||
constructor(name = 'test', data: object, parserConfig = {}) { |
||||
super() |
||||
this.config = { |
||||
maxRowsToParse: 500, |
||||
...parserConfig, |
||||
} |
||||
this.name = name |
||||
this._jsonData = data |
||||
this.project = { |
||||
title: this.name, |
||||
tables: [], |
||||
} |
||||
this.jsonData = [] |
||||
this.data = [] |
||||
this.columns = {} |
||||
} |
||||
|
||||
async init() { |
||||
const parsedJsonData = |
||||
typeof this._jsonData === 'string' |
||||
? // for json editor
|
||||
JSON.parse(this._jsonData) |
||||
: // for file upload
|
||||
JSON.parse(new TextDecoder().decode(this._jsonData as BufferSource)) |
||||
this.jsonData = Array.isArray(parsedJsonData) ? parsedJsonData : [parsedJsonData] |
||||
} |
||||
|
||||
getColumns(): any { |
||||
return this.columns |
||||
} |
||||
|
||||
getData(): any { |
||||
return this.data |
||||
} |
||||
|
||||
parse(): any { |
||||
const jsonData = this.jsonData |
||||
const tn = 'table' |
||||
const table: any = { table_name: tn, ref_table_name: tn, columns: [] } |
||||
|
||||
this.data[tn] = [] |
||||
|
||||
for (const col of Object.keys(jsonData[0])) { |
||||
const columns = this._parseColumn([col], jsonData) |
||||
table.columns.push(...columns) |
||||
} |
||||
|
||||
if (this.config.importData) { |
||||
this._parseTableData(table) |
||||
} |
||||
|
||||
this.project.tables.push(table) |
||||
} |
||||
|
||||
getTemplate() { |
||||
return this.project |
||||
} |
||||
|
||||
_parseColumn( |
||||
path: any = [], |
||||
jsonData = this.jsonData, |
||||
firstRowVal = path.reduce((val: any, k: any) => val && val[k], this.jsonData[0]), |
||||
): any { |
||||
const columns = [] |
||||
// parse nested
|
||||
if (firstRowVal && typeof firstRowVal === 'object' && !Array.isArray(firstRowVal) && this.config.normalizeNested) { |
||||
for (const key of Object.keys(firstRowVal)) { |
||||
const normalizedNestedColumns = this._parseColumn([...path, key], this.jsonData, firstRowVal[key]) |
||||
columns.push(...normalizedNestedColumns) |
||||
} |
||||
} else { |
||||
const cn = path.join('_').replace(/\W/g, '_').trim() |
||||
|
||||
const column: Record<string, any> = { |
||||
column_name: cn, |
||||
ref_column_name: cn, |
||||
path, |
||||
} |
||||
|
||||
column.uidt = jsonTypeToUidt[typeof firstRowVal] || UITypes.SingleLineText |
||||
|
||||
const colData = jsonData.map((r: any) => extractNestedData(r, path)) |
||||
Object.assign(column, this._getColumnUIDTAndMetas(colData, column.uidt)) |
||||
columns.push(column) |
||||
} |
||||
|
||||
return columns |
||||
} |
||||
|
||||
_getColumnUIDTAndMetas(colData: any, defaultType: any) { |
||||
const colProps = { uidt: defaultType } |
||||
// todo: optimize
|
||||
if (colProps.uidt === UITypes.SingleLineText) { |
||||
// check for long text
|
||||
if (isMultiLineTextType(colData)) { |
||||
colProps.uidt = UITypes.LongText |
||||
} |
||||
if (isEmailType(colData)) { |
||||
colProps.uidt = UITypes.Email |
||||
} |
||||
if (isUrlType(colData)) { |
||||
colProps.uidt = UITypes.URL |
||||
} else { |
||||
const checkboxType = isCheckboxType(colData) |
||||
if (checkboxType.length === 1) { |
||||
colProps.uidt = UITypes.Checkbox |
||||
} else { |
||||
Object.assign(colProps, extractMultiOrSingleSelectProps(colData)) |
||||
} |
||||
} |
||||
} else if (colProps.uidt === UITypes.Number) { |
||||
if (isDecimalType(colData)) { |
||||
colProps.uidt = UITypes.Decimal |
||||
} |
||||
} |
||||
return colProps |
||||
} |
||||
|
||||
_parseTableData(tableMeta: any) { |
||||
for (const row of this.jsonData as any) { |
||||
const rowData: any = {} |
||||
for (let i = 0; i < tableMeta.columns.length; i++) { |
||||
const value = extractNestedData(row, tableMeta.columns[i].path || []) |
||||
if (tableMeta.columns[i].uidt === UITypes.Checkbox) { |
||||
rowData[tableMeta.columns[i].ref_column_name] = getCheckboxValue(value) |
||||
} else if (tableMeta.columns[i].uidt === UITypes.SingleSelect || tableMeta.columns[i].uidt === UITypes.MultiSelect) { |
||||
rowData[tableMeta.columns[i].ref_column_name] = (value || '').toString().trim() || null |
||||
} else if (tableMeta.columns[i].uidt === UITypes.JSON) { |
||||
rowData[tableMeta.columns[i].ref_column_name] = JSON.stringify(value) |
||||
} else { |
||||
// toto: do parsing if necessary based on type
|
||||
rowData[tableMeta.columns[i].column_name] = value |
||||
} |
||||
} |
||||
this.data[tableMeta.ref_table_name].push(rowData) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
import JSONTemplateAdapter from './JSONTemplateAdapter' |
||||
import { useNuxtApp } from '#imports' |
||||
|
||||
export default class JSONUrlTemplateAdapter extends JSONTemplateAdapter { |
||||
url: string |
||||
$api: any |
||||
|
||||
constructor(url: string, parserConfig: Record<string, any>) { |
||||
const { $api } = useNuxtApp() |
||||
const name = url.split('/').pop() |
||||
super(name, parserConfig) |
||||
this.url = url |
||||
this.$api = $api |
||||
} |
||||
|
||||
async init() { |
||||
const data = await this.$api.utils.axiosRequestMake({ |
||||
apiMeta: { |
||||
url: this.url, |
||||
}, |
||||
}) |
||||
this._jsonData = data |
||||
await super.init() |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
export default class TemplateGenerator { |
||||
parse() { |
||||
throw new Error("'parse' method is not implemented") |
||||
} |
||||
|
||||
parseData() { |
||||
throw new Error("'parseData' method is not implemented") |
||||
} |
||||
|
||||
parseTemplate() { |
||||
throw new Error("'parseTemplate' method is not implemented") |
||||
} |
||||
|
||||
getColumns() { |
||||
throw new Error("'getColumns' method is not implemented") |
||||
} |
||||
|
||||
getTemplate() { |
||||
throw new Error("'getTemplate' method is not implemented") |
||||
} |
||||
|
||||
getData() { |
||||
throw new Error("'getData' method is not implemented") |
||||
} |
||||
} |
@ -0,0 +1,7 @@
|
||||
import JSONTemplateAdapter from './JSONTemplateAdapter' |
||||
import JSONUrlTemplateAdapter from './JSONUrlTemplateAdapter' |
||||
import CSVTemplateAdapter from './CSVTemplateAdapter' |
||||
import ExcelTemplateAdapter from './ExcelTemplateAdapter' |
||||
import ExcelUrlTemplateAdapter from './ExcelUrlTemplateAdapter' |
||||
|
||||
export { JSONTemplateAdapter, JSONUrlTemplateAdapter, CSVTemplateAdapter, ExcelTemplateAdapter, ExcelUrlTemplateAdapter } |
@ -0,0 +1,95 @@
|
||||
import { UITypes } from 'nocodb-sdk' |
||||
import { isValidURL } from '~/utils/urlUtils' |
||||
import { isEmail } from '~/utils/validation' |
||||
|
||||
const booleanOptions = [ |
||||
{ checked: true, unchecked: false }, |
||||
{ 'x': true, '': false }, |
||||
{ yes: true, no: false }, |
||||
{ y: true, n: false }, |
||||
{ 1: true, 0: false }, |
||||
{ '[x]': true, '[]': false, '[ ]': false }, |
||||
{ '☑': true, '': false }, |
||||
{ '✅': true, '': false }, |
||||
{ '✓': true, '': false }, |
||||
{ '✔': true, '': false }, |
||||
{ enabled: true, disabled: false }, |
||||
{ on: true, off: false }, |
||||
{ 'done': true, '': false }, |
||||
{ true: true, false: false }, |
||||
] |
||||
const aggBooleanOptions: any = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {}) |
||||
|
||||
const getColVal = (row: any, col = null) => { |
||||
return row && col ? row[col] : row |
||||
} |
||||
|
||||
export const isCheckboxType: any = (values: [], col = null) => { |
||||
let options = booleanOptions |
||||
for (let i = 0; i < values.length; i++) { |
||||
const val = getColVal(values[i], col) |
||||
|
||||
if (val === null || val === undefined || val.toString().trim() === '') { |
||||
continue |
||||
} |
||||
|
||||
options = options.filter((v) => val in v) |
||||
if (!options.length) { |
||||
return false |
||||
} |
||||
} |
||||
return options |
||||
} |
||||
export const getCheckboxValue = (value: number) => { |
||||
return value && aggBooleanOptions[value] |
||||
} |
||||
|
||||
export const isMultiLineTextType = (values: [], col = null) => { |
||||
return values.some( |
||||
(r) => (getColVal(r, col) || '').toString().match(/[\r\n]/) || (getColVal(r, col) || '').toString().length > 255, |
||||
) |
||||
} |
||||
|
||||
export const extractMultiOrSingleSelectProps = (colData: []) => { |
||||
const colProps: any = {} |
||||
if (colData.some((v: any) => v && (v || '').toString().includes(','))) { |
||||
let flattenedVals = colData.flatMap((v: any) => |
||||
v |
||||
? v |
||||
.toString() |
||||
.trim() |
||||
.split(/\s*,\s*/) |
||||
: [], |
||||
) |
||||
const uniqueVals = (flattenedVals = flattenedVals.filter( |
||||
(v, i, arr) => i === arr.findIndex((v1) => v.toLowerCase() === v1.toLowerCase()), |
||||
)) |
||||
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) { |
||||
colProps.uidt = UITypes.MultiSelect |
||||
colProps.dtxp = `'${uniqueVals.join("','")}'` |
||||
} |
||||
} else { |
||||
const uniqueVals = colData |
||||
.map((v: any) => (v || '').toString().trim()) |
||||
.filter((v, i, arr) => i === arr.findIndex((v1) => v.toLowerCase() === v1.toLowerCase())) |
||||
if (colData.length > uniqueVals.length && uniqueVals.length <= Math.ceil(colData.length / 2)) { |
||||
colProps.uidt = UITypes.SingleSelect |
||||
colProps.dtxp = `'${uniqueVals.join("','")}'` |
||||
} |
||||
} |
||||
return colProps |
||||
} |
||||
|
||||
export const isDecimalType = (colData: []) => |
||||
colData.some((v: any) => { |
||||
return v && parseInt(v) !== +v |
||||
}) |
||||
|
||||
export const isEmailType = (colData: []) => |
||||
!colData.some((v: any) => { |
||||
return v && !isEmail(v) |
||||
}) |
||||
export const isUrlType = (colData: []) => |
||||
!colData.some((v: any) => { |
||||
return v && !isValidURL(v) |
||||
}) |
Loading…
Reference in new issue