Skip to content

Commit

Permalink
feat: support personal access token and can drop down to select table…
Browse files Browse the repository at this point in the history
… and base (#2)

* feat: support personal access token and can drop down to select table and base

* chore: update .gitignore and widget.config.json

* 0.0.28

* chore: refactor GetBases API call and handle error responses

* 0.0.29

* chore: refactor error handling in GetBases API

* 0.0.30

* Remove unused API function

* chore: update CSS, Prettier configuration, and file imports

* chore: modify widget.config.json and update package version
  • Loading branch information
XuKecheng authored Dec 14, 2023
1 parent 03a1c27 commit 370dbcb
Show file tree
Hide file tree
Showing 39 changed files with 1,897 additions and 1,316 deletions.
98 changes: 98 additions & 0 deletions .github/workflows/submit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# This is a basic workflow to help you get started with Actions

name: Submit

# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches:
- main
paths:
- 'package.json'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
allow_error:
description: 'allow a workflow run from failing when a job fails'
required: false
default: false
type: boolean
jobs:
submit_widget_develop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::node_modules"

- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install NPM dependencies
run: yarn install

- name:
run: |
echo "Modify widget.config.json"
sed -i "s/\"globalPackageId\": \"[a-zA-Z0-9]*\"/\"globalPackageId\": \"${{ secrets.DEVELOP_PACKAGE_ID }}\"/g" widget.config.json
git add widget.config.json
git commit -m "Modify widget.config.json"
- name: Get submit version
id: version
uses: ashley-taylor/read-json-property-action@v1.0
with:
path: ./package.json
property: version

- name: Submit Widget develop
continue-on-error: ${{ inputs.allow_error }}
run: |
echo "Submit Widget develop"
npx -y @apitable/widget-cli submit --version ${{steps.version.outputs.value}} --host ${{ secrets.DEVELOP_HOST }} --token ${{ secrets.DEVELOP_TOKEN }}
submit_widget_production:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::node_modules"

- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install NPM dependencies
run: yarn install

- name: Get submit version
id: version
uses: ashley-taylor/read-json-property-action@v1.0
with:
path: ./package.json
property: version

# production submit
- name: Submit Widget production
continue-on-error: ${{ inputs.allow_error }}
run: |
echo "Submit Widget production"
npx -y @apitable/widget-cli submit --version ${{steps.version.outputs.value}} --host ${{ secrets.PRODUCTION_HOST }} --token ${{ secrets.PRODUCTION_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ lerna-debug.log*

# vika yml will store your api token, so you need to ignore it
.vika.yml
.apitable.yml

# ignore packed files
*.zip
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"bracketSpacing": true,
"printWidth": 150,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@

`airtable-import` 小程序,支持将 Airtable 中的数据导入到 Vika 维格表中。

## 获取 Personal Access Token

## 获取配置
要使用该小程序,首先需要从你的 Airtable 账户中获取 Personal Access Token。获取后,只需将其输入,即可开始获取和导入原始数据。

1. 如何获取 API Key
你可以使用 [此链接](https://airtable.com/create/tokens) 创建和管理你的个人访问令牌。请注意,Personal Access Token 与之前使用的 API Key 不同。Airtable 已经宣布,他们将在 2024 年 1 月底停止支持 API Key。要了解更多详情,请阅读[本文](https://support.airtable.com/docs/airtable-api-key-deprecation-notice)

需要你的 Airtable API Key 才能获取数据源,Airtable API Key 获取方式可以参考 [Airtable 的官方文档](https://support.airtable.com/hc/en-us/articles/219046777) 或点击直接进入 [Airtable Account](https://airtable.com/account)获取。

2. 如何获取 Base ID

我们需要对应的 Airtable Base ID 才能获取数据源,Airtable Base ID 获取方式可以点击进入 [Airtable Rest API](https://airtable.com/api) 获取,首先选择想要导入的 Base,然后从 INTRODUCTION 页面复制即可。

3. 如何获取 Table ID

我们需要对应的 Airtable Table ID 才能获取数据源,Airtable Table ID 获取方式可以点击进入 [Airtable Rest API](https://airtable.com/api) 获取,首先选择想要导入的 Table,然后从介绍处获取即可。
访问上面的链接后,只需点击 "Generate new token" 按钮即可创建一个新的 Generate new token。

## 支持列类型

Expand All @@ -37,4 +30,3 @@
- [ ] 成员
- [ ] 神奇关联
- [ ] 神奇引用

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.0.27",
"version": "0.0.32",
"description": "a vika widget",
"engines": {
"node": ">=8.x"
Expand Down Expand Up @@ -27,9 +27,9 @@
"dependencies": {
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@vikadata/components": "latest",
"@vikadata/icons": "latest",
"@vikadata/widget-sdk": "latest",
"@apitable/components": "latest",
"@apitable/icons": "latest",
"@apitable/widget-sdk": "latest",
"lodash": "^4.17.21",
"query-string": "^7.1.1",
"typescript": "4.1.2"
Expand Down
67 changes: 30 additions & 37 deletions src/airtable-import/add-record.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Button, Typography } from '@vikadata/components';
import {
FieldType, useActiveViewId, useDatasheet, useFields, upload, IAttachmentValue, t,
getLanguage, LangType,
} from '@vikadata/widget-sdk';
import { Button, Typography } from '@apitable/components';
import { FieldType, useActiveViewId, useDatasheet, useFields, upload, IAttachmentValue, t, getLanguage, LangType } from '@apitable/widget-sdk';
import { find, has, isEmpty } from 'lodash';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { getFileBlob, Strings } from '../utils';
Expand All @@ -16,7 +13,7 @@ interface IAddRecord {
records?: IRecord[];
fieldMap: IFieldMap;
}
export const AddRecord: React.FC<IAddRecord> = props => {
export const AddRecord: React.FC<IAddRecord> = (props) => {
const { records, fieldMap } = props;
const [importing, setImporting] = useState(false);
const { setStep } = useContext(Context);
Expand All @@ -32,29 +29,29 @@ export const AddRecord: React.FC<IAddRecord> = props => {
}
const sync = async () => {
if (records) {
setImporting(true)
setImporting(true);
let i = 0;
while(i < records.length && !stopRef.current) {
while (i < records.length && !stopRef.current) {
const record = records[i];
let newRecord: object = {};
for (const fieldName in record.fields) {
const field = find(fields, { name: fieldName });
if (!field || !fieldMap[fieldName]) {
// console.log(`${fieldName} 没有对应列`);
// console.log(`${fieldName} has no column`);
continue;
} else {
let recordValue = record.fields[fieldName];
// 附件添加行:获取附件 blob => file 带文件名、文件类型 => 上传 => 添加行
// TODO: 要限制 blob 大小
// Attachment adding line: obtain attachment blob => file with file name and file type => upload => add line
// TODO: limit file blob size
if (field.type === FieldType.Attachment) {
const files: IAttachmentValue[] = [];
for(let k = 0; k < recordValue.length; k++) {
for (let k = 0; k < recordValue.length; k++) {
const rv = recordValue[k];
const fileBlob = await getFileBlob(rv.url);
// 上传小于 10MB 的文件
// Upload files smaller than 10MB
if (fileBlob.size < MAX_FILE_SIZE) {
const curFile = new File([fileBlob], rv.filename, {
type: rv.type
type: rv.type,
});
const uploadRlt = await upload({
file: curFile,
Expand All @@ -64,11 +61,7 @@ export const AddRecord: React.FC<IAddRecord> = props => {
}
}
recordValue = files;
} else if (
field.type !== FieldType.MultiSelect &&
Array.isArray(recordValue) &&
typeof recordValue[0] === 'string'
) {
} else if (field.type !== FieldType.MultiSelect && Array.isArray(recordValue) && typeof recordValue[0] === 'string') {
recordValue = recordValue.join(',');
} else if (field.type !== FieldType.MultiSelect && typeof recordValue === 'object') {
recordValue = JSON.stringify(recordValue);
Expand All @@ -78,16 +71,16 @@ export const AddRecord: React.FC<IAddRecord> = props => {
}
try {
// console.log('newRecord', newRecord);
// 整行为空忽略
// Integer line null ignore
if (!isEmpty(newRecord)) {
const checkRlt = await datasheet.checkPermissionsForAddRecord(newRecord);
if (checkRlt.acceptable) {
await datasheet.addRecord(newRecord);
successCountRef.current++;
successCountRef.current++;
} else {
failCountRef.current++;
console.error(checkRlt.message);
}
}
}
} catch (e) {
failCountRef.current++;
Expand All @@ -97,42 +90,42 @@ export const AddRecord: React.FC<IAddRecord> = props => {
}
setImporting(false);
}
}
};
sync();
}, []);

const isZh = getLanguage() === LangType.ZhCN;

const stopImport = () => {
stopRef.current = true;
}
};

return (
<div className={style.importAddRecord}>
{!importing && !stopRef.current && (
<img className={style.importAddRecordImg} src={successImg} alt="succee image"/>
)}
<Typography variant="h6" className={style.importProcess}>
{!importing && !stopRef.current && <img className={style.importAddRecordImg} src={successImg} alt="succee image" />}
<Typography variant="h6" className={style.importProcess}>
{!importing && !stopRef.current && (
<span>
{t(Strings.import_completed)}{t(Strings.dot)}
{t(Strings.import_completed)}
{t(Strings.dot)}
</span>
)}
{!importing && stopRef.current && (
<span>
{t(Strings.import_stoped)}{t(Strings.dot)}
{t(Strings.import_stoped)}
{t(Strings.dot)}
</span>
)}
{isZh ? (
<span>
{records?.length} 行数据,已导入
<span className={style.importAddRecordSuccess}>{successCountRef.current}</span> 行、失败
{t(Strings.total)} {records?.length} {t(Strings.rows_imported)}
<span className={style.importAddRecordSuccess}>{successCountRef.current}</span> 行、失败
<span className={style.importAddRecordFail}>{failCountRef.current}</span>
</span>
): (
) : (
<span>
A total of {records?.length} records,
<span className={style.importAddRecordSuccess}>{successCountRef.current}</span> records has been imported,
A total of {records?.length} records,
<span className={style.importAddRecordSuccess}>{successCountRef.current}</span> records has been imported,
<span className={style.importAddRecordFail}>{failCountRef.current}</span> records failed
</span>
)}
Expand All @@ -148,5 +141,5 @@ export const AddRecord: React.FC<IAddRecord> = props => {
</Button>
)}
</div>
)
}
);
};
2 changes: 1 addition & 1 deletion src/airtable-import/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@

.importAddRecordFail {
color: var(--textDangerDefault);
}
}
26 changes: 12 additions & 14 deletions src/airtable-import/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
import { Typography } from '@vikadata/components';
import { t, useDatasheet } from '@vikadata/widget-sdk';
import { Typography } from '@apitable/components';
import { t, useDatasheet } from '@apitable/widget-sdk';
import React, { useState } from 'react';
import { useEffect } from 'react'
import { useEffect } from 'react';
import { IFieldMap, IRecord } from '../types';
import { addField, sleep, Strings } from '../utils';
import { AddRecord } from './add-record';
import style from './index.css';

interface IAirTableImport {
fieldMap: IFieldMap;
records?: IRecord[]
records?: IRecord[];
}

export const AirTableImport: React.FC<IAirTableImport> = props => {
export const AirTableImport: React.FC<IAirTableImport> = (props) => {
const { fieldMap, records } = props;
const [importing, setImporting] = useState(true);
const datasheet = useDatasheet();
useEffect(() => {
console.log('创建列...');
console.log('Create field ...');
setImporting(true);
const sync = async () => {
await addField(fieldMap, datasheet);
await sleep(3000);
setImporting(false);
}
};
sync();
}, [])
}, []);
if (!importing) {
return <AddRecord records={records} fieldMap={fieldMap} />
return <AddRecord records={records} fieldMap={fieldMap} />;
}
return (
<div className={style.importAddField}>
<Typography variant="h3">
{t(Strings.create_fields)}...
</Typography>
<Typography variant="h3">{t(Strings.create_fields)}...</Typography>
</div>
)
}
);
};
Loading

0 comments on commit 370dbcb

Please sign in to comment.