diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a2c9e57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,35 @@ +# This template is heavily inspired by the acme-corp and shadcn-ui/ui repositories. +# See: https://github.com/juliusmarminge/acme-corp/blob/main/.github/ISSUE_TEMPLATE/bug_report.yml +# See: https://github.com/shadcn-ui/ui/blob/main/.github/ISSUE_TEMPLATE/feature_request.yml + +name: Bug report +description: Create a bug report to help us improve +title: "[bug]: " +labels: ["🐞❔ unconfirmed bug"] +body: + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. + validations: + required: true + - type: textarea + attributes: + label: How to reproduce + description: A step-by-step description of how to reproduce the bug. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. See error + validations: + required: true + - type: input + attributes: + label: Link to reproduction + description: A link to a CodeSandbox or StackBlitz that includes a minimal reproduction of the problem. In rare cases when not applicable, you can link to a GitHub repository that we can easily run to recreate the issue. If a report is vague and does not have a reproduction, it will be closed without warning. + validations: + required: true + - type: textarea + attributes: + label: Additional information + description: Add any other information related to the bug here, screenshots if applicable. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6c839a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +# This template is heavily inspired by the shadcn-ui/ui repository. +# See: https://github.com/shadcn-ui/ui/blob/main/.github/ISSUE_TEMPLATE/config.yml + +blank_issues_enabled: false +contact_links: + - name: General questions + url: https://github.com/sadmann7/csv-importer/discussions?category=general + about: Please ask and answer questions here diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a283565 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +# This template is heavily inspired by the shadcn-ui/ui repository. +# See: https://github.com/shadcn-ui/ui/blob/main/.github/ISSUE_TEMPLATE/feature_request.yml + +name: "Feature request" +description: Create a feature request for csv-importer +title: "[feat]: " +labels: ["✨ enhancement"] +body: + - type: markdown + attributes: + value: | + ### Thanks for suggesting a feature request! Make sure to see if your feature request has already been suggested by searching through the existing issues. If you find a similar request, give it a thumbs up and add any additional context you have in the comments. + + - type: textarea + id: feature-description + attributes: + label: Feature description + description: Tell us about your feature request. + placeholder: "I think this feature would be great because..." + value: "Describe your feature request..." + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any other context about the feature here. + placeholder: ex. screenshots, Stack Overflow links, forum links, etc. + value: "Additional details here..." + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: Before submitting + description: Please ensure the following + options: + - label: I've made research efforts and searched the documentation + required: true + - label: I've searched for existing issues and PRs + required: true diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml new file mode 100644 index 0000000..38a6d7c --- /dev/null +++ b/.github/workflows/code-check.yml @@ -0,0 +1,124 @@ +name: Code check + +on: + pull_request: + branches: ["*"] + push: + branches: ["main"] + +jobs: + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3.0.0 + name: Install pnpm + id: pnpm-install + with: + version: 8.6.1 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm install + + - run: cp .env.example .env.local + + - run: pnpm lint + + format: + runs-on: ubuntu-latest + name: Format + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3.0.0 + name: Install pnpm + id: pnpm-install + with: + version: 8.6.1 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - run: cp .env.example .env.local + + - run: pnpm format:check + + tsc: + runs-on: ubuntu-latest + name: Typecheck + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3.0.0 + name: Install pnpm + id: pnpm-install + with: + version: 8.6.1 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm install + + - run: cp .env.example .env.local + + - run: pnpm typecheck diff --git a/src/components/csv-importer.tsx b/src/components/csv-importer.tsx index f50b2ed..b16d9f3 100644 --- a/src/components/csv-importer.tsx +++ b/src/components/csv-importer.tsx @@ -44,11 +44,38 @@ import { FileUploader } from "@/components/file-uploader" interface CsvImporterProps extends React.ComponentPropsWithoutRef, ButtonProps { + /** + * Array of field mappings defining the imported data structure. + * Each includes a label, value, and optional required flag. + * @example fields={[{ label: 'Name', value: 'name', required: true }, { label: 'Email', value: 'email' }]} + */ fields: { + /** + * Field display label shown to the user. + * @example "Name" + */ label: string + + /** + * Key identifying the field in the imported data. + * @example "name" + */ value: string + + /** + * Optional flag indicating if the field is required. + * Required fields cannot be unchecked during mapping. + * @default false + * @example true + */ required?: boolean }[] + + /** + * Callback function called on data import. + * Receives an array of records as key-value pairs. + * @example onImport={(data) => console.log(data)} + */ onImport: (data: Record[]) => void } @@ -91,7 +118,9 @@ export function CsvImporter({ multiple={false} maxSize={4 * 1024 * 1024} maxFileCount={1} - //* Can also use this without uploading the file + /** + * alternatively this can be used without uploading the file + */ // onValueChange={(files) => { // const file = files[0] // if (!file) return diff --git a/src/hooks/use-parse-csv.ts b/src/hooks/use-parse-csv.ts index e4fcc99..f4055c0 100644 --- a/src/hooks/use-parse-csv.ts +++ b/src/hooks/use-parse-csv.ts @@ -3,13 +3,6 @@ import * as Papa from "papaparse" import { getErrorMessage } from "@/lib/handle-error" -interface UseParseCsvProps extends Papa.ParseConfig { - fields: { label: string; value: string; required?: boolean }[] - onSuccess?: (data: Record[]) => void - onError?: (message: string) => void - showEmptyFields?: boolean -} - interface CsvState { fileName: string data: { @@ -23,6 +16,36 @@ interface CsvState { error: string | null } +interface UseParseCsvProps extends Papa.ParseConfig { + /** + * Array of field mappings defining the structure of the imported data. + * Each field includes a label, value, and optional required flag. + * @example fields={[{ label: 'Name', value: 'name', required: true }, { label: 'Email', value: 'email' }]} + */ + fields: { label: string; value: string; required?: boolean }[] + + /** + * Callback function invoked when data is successfully parsed. + * Receives an array of records representing the imported data. + * @example onSuccess={(data) => console.log(data)} + */ + onSuccess?: (data: Record[]) => void + + /** + * Callback function invoked when an error occurs during parsing. + * Receives an error message. + * @example onError={(message) => console.error(message)} + */ + onError?: (message: string) => void + + /** + * Flag to indicate if empty fields should be shown. + * @default false + * @example showEmptyFields={true} + */ + showEmptyFields?: boolean +} + export function useParseCsv({ fields, onSuccess, @@ -61,19 +84,24 @@ export function useParseCsv({ const rows = parsedChunk.data const columns = rows[0] ?? [] - const columnsWithNameAndValues = columns.filter((_, index) => { - const values = rows.slice(1).map((row) => row[index]) - return columns[index] || values.some((value) => value !== "") - }) - - const newColumns = ( - showEmptyFields ? columns : columnsWithNameAndValues - ).map((column, index) => { - if (column.trim() === "") { - return `Column ${index + 1}` - } - return column - }) + const newColumns = columns + .map((column, index) => { + if (column.trim() === "" && !showEmptyFields) { + const hasNonEmptyValue = rows + .slice(1) + .some( + (row) => + row[index] !== "" && + row[index] !== null && + row[index] !== undefined + ) + if (!hasNonEmptyValue) { + return null + } + } + return column.trim() === "" ? `Column ${index + 1}` : column + }) + .filter((column) => column !== null) rows[0] = newColumns return Papa.unparse(rows)