多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

429 lines
14 KiB

<template>
<v-dialog v-model="airtableModal" max-width="min(600px, 90%)" persistent>
<v-card class="nc-import-card h-100">
<v-toolbar class="elevation-0 align-center" height="68">
<h3 class="mt-2">
{{ $t('title.importFromAirtable') }}
</h3>
<div v-t="['c:airtable-import:turbo-mode']" class="ml-2 mt-3 title pointer nc-btn-enable-turbo">🚀</div>
<v-spacer />
<v-icon color="warning" class="" @click="airtableModal = false"> mdi-close </v-icon>
</v-toolbar>
<v-divider />
<div class="h-100" style="width: 100%">
<div>
<v-card :class="{ 'pb-4 mt-4': step === 2, 'py-6': step === 1 }" class="elevation-0" min-height="500">
<template v-if="step === 1">
<div class="d-flex flex-column justify-center align-center pt-2 pb-6">
<span
class="subtitle-1 font-weight-medium"
@dblclick="$set(syncSource.details.options, 'syncViews', true)"
>
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="caption grey--text"
target="_blank"
>Where to find this?</a
>
</div>
<v-form v-model="valid">
<div v-if="syncSource" class="px-10 mt-1 mx-auto" style="max-width: 400px">
<v-text-field
v-model="syncSource.details.apiKey"
outlined
dense
label="Api Key"
class="caption nc-input-api-key"
:type="isPasswordVisible ? 'text' : 'password'"
autocomplete="off"
:rules="[v => !!v || 'Api Key is required']"
>
<template #append="">
<v-icon class="mt-1" small @click="isPasswordVisible = !isPasswordVisible">
{{ isPasswordVisible ? 'visibility_off' : 'visibility' }}
</v-icon>
</template>
</v-text-field>
<v-text-field
v-model="syncSourceUrlOrId"
outlined
dense
label="Shared Base ID / URL"
class="caption nc-input-shared-base"
:rules="[v => !!v || 'Shared Base ID / URL is required']"
/>
</div>
<v-expansion-panels v-model="advanceOptionsPanel" class="mx-auto" style="width: 50%" flat>
<v-expansion-panel>
<v-expansion-panel-header hide-actions>
<v-spacer />
<span class="grey--text caption"
>More Options
<v-icon color="grey" small>
mdi-chevron-{{ advanceOptionsPanel === 0 ? 'up' : 'down' }}
</v-icon></span
>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-checkbox
v-model="syncSource.details.options.syncData"
class="caption mt-n4"
label="Import Data"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncViews"
class="caption"
label="Import Secondary Views"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncRollup"
class="caption"
label="Import Rollup Columns"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncLookup"
class="caption"
label="Import Lookup Columns"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncAttachment"
class="caption"
label="Import Attachment Columns"
hide-details
dense
/>
<v-tooltip bottom>
<template #activator="{ on }">
<div v-on="on">
<v-checkbox
v-model="syncSource.details.options.syncFormula"
class="caption"
label="Import Formula Columns"
hide-details
dense
disabled
/>
</div>
</template>
<span>Coming Soon!</span>
</v-tooltip>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-form>
<v-card-actions class="justify-center pb-6 pt-6">
<v-btn
v-t="['c:sync-airtable:save-and-sync']"
class="nc-btn-airtable-import"
:disabled="!valid"
large
color="primary"
@click="saveAndSync"
>
Import
</v-btn>
</v-card-actions>
</template>
<template v-else-if="step === 2">
<v-card-title class="justify-center">
<span class="subtitle-1 font-weight-medium">Logs</span>
</v-card-title>
<v-card
ref="log"
dark
class="mt-2 mx-4 pa-4 elevation-0 green--text"
height="500"
style="overflow-y: auto"
>
<div v-for="({ msg, status }, i) in progress" :key="i">
<v-icon v-if="status === 'FAILED'" color="red" size="15"> mdi-close-circle-outline </v-icon>
<v-icon v-else color="green" size="15"> mdi-currency-usd </v-icon>
<span class="caption nc-text">{{ msg }}</span>
</div>
<div
v-if="
!progress ||
!progress.length ||
(progress[progress.length - 1].status !== 'COMPLETED' &&
progress[progress.length - 1].status !== 'FAILED')
"
class=""
>
<v-icon color="green" size="15"> mdi-loading mdi-spin </v-icon>
<span class="caption nc-text">Importing </span>
<!-- <div class="nc-progress" />-->
</div>
</v-card>
<div
v-if="progress && progress.length && progress[progress.length - 1].status === 'COMPLETED'"
class="pa-4 pt-8 text-center"
>
<v-btn large color="primary" class="nc-btn-go-dashboard" @click="airtableModal = false">
Go to dashboard
</v-btn>
</div>
</template>
<div class="text-center pa-4 pb-0">
<a class="caption grey--text" href="https://github.com/nocodb/nocodb/issues/2052" target="_blank"
>Questions / Help - reach out here</a
>
<br />
<span class="caption grey--text">
This feature is currently in beta and more information can be found
<a href="https://github.com/nocodb/nocodb/discussions/2122" class="caption grey--text" target="_blank"
>here</a
>.</span
>
</div>
</v-card>
</div>
</div>
</v-card>
</v-dialog>
</template>
<script>
import io from 'socket.io-client';
export default {
name: 'ImportFromAirtable',
props: {
value: Boolean,
},
data: () => ({
advanceOptionsPanel: false,
isPasswordVisible: false,
valid: false,
socket: null,
step: 1,
progress: [],
syncSource: {
type: 'Airtable',
details: {
syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1,
apiKey: '',
shareId: '',
options: {
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true,
},
},
},
syncSourceUrlOrId: '',
}),
computed: {
airtableModal: {
set(v) {
this.$emit('input', v);
},
get() {
return this.value;
},
},
},
watch: {
syncSourceUrlOrId(v) {
if (this.syncSource && this.syncSource.details) {
const m = v && v.match(/(exp|shr).{14}/g);
this.syncSource.details.shareId = m ? m[0] : null;
}
},
},
created() {
this.socket = io(new URL(this.$axios.defaults.baseURL, window.location.href.split(/[?#]/)[0]).href, {
extraHeaders: { 'xc-auth': this.$store.state.users.token },
});
this.socket.on('connect_error', () => {
this.socket.disconnect();
this.socket = null;
});
const socket = this.socket;
socket.on('connect', function (data) {
console.log(socket.id);
console.log('socket connected', data);
});
socket.on('progress', d => {
this.progress.push(d);
this.$nextTick(() => {
if (this.$refs.log) {
const el = this.$refs.log.$el;
el.scrollTop = el.scrollHeight;
}
});
if (d.status === 'COMPLETED') {
this.$store
.dispatch('project/_loadTables', {
dbKey: '0.projectJson.envs._noco.db.0',
key: '0.projectJson.envs._noco.db.0.tables',
_nodes: {
dbAlias: 'db',
env: '_noco',
type: 'tableDir',
},
})
.then(() => this.$store.dispatch('tabs/loadFirstTableTab'));
}
});
this.loadSyncSrc();
},
beforeDestroy() {
if (this.socket) {
this.socket.disconnect();
}
},
methods: {
async saveAndSync() {
await this.createOrUpdate();
this.sync();
},
sync() {
this.step = 2;
this.$axios.post(`/api/v1/db/meta/syncs/${this.syncSource.id}/trigger`, this.payload, {
params: {
id: this.socket.id,
},
});
},
async loadSyncSrc() {
const {
data: { list: srcs },
} = await this.$axios.get(`/api/v1/db/meta/projects/${this.projectId}/syncs`);
if (srcs && srcs[0]) {
srcs[0].details = srcs[0].details || {};
this.syncSource = this.migrateSync(srcs[0]);
this.syncSourceUrlOrId = srcs[0].details.shareId;
} else {
this.syncSource = {
type: 'Airtable',
details: {
syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1,
apiKey: '',
shareId: '',
options: {
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true,
},
},
};
}
},
async createOrUpdate() {
try {
const { id, ...payload } = this.syncSource;
if (id) {
await this.$axios.patch(`/api/v1/db/meta/syncs/${id}`, payload);
} else {
this.syncSource = (await this.$axios.post(`/api/v1/db/meta/projects/${this.projectId}/syncs`, payload)).data;
}
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000);
}
},
// enableTurbo() {
// this.$set(this.syncSource.details.options, 'syncViews', true)
// this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
// },
migrateSync(src) {
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;
},
},
};
</script>
<style scoped>
.nc-progress {
margin-left: 12px;
position: relative;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #9880ff;
color: #9880ff;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.5s;
}
.nc-progress::before,
.nc-progress::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.nc-progress::before {
left: -7.5px;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #9880ff;
color: #9880ff;
animation: dotFlashing 1s infinite alternate;
animation-delay: 0s;
}
.nc-progress::after {
left: 7.5px;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: var(--v-primary-base);
color: var(--v-primary-base);
animation: dotFlashing 1s infinite alternate;
animation-delay: 1s;
}
@keyframes dotFlashing {
0% {
background-color: var(--v-primary-base);
}
50%,
100% {
background-color: var(--v-backgroundColor-base);
}
}
</style>