From 370dbcb9703b730fc7a8d1a0e3aeb64ae11902f6 Mon Sep 17 00:00:00 2001 From: XuKecheng Date: Thu, 14 Dec 2023 14:03:36 +0800 Subject: [PATCH] feat: support personal access token and can drop down to select table 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 --- .github/workflows/submit.yml | 98 ++ .gitignore | 1 + .prettierrc | 9 + README.md | 16 +- package.json | 8 +- src/airtable-import/add-record.tsx | 67 +- src/airtable-import/index.css | 2 +- src/airtable-import/index.tsx | 26 +- src/apis/get-bases.ts | 33 + src/apis/get-records.ts | 16 +- src/apis/get-tables.ts | 22 + src/apis/index.ts | 4 +- src/choose-field/index.css | 8 +- src/choose-field/index.tsx | 145 +- src/components/form-input/index.tsx | 19 +- src/components/form-select/index.css | 19 + src/components/form-select/index.tsx | 30 + src/components/type-select/index.tsx | 10 +- src/constants.tsx | 71 +- src/context.ts | 2 +- src/index.css | 2 +- src/index.tsx | 45 +- src/setting/index.tsx | 174 ++- src/types/base.ts | 4 + src/types/field.ts | 4 +- src/types/form-data.ts | 7 +- src/types/index.ts | 5 +- src/types/record.ts | 2 +- src/types/table.ts | 4 + src/types/view.ts | 4 + src/utils/add-fields.ts | 83 +- src/utils/get-field-name.ts | 32 +- src/utils/get-options.ts | 23 +- src/utils/i18n.ts | 4 +- src/utils/validate-config.ts | 16 +- src/strings.json => strings.json | 120 +- tsconfig.json | 7 +- widget.config.json | 8 +- yarn.lock | 2063 +++++++++++++++----------- 39 files changed, 1897 insertions(+), 1316 deletions(-) create mode 100644 .github/workflows/submit.yml create mode 100644 .prettierrc create mode 100644 src/apis/get-bases.ts create mode 100644 src/apis/get-tables.ts create mode 100644 src/components/form-select/index.css create mode 100644 src/components/form-select/index.tsx create mode 100644 src/types/base.ts create mode 100644 src/types/table.ts create mode 100644 src/types/view.ts rename src/strings.json => strings.json (54%) diff --git a/.github/workflows/submit.yml b/.github/workflows/submit.yml new file mode 100644 index 0000000..e64ccdb --- /dev/null +++ b/.github/workflows/submit.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 039aa52..6ff10c2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2e049bd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "bracketSpacing": true, + "printWidth": 150, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/README.md b/README.md index 14a2b83..46bed75 100644 --- a/README.md +++ b/README.md @@ -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。 ## 支持列类型 @@ -37,4 +30,3 @@ - [ ] 成员 - [ ] 神奇关联 - [ ] 神奇引用 - diff --git a/package.json b/package.json index 900681a..2882019 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.27", + "version": "0.0.32", "description": "a vika widget", "engines": { "node": ">=8.x" @@ -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" diff --git a/src/airtable-import/add-record.tsx b/src/airtable-import/add-record.tsx index 7d4afce..c63875e 100644 --- a/src/airtable-import/add-record.tsx +++ b/src/airtable-import/add-record.tsx @@ -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'; @@ -16,7 +13,7 @@ interface IAddRecord { records?: IRecord[]; fieldMap: IFieldMap; } -export const AddRecord: React.FC = props => { +export const AddRecord: React.FC = (props) => { const { records, fieldMap } = props; const [importing, setImporting] = useState(false); const { setStep } = useContext(Context); @@ -32,29 +29,29 @@ export const AddRecord: React.FC = 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, @@ -64,11 +61,7 @@ export const AddRecord: React.FC = 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); @@ -78,16 +71,16 @@ export const AddRecord: React.FC = 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++; @@ -97,7 +90,7 @@ export const AddRecord: React.FC = props => { } setImporting(false); } - } + }; sync(); }, []); @@ -105,34 +98,34 @@ export const AddRecord: React.FC = props => { const stopImport = () => { stopRef.current = true; - } + }; return (
- {!importing && !stopRef.current && ( - succee image - )} - + {!importing && !stopRef.current && succee image} + {!importing && !stopRef.current && ( - {t(Strings.import_completed)}{t(Strings.dot)} + {t(Strings.import_completed)} + {t(Strings.dot)} )} {!importing && stopRef.current && ( - {t(Strings.import_stoped)}{t(Strings.dot)} + {t(Strings.import_stoped)} + {t(Strings.dot)} )} {isZh ? ( - 共 {records?.length} 行数据,已导入 - {successCountRef.current} 行、失败 + {t(Strings.total)} {records?.length} {t(Strings.rows_imported)} + {successCountRef.current} 行、失败 {failCountRef.current} - ): ( + ) : ( - A total of {records?.length} records, - {successCountRef.current} records has been imported, + A total of {records?.length} records, + {successCountRef.current} records has been imported, {failCountRef.current} records failed )} @@ -148,5 +141,5 @@ export const AddRecord: React.FC = props => { )}
- ) -} \ No newline at end of file + ); +}; diff --git a/src/airtable-import/index.css b/src/airtable-import/index.css index 6e086b5..7a734ed 100644 --- a/src/airtable-import/index.css +++ b/src/airtable-import/index.css @@ -33,4 +33,4 @@ .importAddRecordFail { color: var(--textDangerDefault); -} \ No newline at end of file +} diff --git a/src/airtable-import/index.tsx b/src/airtable-import/index.tsx index d79c935..7715d06 100644 --- a/src/airtable-import/index.tsx +++ b/src/airtable-import/index.tsx @@ -1,7 +1,7 @@ -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'; @@ -9,31 +9,29 @@ import style from './index.css'; interface IAirTableImport { fieldMap: IFieldMap; - records?: IRecord[] + records?: IRecord[]; } -export const AirTableImport: React.FC = props => { +export const AirTableImport: React.FC = (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 + return ; } return (
- - {t(Strings.create_fields)}... - + {t(Strings.create_fields)}...
- ) -} \ No newline at end of file + ); +}; diff --git a/src/apis/get-bases.ts b/src/apis/get-bases.ts new file mode 100644 index 0000000..3fc44e2 --- /dev/null +++ b/src/apis/get-bases.ts @@ -0,0 +1,33 @@ +import { AIRTABLE_API_VERSION, AIRTABLE_URL } from '../constants'; +import { Base } from '../types'; + +export const GetBases = async (personalAccessToken: string) => { + if (!personalAccessToken) { + throw new Error('personalAccessToken is required'); + } + + const url = `${AIRTABLE_URL}/${AIRTABLE_API_VERSION}/meta/bases`; + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${personalAccessToken}`, + Host: AIRTABLE_URL, + }, + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error('Unauthorized: Error Personal Access Token.'); + } else { + const json = await response.json(); + throw new Error(json.error.message || 'Error fetching bases, please check your Personal Access Token.'); + } + } + + const json = await response.json(); + const bases = json.bases.map((base: Base) => ({ id: base.id, name: base.name })); + return bases; +}; diff --git a/src/apis/get-records.ts b/src/apis/get-records.ts index 5637003..99632d7 100644 --- a/src/apis/get-records.ts +++ b/src/apis/get-records.ts @@ -7,24 +7,24 @@ export const getRecords = async (apiKey: string, baseId: string, tableId: string const _query = query || {}; - // 调试单列 + // Debug single field // _query['fields[]'] = 'number'; const queryStr = queryString.stringify(_query); - + const url = `${AIRTABLE_URL}/${AIRTABLE_API_VERSION}/${baseId}/${tableId}?${queryStr}`; const response = await fetch(url, { method: 'GET', headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + apiKey, - 'Host': AIRTABLE_URL - } + Authorization: 'Bearer ' + apiKey, + Host: AIRTABLE_URL, + }, }); const json = await response.json(); - return json -} \ No newline at end of file + return json; +}; diff --git a/src/apis/get-tables.ts b/src/apis/get-tables.ts new file mode 100644 index 0000000..f8da0a7 --- /dev/null +++ b/src/apis/get-tables.ts @@ -0,0 +1,22 @@ +import { AIRTABLE_API_VERSION, AIRTABLE_URL } from '../constants'; +import { Table } from '../types'; + +export const GetTables = async (personalAccessToken: string, baseId: string) => { + if (!personalAccessToken) return null; + + const url = `${AIRTABLE_URL}/${AIRTABLE_API_VERSION}/meta/bases/${baseId}/tables`; + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + personalAccessToken, + Host: AIRTABLE_URL, + }, + }); + + const json = await response.json(); + const tables = json.tables.map((table: Table) => ({ id: table.id, name: table.name })); + return tables; +}; diff --git a/src/apis/index.ts b/src/apis/index.ts index 70fbd52..a05f8a2 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1 +1,3 @@ -export * from './get-records'; \ No newline at end of file +export * from './get-records'; +export * from './get-bases'; +export * from './get-tables'; diff --git a/src/choose-field/index.css b/src/choose-field/index.css index 4af0590..7d0e8d3 100644 --- a/src/choose-field/index.css +++ b/src/choose-field/index.css @@ -1,7 +1,7 @@ .chooseField { max-width: 760px; padding: 24px 16px; - margin: 0 auto; + margin: 0 auto; height: 100%; } @@ -14,7 +14,7 @@ } .chooseFieldLoading { - display:flex; + display: flex; align-items: center; justify-content: center; height: 100%; @@ -74,7 +74,7 @@ .chooseFieldText { margin: 24px 0; color: var(--textCommonSecondary); - display: flex + display: flex; } .chooseFieldText a { @@ -84,4 +84,4 @@ .chooseFieldAction { display: flex; justify-content: space-between; -} \ No newline at end of file +} diff --git a/src/choose-field/index.tsx b/src/choose-field/index.tsx index ce1b991..5f16b92 100644 --- a/src/choose-field/index.tsx +++ b/src/choose-field/index.tsx @@ -1,4 +1,4 @@ -import { Button, IconButton, LinkButton, Modal, showAlert, TextButton, Typography } from '@vikadata/components'; +import { Button, IconButton, LinkButton, Modal, showAlert, TextButton, Typography } from '@apitable/components'; import { getRecords } from '../apis'; import React, { useContext, useEffect, useRef, useState } from 'react'; import { IFieldMap, IFormData, IRecord } from '../types'; @@ -8,8 +8,8 @@ import { concat, keys, omit, toPairs, values } from 'lodash'; import { TypeSelect } from '../components/type-select'; import { Context } from '../context'; import { AirTableImport } from '../airtable-import'; -import { t, FieldType } from '@vikadata/widget-sdk'; -import { DeleteOutlined } from '@vikadata/icons'; +import { t, FieldType } from '@apitable/widget-sdk'; +import { DeleteOutlined } from '@apitable/icons'; import { MAX_FIELDS_LEN } from '../constants'; interface IChooseField { @@ -17,19 +17,21 @@ interface IChooseField { } interface IError { - error: { - message: string; - type: string; - } | string + error: + | { + message: string; + type: string; + } + | string; } export const ChooseField: React.FC = (props) => { const { formData } = props; const { step, setStep } = useContext(Context); const loadRef = useRef(false); - // airtable api 限制必须分页获取数据,每次最多获取 100 条 - // offset 存在表示还有数据,继续请求 - const [data, setData]= useState([]); + // Airtable api limits that the data must be obtained in pages, with a maximum of 100 entries each time + // The existence of offset indicates that there is still data. Continue to request + const [data, setData] = useState([]); useEffect(() => { const load = async () => { @@ -37,10 +39,10 @@ export const ChooseField: React.FC = (props) => { let fetching = true; let offset = ''; let records: IRecord[] = []; - while(fetching) { - const rlt = await getRecords(formData.apiKey, formData.baseId, formData.tableId, { + while (fetching) { + const rlt = await getRecords(formData.personalAccessToken, formData.baseId, formData.tableId, { offset, - view: formData.viewId || '' + view: formData.viewId || '', }); if (rlt.error) { loadRef.current = false; @@ -54,9 +56,9 @@ export const ChooseField: React.FC = (props) => { } loadRef.current = false; setData(records); - } + }; load(); - }, []) + }, []); const isError = !Array.isArray(data); @@ -71,7 +73,7 @@ export const ChooseField: React.FC = (props) => { } else { setFieldMap({}); } - }, [isDataChange]) + }, [isDataChange]); const fieldCount = keys(fieldMap).length; @@ -81,62 +83,57 @@ export const ChooseField: React.FC = (props) => { content: t(Strings.over_200_fields), type: 'error', closable: true, - duration: 0 + duration: 0, }); } - }, [fieldCount]) + }, [fieldCount]); - if (loadRef.current) return ( -
- {t(Strings.get_data)}... -
- ); + if (loadRef.current) return
{t(Strings.get_data)}...
; if (isError) { return (
- {typeof data?.error === 'object' && - {data?.error.type} - } + {typeof data?.error === 'object' && ( + + {data?.error.type} + + )} - - {typeof data?.error === 'object' ? data?.error.message : data?.error} - - + {typeof data?.error === 'object' ? data?.error.message : data?.error} + {t(Strings.help)} -
- ) + ); } const handleNext = () => { setStep(3); - } + }; const handlePre = () => { setStep(1); - } + }; // console.log('fieldMap', fieldMap); if (step === 3) { - return + return ; } - + return (
- 2. {t(Strings.choose_field_type)} + 2. {t(Strings.choose_field_type)} {t(Strings.field_edit_title)} @@ -144,13 +141,9 @@ export const ChooseField: React.FC = (props) => {
- - {t(Strings.pre_field_name)} - + {t(Strings.pre_field_name)}
- - {t(Strings.field_type)} - + {t(Strings.field_type)}
{toPairs(fieldMap).map(([fieldKey, fieldType], index) => { return ( @@ -162,8 +155,8 @@ export const ChooseField: React.FC = (props) => { setValue={(val) => { setFieldMap({ ...fieldMap, - [fieldKey]: [val, fieldType[1]] - }) + [fieldKey]: [val, fieldType[1]], + }); }} /> = (props) => { }} />
- ) + ); })}
- handlePre()}> - {t(Strings.pre)} - -
- ) -} \ No newline at end of file + ); +}; diff --git a/src/components/form-input/index.tsx b/src/components/form-input/index.tsx index f811797..31de2da 100644 --- a/src/components/form-input/index.tsx +++ b/src/components/form-input/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Typography, TextInput } from '@vikadata/components'; +import { Typography, TextInput } from '@apitable/components'; import styles from './index.css'; -import { t } from '@vikadata/widget-sdk'; +import { t } from '@apitable/widget-sdk'; import { Strings } from '../../utils'; interface IFormInput { @@ -12,25 +12,20 @@ interface IFormInput { error?: string; } -export const FormInput: React.FC = props => { +export const FormInput: React.FC = (props) => { const { label, onChange, required, value, error } = props; const handleChange = (e) => { const value = e.target.value.trim(); onChange?.(value); - } + }; return (
{required && *} {label} - + {error &&
{error}
}
- ) -} \ No newline at end of file + ); +}; diff --git a/src/components/form-select/index.css b/src/components/form-select/index.css new file mode 100644 index 0000000..e666e84 --- /dev/null +++ b/src/components/form-select/index.css @@ -0,0 +1,19 @@ +.formSelect { + margin-top: 24px; +} + +.formSelect .label { + margin-bottom: 4px; +} + +.formSelect .formSelectRequired { + color: var(--textDangerDefault); + padding-right: 4px; +} + +.formSelect .formSelectError { + margin-top: 4px; + color: var(--textDangerDefault); + font-size: 12px; + line-height: 18px; +} diff --git a/src/components/form-select/index.tsx b/src/components/form-select/index.tsx new file mode 100644 index 0000000..31d6bd7 --- /dev/null +++ b/src/components/form-select/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Typography, DropdownSelect } from '@apitable/components'; +import styles from './index.css'; + +interface IFormSelect { + options: { value: string; label: string }[]; + label: string; + required?: boolean; + onSelected?: (val: string) => void; + value?: string; + error?: string; +} + +export const FormSelect: React.FC = (props) => { + const { options, label, onSelected, required, value, error } = props; + const handleChange = (e) => { + const value = e.value.trim(); + onSelected?.(value); + }; + return ( +
+ + {required && *} + {label} + + + {error &&
{error}
} +
+ ); +}; diff --git a/src/components/type-select/index.tsx b/src/components/type-select/index.tsx index dd14997..7fe5d53 100644 --- a/src/components/type-select/index.tsx +++ b/src/components/type-select/index.tsx @@ -1,12 +1,12 @@ -import { Select } from '@vikadata/components'; +import { Select } from '@apitable/components'; import React, { useMemo } from 'react'; import { getOptions } from '../../utils'; -export const TypeSelect: React.FC = props => { +export const TypeSelect: React.FC = (props) => { const { value, setValue, fieldValue } = props; const options = useMemo(() => { return getOptions(value, fieldValue); - }, [value]) + }, [value]); return (