diff --git a/.eslintrc.js b/.eslintrc.js index 8efb3c8..1703a68 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ module.exports = { Atomics: 'readonly', SharedArrayBuffer: 'readonly', }, + parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, @@ -15,8 +16,10 @@ module.exports = { ecmaVersion: 2018, sourceType: 'module', }, - plugins: ['react', 'react-hooks'], + plugins: ['@typescript-eslint', 'react', 'react-hooks'], rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', 'react/prop-types': 0, 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3c99c99 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test +on: + - push + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup + run: npm install + + - name: Test + run: npm run test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4b3ba72 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.0.3] - 2022-10-25 + +### Added + +- This changelog :tada: + +### Changed + +- Moved all code to Typescript +- Refactored code to be more readable +- Added automated testing +- Added list to Airtable marketplace in the Readme + +### Fixed + +- Fixed bug where _Date & time_ generator never generated a time + +## [0.0.2] - 2022-10-20 + +### Added + +- Project was published to the Airtable marketplace + +## [0.0.1] - 2022-09-20 + +Initial project diff --git a/README.md b/README.md index cff1215..e07b285 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Test](https://github.com/usdigitalresponse/airtable-extension-random-record-generator/actions/workflows/test.yml/badge.svg)](https://github.com/usdigitalresponse/airtable-extension-random-record-generator/actions/workflows/test.yml) + # Random record generator An Airtable extension that makes generating fake records for the purpose of demonstrating bases. @@ -6,7 +8,11 @@ An Airtable extension that makes generating fake records for the purpose of demo ## Installation -This extension is not yet published in the Airtable Marketplace. To install it into a base: +Add this extension [through the Airtable marketplace](https://airtable.com/marketplace/blkuZ6kbfEjCTxSNV/random-record-generator). + +## Development + +To start developing on this extension: - Open your base, click **Extensions**, then **Add an extension** - Click the small plus sign (sometimes it says "Build a custom extension" next to it) diff --git a/block.json b/block.json index 83ed6ff..8a6a7df 100644 --- a/block.json +++ b/block.json @@ -1,5 +1,4 @@ { "version": "1.0", - "frontendEntry": "./frontend/index.js", - "frontendTestingEntry": "./test/index.js" + "frontendEntry": "./frontend/index.tsx" } diff --git a/frontend/random-name-generator.js b/frontend/app.tsx similarity index 58% rename from frontend/random-name-generator.js rename to frontend/app.tsx index 100f1ec..f7b5f42 100644 --- a/frontend/random-name-generator.js +++ b/frontend/app.tsx @@ -8,16 +8,24 @@ import { colors, } from '@airtable/blocks/ui' import React from 'react' -import GenerateRecordForm from './form' +import GenerateRecordForm from './components/form' -const RandomRecordGeneratorApp = () => { +/** + * The main app component. Fetches the currently selected table using + * the cursor, checks access to create records in the table, then + * renders the generate record form. + * @returns + */ +const RandomRecordGeneratorApp: React.FC = () => { const base = useBase() const cursor = useCursor() const activeTable = cursor.activeTableId const table = activeTable && base.getTableByIdIfExists(activeTable) + + // There is no active table. if (!table) { return ( - + Random Record Generator Select a table to get started. @@ -25,21 +33,28 @@ const RandomRecordGeneratorApp = () => { ) } + const checkTablePermission = table.checkPermissionsForCreateRecord() return ( - + {table && ( <> - + Generate random records for {table.name}.{' '} {checkTablePermission.hasPermission ? ( ) : ( - - {checkTablePermission.reasonDisplayString} + + { + // @ts-ignore + checkTablePermission.reasonDisplayString + } )} diff --git a/frontend/components/form/field-select.tsx b/frontend/components/form/field-select.tsx new file mode 100644 index 0000000..1c16c77 --- /dev/null +++ b/frontend/components/form/field-select.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { FormField, Box, colors, Text, Select } from '@airtable/blocks/ui' +import { Table, Field } from '@airtable/blocks/models' +import Sample from './sample' + +interface Props { + table: Table + field: Field + fieldSettings: object + setFieldSettings: (fieldSettings: object) => void + generators: any +} + +/** + * A field select - used to select a generator for a field. + * @returns + */ +const FieldSelect: React.FC = ({ + table, + field, + fieldSettings, + setFieldSettings, + generators, +}) => { + const createPermission = table.checkPermissionsForCreateRecord({ + [field.id]: null, + }) + + return ( + + + {createPermission.hasPermission ? ( + + { + const newValue = parseInt(event.target.value, 10) + setNumberOfRecords(newValue) + if (newValue > 1000 || !newValue || event.target.value === '') { + setHasError(true) + } else { + setHasError(false) + } + }} + /> + {numberOfRecords > 1000 && ( + + You can only generate up to 1000 records at a time. + + )} + {!numberOfRecords && ( + + You must at least generate one record. + + )} + +) + +export default NumberOfRecords diff --git a/frontend/components/form/sample.tsx b/frontend/components/form/sample.tsx new file mode 100644 index 0000000..7c7a3d5 --- /dev/null +++ b/frontend/components/form/sample.tsx @@ -0,0 +1,45 @@ +import React, { useState, useEffect } from 'react' +import { Field } from '@airtable/blocks/models' +import { Text, Loader, colors, useBase } from '@airtable/blocks/ui' +import generateContent from '../../lib/generators/generate-content' + +interface Props { + generatorId: string | boolean + field: Field +} + +const Sample: React.FC = ({ generatorId, field }) => { + const base = useBase() + const [value, setValue] = useState(null) + + const { generate } = generateContent(base) + + useEffect(() => { + generate({ + generatorId, + preview: true, + field, + }).then((result) => setValue(result)) + }, [generatorId, field]) + + if (!generatorId || !field) { + return null + } + + return ( + <> + + Sample + + {value ? ( + + {value} + + ) : ( + + )} + + ) +} + +export default Sample diff --git a/frontend/results.js b/frontend/components/results.tsx similarity index 60% rename from frontend/results.js rename to frontend/components/results.tsx index b96cfb4..39c43cd 100644 --- a/frontend/results.js +++ b/frontend/components/results.tsx @@ -1,10 +1,26 @@ import { Box, Heading, Text, ProgressBar, Button } from '@airtable/blocks/ui' import React from 'react' -const GenerateResults = ({ generated, numberOfRecords, onDone }) => ( +interface Props { + generated: number + numberOfRecords: number + onDone: () => void +} + +/** + * The results component. Displays the number of records generated and + * a progress bar. Calls onDone when the user clicks a Start Over button + * once record generation is complete. + * @returns + */ +const GenerateResults: React.FC = ({ + generated, + numberOfRecords, + onDone, +}) => ( Generating records - + Generated {generated} of{' '} {numberOfRecords} records. diff --git a/frontend/data/cities.js b/frontend/data/cities.ts similarity index 100% rename from frontend/data/cities.js rename to frontend/data/cities.ts diff --git a/frontend/form.js b/frontend/form.js deleted file mode 100644 index 0b74e7d..0000000 --- a/frontend/form.js +++ /dev/null @@ -1,246 +0,0 @@ -import { - Box, - FormField, - Button, - Input, - Select, - Text, - Loader, - colors, - colorUtils, - ConfirmationDialog, - useBase, -} from '@airtable/blocks/ui' -import generateContent from './generators' -import GenerateResults from './results' -import React, { useEffect, useState } from 'react' - -const GenerateRecordForm = ({ table }) => { - const base = useBase() - const [fieldSettings, setFieldSettings] = useState( - Object.fromEntries(table.fields.map((field) => [field.id, false])) - ) - const [numberOfRecords, setNumberOfRecords] = useState(100) - const [isDialogOpen, setIsDialogOpen] = useState(false) - const [isGenerating, setIsGenerating] = useState(false) - const [generated, setGenerated] = useState(0) - const [hasError, setHasError] = useState(false) - - const { generators, generate } = generateContent(base) - - const GenerateSample = ({ generatorId, field }) => { - const [value, setValue] = useState(false) - - useEffect(() => { - generate({ - generatorId, - preview: true, - field, - }).then((result) => setValue(result)) - }, [generatorId, field]) - - return ( - <> - - Sample - - {value ? ( - - {value} - - ) : ( - - )} - - ) - } - - const generateRecords = async () => { - const batchSize = 10 - const recordsToInsert = [] - for (let i = 0; i < numberOfRecords; i++) { - const { generate } = generateContent(base) - const record = {} - for (const [fieldId, generatorId] of Object.entries(fieldSettings)) { - if (generatorId) { - record[fieldId] = await generate({ - generatorId, - field: table.getFieldById(fieldId), - }) - } - } - recordsToInsert.push({ fields: record }) - } - let i = 0 - while (i < recordsToInsert.length) { - const recordBatch = recordsToInsert.slice(i, i + batchSize) - await table.createRecordsAsync(recordBatch) - i += recordBatch.length - setGenerated(i) - } - } - - if (isGenerating) { - return ( - { - setIsGenerating(false) - setGenerated(0) - setFieldSettings( - Object.fromEntries(table.fields.map((field) => [field.id, false])) - ) - }} - /> - ) - } - - return ( - - {isDialogOpen && ( - - This will generate {numberOfRecords} records in{' '} - {table.name}. - - } - onConfirm={() => { - setIsDialogOpen(false) - setIsGenerating(true) - generateRecords() - }} - onCancel={() => setIsDialogOpen(false)} - /> - )} -
- - { - const newValue = parseInt(event.target.value, 10) - setNumberOfRecords(newValue) - if (newValue > 1000 || !newValue || event.target.value === '') { - setHasError(true) - } else { - setHasError(false) - } - }} - /> - {numberOfRecords > 1000 && ( - - You can only generate up to 1000 records at a time. - - )} - {!numberOfRecords && ( - - You must at least generate one record. - - )} - - - {table.fields.map((field) => ( - - - {table.checkPermissionsForCreateRecord({ [field.id]: null }) - .hasPermission ? ( - -