From 43d7775d085e917da9b379504ba53d20aa130cbb Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 19 Feb 2025 17:11:17 -0500 Subject: [PATCH] Add in multi-input and multi-select fields, fix/enhance data validation --- inventory/package-lock.json | 447 +++++++++++++++++- inventory/package.json | 4 +- .../MatchColumnsStep/MatchColumnsStep.tsx | 17 + .../utils/normalizeTableData.ts | 25 + .../steps/MatchColumnsStep/utils/setColumn.ts | 29 +- .../steps/ValidationStep/ValidationStep.tsx | 443 ++++++++++++++--- .../ValidationStep/components/columns.tsx | 191 -------- .../lib/react-spreadsheet-import/src/types.ts | 15 +- inventory/src/pages/import/Import.tsx | 74 +-- 9 files changed, 919 insertions(+), 326 deletions(-) delete mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/components/columns.tsx diff --git a/inventory/package-lock.json b/inventory/package-lock.json index d6ed5bd..d8da22b 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -33,10 +33,10 @@ "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.2", @@ -5720,19 +5720,435 @@ } }, "node_modules/cmdk": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", - "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.0", - "use-sync-external-store": "^1.2.2" + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/codepage": { @@ -9240,15 +9656,6 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/inventory/package.json b/inventory/package.json index 6d03975..bd93c3d 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -35,10 +35,10 @@ "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.2", diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index fc66a36..b8c515e 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -38,6 +38,8 @@ export enum ColumnType { matchedCheckbox, matchedSelect, matchedSelectOptions, + matchedMultiInput, + matchedMultiSelect, } export type MatchedOptions = { @@ -63,6 +65,19 @@ export type MatchedSelectOptionsColumn = { value: T matchedOptions: MatchedOptions[] } +export type MatchedMultiInputColumn = { + type: ColumnType.matchedMultiInput + index: number + header: string + value: T +} +export type MatchedMultiSelectColumn = { + type: ColumnType.matchedMultiSelect + index: number + header: string + value: T + matchedOptions: MatchedOptions[] +} export type Column = | EmptyColumn @@ -71,6 +86,8 @@ export type Column = | MatchedSwitchColumn | MatchedSelectColumn | MatchedSelectOptionsColumn + | MatchedMultiInputColumn + | MatchedMultiSelectColumn export type Columns = Column[] diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/normalizeTableData.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/normalizeTableData.ts index e1c8b29..1f01fd1 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/normalizeTableData.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/normalizeTableData.ts @@ -25,12 +25,37 @@ export const normalizeTableData = (columns: Columns, data: acc[column.value] = curr === "" ? undefined : curr return acc } + case ColumnType.matchedMultiInput: { + const field = fields.find((field) => field.key === column.value)! + if (curr) { + const separator = field.fieldType.type === "multi-input" ? field.fieldType.separator || "," : "," + acc[column.value] = curr.split(separator).map(v => v.trim()).filter(Boolean) + } else { + acc[column.value] = undefined + } + return acc + } case ColumnType.matchedSelect: case ColumnType.matchedSelectOptions: { const matchedOption = column.matchedOptions.find(({ entry, value }) => entry === curr) acc[column.value] = matchedOption?.value || undefined return acc } + case ColumnType.matchedMultiSelect: { + const field = fields.find((field) => field.key === column.value)! + if (curr) { + const separator = field.fieldType.type === "multi-select" ? field.fieldType.separator || "," : "," + const entries = curr.split(separator).map(v => v.trim()).filter(Boolean) + const values = entries.map(entry => { + const matchedOption = column.matchedOptions.find(({ entry: optEntry }) => optEntry === entry) + return matchedOption?.value + }).filter(Boolean) as string[] + acc[column.value] = values.length ? values : undefined + } else { + acc[column.value] = undefined + } + return acc + } case ColumnType.empty: case ColumnType.ignored: { return acc diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setColumn.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setColumn.ts index 6c7a811..cd3d6ff 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -1,4 +1,4 @@ -import type { Field } from "../../../types" +import type { Field, MultiSelect } from "../../../types" import { Column, ColumnType, MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep" import { uniqueEntries } from "./uniqueEntries" @@ -28,10 +28,37 @@ export const setColumn = ( value: field.key, matchedOptions, } + case "multi-select": + const multiSelectFieldType = field.fieldType as MultiSelect + const multiSelectFieldOptions = multiSelectFieldType.options + const multiSelectUniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] + const multiSelectMatchedOptions = autoMapSelectValues + ? multiSelectUniqueData.map((record) => { + // Split the entry by the separator (default to comma) + const entries = record.entry.split(multiSelectFieldType.separator || ",").map(e => e.trim()) + // Try to match each entry to an option + const values = entries.map(entry => { + const value = multiSelectFieldOptions.find( + (fieldOption) => fieldOption.value === entry || fieldOption.label === entry, + )?.value + return value + }).filter(Boolean) as T[] + return { ...record, value: values.length ? values[0] : undefined } as MatchedOptions + }) + : multiSelectUniqueData + + return { + ...oldColumn, + type: ColumnType.matchedMultiSelect, + value: field.key, + matchedOptions: multiSelectMatchedOptions, + } case "checkbox": return { index: oldColumn.index, type: ColumnType.matchedCheckbox, value: field.key, header: oldColumn.header } case "input": return { index: oldColumn.index, type: ColumnType.matched, value: field.key, header: oldColumn.header } + case "multi-input": + return { index: oldColumn.index, type: ColumnType.matchedMultiInput, value: field.key, header: oldColumn.header } default: return { index: oldColumn.index, header: oldColumn.header, type: ColumnType.empty } } diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index d3843e9..8659d4f 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -1,8 +1,23 @@ -import { useCallback, useMemo, useState } from "react" +import { useCallback, useMemo, useState, useEffect } from "react" import { useRsi } from "../../hooks/useRsi" import type { Meta } from "./types" import { addErrorsAndRunHooks } from "./utils/dataMutations" -import type { Data, Field, SelectOption } from "../../types" +import type { Data, Field, SelectOption, MultiInput } from "../../types" +import { Check, ChevronsUpDown, ArrowDown } from "lucide-react" +import { cn } from "@/lib/utils" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" import { Table, TableBody, @@ -20,6 +35,9 @@ import { } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Switch } from "@/components/ui/switch" +import { useToast } from "@/hooks/use-toast" import { AlertDialog, AlertDialogAction, @@ -32,16 +50,6 @@ import { AlertDialogPortal, AlertDialogOverlay, } from "@/components/ui/alert-dialog" -import { Button } from "@/components/ui/button" -import { Switch } from "@/components/ui/switch" -import { useToast } from "@/hooks/use-toast" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" type Props = { initialData: (Data & Meta)[] @@ -59,59 +67,209 @@ type CellProps = { const EditableCell = ({ value, onChange, error, field }: CellProps) => { const [isEditing, setIsEditing] = useState(false) const [inputValue, setInputValue] = useState(value ?? "") + const [validationError, setValidationError] = useState<{level: string, message: string} | undefined>(error) + const validateRegex = (val: string) => { + const regexValidation = field.validations?.find(v => v.rule === "regex") + if (regexValidation && val) { + const regex = new RegExp(regexValidation.value, regexValidation.flags) + if (!regex.test(val)) { + return { level: regexValidation.level || "error", message: regexValidation.errorMessage } + } + } + return undefined + } + const getDisplayValue = (value: any, fieldType: Field["fieldType"]) => { if (fieldType.type === "select") { return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value } + if (fieldType.type === "multi-select") { + if (Array.isArray(value)) { + return value.map(v => fieldType.options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ") + } + return value + } if (fieldType.type === "checkbox") { if (typeof value === "boolean") return value ? "Yes" : "No" return value } + if (fieldType.type === "multi-input" && Array.isArray(value)) { + return value.join(", ") + } return value } - const isRequiredAndEmpty = field.validations?.some(v => v.rule === "required") && !value + const isRequired = field.validations?.some(v => v.rule === "required") + const isRequiredAndEmpty = isRequired && !value - // Show editing UI for: - // 1. Error cells - // 2. When actively editing - // 3. Required select fields that are empty - // 4. Checkbox fields (always show the checkbox) - const shouldShowEditUI = error?.level === "error" || - isEditing || - (field.fieldType.type === "select" && isRequiredAndEmpty) || - field.fieldType.type === "checkbox" + // Determine the current validation state + const getValidationState = () => { + // Never show validation during editing + if (isEditing) return undefined + + // Only show validation errors if there's a value + if (value) { + if (error) return error + if (validationError) return validationError + } else if (isRequired && !isEditing) { + // Only show required validation when not editing and empty + return { level: "error", message: "Required" } + } + + return undefined + } + + const currentError = getValidationState() + + useEffect(() => { + // Update validation state when value changes externally (e.g. from copy down) + if (!isEditing) { + const newValidationError = value ? validateRegex(value) : undefined + setValidationError(newValidationError) + } + }, [value]) + + const validateAndCommit = (newValue: string) => { + const regexError = newValue ? validateRegex(newValue) : undefined + setValidationError(regexError) + + // Always commit the value + onChange(newValue) + + // Only exit edit mode if there are no errors (except required field errors) + if (!error && !regexError) { + setIsEditing(false) + } + } + + // Handle blur for all input types + const handleBlur = () => { + validateAndCommit(inputValue) + setIsEditing(false) + } + + // Show editing UI only when actually editing + const shouldShowEditUI = isEditing if (shouldShowEditUI) { switch (field.fieldType.type) { case "select": return ( - +
+ { + if (!open) handleBlur() + setIsEditing(open) + }}> + + + + + + + + No options found. + + {field.fieldType.options.map((option) => ( + { + onChange(currentValue) + setIsEditing(false) + }} + > + {option.label} + + + ))} + + + + + + {currentError && ( +

{currentError.message}

+ )} +
+ ) + case "multi-select": + const selectedValues = Array.isArray(value) ? value : value ? [value] : [] + return ( + { + if (!open) handleBlur() + setIsEditing(open) + }}> + + + + + + + + No options found. + + {field.fieldType.options.map((option) => ( + { + const valueIndex = selectedValues.indexOf(currentValue) + let newValues + if (valueIndex === -1) { + newValues = [...selectedValues, currentValue] + } else { + newValues = selectedValues.filter((_, i) => i !== valueIndex) + } + onChange(newValues) + // Don't close on selection for multi-select + }} + > +
+ + {option.label} +
+
+ ))} +
+
+
+
+
) case "checkbox": return ( @@ -124,33 +282,52 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { /> ) - default: + case "multi-input": return ( setInputValue(e.target.value)} + onChange={(e) => { + setInputValue(e.target.value) + }} onKeyDown={(e) => { if (e.key === "Enter") { - onChange(inputValue) - if (!error?.level) { - setIsEditing(false) - } + handleBlur() } }} - onBlur={() => { - onChange(inputValue) - if (!error?.level) { - setIsEditing(false) - } - }} - className={`w-full bg-transparent ${ - error?.level === "error" - ? "border-destructive text-destructive" - : "" - }`} - autoFocus={!error?.level} + onBlur={handleBlur} + className={cn( + "w-full bg-transparent", + currentError ? "border-destructive text-destructive" : "" + )} + autoFocus={!error} + placeholder={`Enter values separated by ${(field.fieldType as MultiInput).separator || ","}`} /> ) + default: + return ( +
+ { + setInputValue(e.target.value) + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleBlur() + } + }} + onBlur={handleBlur} + className={cn( + "w-full bg-transparent", + currentError ? "border-destructive text-destructive" : "" + )} + autoFocus={!error} + /> + {currentError && ( +

{currentError.message}

+ )} +
+ ) } } @@ -160,18 +337,105 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { onClick={() => { if (field.fieldType.type !== "checkbox") { setIsEditing(true) - setInputValue(value ?? "") + setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "") } }} - className={`cursor-text py-2 ${ - error?.level === "error" ? "text-destructive" : "" - }`} + className={cn( + "min-h-[36px] cursor-text p-2 rounded-md border bg-background", + currentError ? "border-destructive" : "border-input", + field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between" + )} > - {getDisplayValue(value, field.fieldType)} +
+ {value ? getDisplayValue(value, field.fieldType) : ""} +
+ {(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && ( + + )} + {currentError && ( +
+ {currentError.message} +
+ )} ) } +// Add this component for the column header with copy down functionality +const ColumnHeader = ({ + field, + data, + onCopyDown +}: { + field: Field, + data: (Data & Meta)[], + onCopyDown: (key: T) => void +}) => { + return ( +
+
+ {field.label} +
+ {data.length > 1 && ( + + )} +
+ ) +} + +// Add this component for the copy down confirmation dialog +const CopyDownDialog = ({ + isOpen, + onClose, + onConfirm, + fieldLabel +}: { + isOpen: boolean + onClose: () => void + onConfirm: () => void + fieldLabel: string +}) => { + return ( + + + + + + + Confirm Copy Down + + + Are you sure you want to copy the value from the first row's "{fieldLabel}" to all rows below? This will overwrite any existing values. + + + + + Cancel + + { + onConfirm() + onClose() + }}> + Copy Down + + + + + + ) +} + export const ValidationStep = ({ initialData, file, onBack }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi() const { toast } = useToast() @@ -181,6 +445,7 @@ export const ValidationStep = ({ initialData, file, onBack }: const [filterByErrors, setFilterByErrors] = useState(false) const [showSubmitAlert, setShowSubmitAlert] = useState(false) const [isSubmitting, setSubmitting] = useState(false) + const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null) // Memoize filtered data to prevent recalculation on every render const filteredData = useMemo(() => { @@ -220,6 +485,25 @@ export const ValidationStep = ({ initialData, file, onBack }: [data, filteredData, updateData], ) + const copyValueDown = useCallback((key: T, label: string) => { + setCopyDownField({ key, label }) + }, []) + + const executeCopyDown = useCallback(() => { + if (!copyDownField || data.length <= 1) return + + const firstRowValue = data[0][copyDownField.key] + const newData = data.map((row, index) => { + if (index === 0) return row + return { + ...row, + [copyDownField.key]: firstRowValue + } + }) + updateData(newData) + setCopyDownField(null) + }, [data, updateData, copyDownField]) + const columns = useMemo & Meta>[]>(() => { const baseColumns: ColumnDef & Meta>[] = [ { @@ -244,7 +528,15 @@ export const ValidationStep = ({ initialData, file, onBack }: }, ...fields.map((field: Field): ColumnDef & Meta> => ({ accessorKey: field.key, - header: field.label, + header: () => ( +
+ copyValueDown(key, field.label)} + /> +
+ ), cell: ({ row, column }) => { const value = row.getValue(column.id) const error = row.original.__errors?.[column.id] @@ -259,7 +551,6 @@ export const ValidationStep = ({ initialData, file, onBack }: /> ) }, - // Use configured width or fallback to sensible defaults size: (field as any).width || ( field.fieldType.type === "checkbox" ? 80 : field.fieldType.type === "select" ? 150 : @@ -268,7 +559,7 @@ export const ValidationStep = ({ initialData, file, onBack }: })), ] return baseColumns - }, [fields, updateRows]) + }, [fields, updateRows, data, copyValueDown]) const table = useReactTable({ data: filteredData, @@ -383,6 +674,12 @@ export const ValidationStep = ({ initialData, file, onBack }: return (
+ setCopyDownField(null)} + onConfirm={executeCopyDown} + fieldLabel={copyDownField?.label || ""} + /> diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/components/columns.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/components/columns.tsx deleted file mode 100644 index 7bfe7bf..0000000 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/components/columns.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import type { Column as RDGColumn, RenderEditCellProps, FormatterProps } from "react-data-grid" -import { useRowSelection } from "react-data-grid" -import { Checkbox, Input, Switch } from "@chakra-ui/react" -import type { Data, Fields, Field, SelectOption } from "../../../types" -import type { ChangeEvent } from "react" -import type { Meta } from "../types" -import { CgInfo } from "react-icons/cg" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -const SELECT_COLUMN_KEY = "select-row" - -function autoFocusAndSelect(input: HTMLInputElement | null) { - input?.focus() - input?.select() -} - -type RowType = Data & Meta - -export const generateColumns = (fields: Fields): RDGColumn>[] => [ - { - key: SELECT_COLUMN_KEY, - name: "", - width: 35, - minWidth: 35, - maxWidth: 35, - resizable: false, - sortable: false, - frozen: true, - cellClass: "rdg-checkbox", - formatter: (props: FormatterProps>) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const [isRowSelected, onRowSelectionChange] = useRowSelection() - return ( - { - onRowSelectionChange({ - row: props.row, - checked: Boolean(event.target.checked), - isShiftClick: (event.nativeEvent as MouseEvent).shiftKey, - }) - }} - /> - ) - }, - }, - ...fields.map( - (column: Field): RDGColumn> => ({ - key: column.key, - name: column.label, - minWidth: 150, - resizable: true, - headerRenderer: () => ( -
-
- {column.label} -
- {column.description && ( -
- -
- )} -
- ), - editable: column.fieldType.type !== "checkbox", - editor: ({ row, onRowChange, onClose }: RenderEditCellProps>) => { - let component - - switch (column.fieldType.type) { - case "select": - component = ( - - ) - break - default: - component = ( -
- ) => { - onRowChange({ ...row, [column.key]: event.target.value }) - }} - onBlur={() => onClose(true)} - /> -
- ) - } - - return component - }, - editorOptions: { - editOnClick: true, - }, - formatter: ({ row, onRowChange }: FormatterProps>) => { - let component - - switch (column.fieldType.type) { - case "checkbox": - component = ( -
{ - event.stopPropagation() - }} - > - { - onRowChange({ ...row, [column.key]: !row[column.key as T] }) - }} - /> -
- ) - break - case "select": - component = ( -
- {column.fieldType.options.find((option: SelectOption) => option.value === row[column.key as T])?.label || null} -
- ) - break - default: - component = ( -
- {row[column.key as T]} -
- ) - } - - if (row.__errors?.[column.key]) { - return ( -
- {component} -
- {row.__errors?.[column.key]?.message} -
-
- ) - } - - return component - }, - cellClass: (row: Meta) => { - switch (row.__errors?.[column.key]?.level) { - case "error": - return "rdg-cell-error" - case "warning": - return "rdg-cell-warning" - case "info": - return "rdg-cell-info" - default: - return "" - } - }, - }), - ), -] diff --git a/inventory/src/lib/react-spreadsheet-import/src/types.ts b/inventory/src/lib/react-spreadsheet-import/src/types.ts index 743dbe0..85a2fa8 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/types.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/types.ts @@ -69,8 +69,8 @@ export type Field = { alternateMatches?: string[] // Validations used for field entries validations?: Validation[] - // Field entry component, default: Input - fieldType: Checkbox | Select | Input + // Field entry component + fieldType: Checkbox | Select | Input | MultiInput | MultiSelect // UI-facing values shown to user as field examples pre-upload phase example?: string } @@ -98,6 +98,17 @@ export type Input = { type: "input" } +export type MultiInput = { + type: "multi-input" + separator?: string // Optional separator for parsing multiple values, defaults to comma +} + +export type MultiSelect = { + type: "multi-select" + options: SelectOption[] + separator?: string // Optional separator for parsing multiple values, defaults to comma +} + export type Validation = RequiredValidation | UniqueValidation | RegexValidation export type RequiredValidation = { diff --git a/inventory/src/pages/import/Import.tsx b/inventory/src/pages/import/Import.tsx index b52c98f..490ba55 100644 --- a/inventory/src/pages/import/Import.tsx +++ b/inventory/src/pages/import/Import.tsx @@ -21,7 +21,7 @@ const IMPORT_FIELDS = [ ], }, width: 200, - validations: [{ rule: "required", errorMessage: "Supplier is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "UPC", @@ -31,9 +31,9 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 150, validations: [ - { rule: "required", errorMessage: "UPC is required", level: "error" }, - { rule: "unique", errorMessage: "UPC must be unique", level: "error" }, - { rule: "regex", value: "^[0-9]+$", errorMessage: "UPC must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "unique", errorMessage: "Must be unique", level: "error" }, + { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -44,8 +44,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 120, validations: [ - { rule: "required", errorMessage: "Supplier # is required", level: "error" }, - { rule: "unique", errorMessage: "Supplier # must be unique", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "unique", errorMessage: "Must be unique", level: "error" }, ], }, { @@ -55,9 +55,9 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 120, validations: [ - { rule: "required", errorMessage: "Notions # is required", level: "error" }, - { rule: "unique", errorMessage: "Notions # must be unique", level: "error" }, - { rule: "regex", value: "^[0-9]+$", errorMessage: "Notions # must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "unique", errorMessage: "Must be unique", level: "error" }, + { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -68,8 +68,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 300, validations: [ - { rule: "required", errorMessage: "Name is required", level: "error" }, - { rule: "unique", errorMessage: "Name must be unique", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "unique", errorMessage: "Must be unique", level: "error" }, ], }, { @@ -79,15 +79,15 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 120, validations: [ - { rule: "required", errorMessage: "Item Number is required", level: "error" }, - { rule: "unique", errorMessage: "Item Number must be unique", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "unique", errorMessage: "Must be unique", level: "error" }, ], }, { label: "Image URL", key: "image_url", description: "Product image URL(s)", - fieldType: { type: "input" }, + fieldType: { type: "multi-input" }, width: 250, }, { @@ -98,8 +98,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "MSRP is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "MSRP must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -110,8 +110,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Qty Per Unit is required", level: "error" }, - { rule: "regex", value: "^[0-9]+$", errorMessage: "Qty Per Unit must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -122,8 +122,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Cost Each is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Cost Each must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -134,7 +134,7 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "regex", value: "^[0-9]+$", errorMessage: "Case Pack must be a number", level: "error" }, + { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -142,7 +142,7 @@ const IMPORT_FIELDS = [ key: "tax_cat", description: "Product tax category", fieldType: { - type: "select", + type: "multi-select", options: [ { label: "Standard", value: "standard" }, { label: "Reduced", value: "reduced" }, @@ -151,7 +151,7 @@ const IMPORT_FIELDS = [ ], }, width: 150, - validations: [{ rule: "required", errorMessage: "Tax Category is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "Company", @@ -159,7 +159,7 @@ const IMPORT_FIELDS = [ description: "Company/Brand name", fieldType: { type: "input" }, width: 200, - validations: [{ rule: "required", errorMessage: "Company is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "Line", @@ -197,8 +197,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Weight is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Weight must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -208,8 +208,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Length is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Length must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -219,8 +219,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Width is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Width must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -230,8 +230,8 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "required", errorMessage: "Height is required", level: "error" }, - { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Height must be a number", level: "error" }, + { rule: "required", errorMessage: "Required", level: "error" }, + { rule: "regex", value: "^[0-9]*.?[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -248,7 +248,7 @@ const IMPORT_FIELDS = [ ], }, width: 150, - validations: [{ rule: "required", errorMessage: "Shipping Restrictions is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "Country Of Origin", @@ -258,7 +258,7 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 100, validations: [ - { rule: "regex", value: "^[A-Z]{2}$", errorMessage: "Country code must be 2 letters", level: "error" }, + { rule: "regex", value: "^[A-Z]{2}$", errorMessage: "Must be 2 letters", level: "error" }, ], }, { @@ -269,7 +269,7 @@ const IMPORT_FIELDS = [ fieldType: { type: "input" }, width: 120, validations: [ - { rule: "regex", value: "^[0-9]+$", errorMessage: "HTS Code must be a number", level: "error" }, + { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], }, { @@ -293,7 +293,7 @@ const IMPORT_FIELDS = [ description: "Detailed product description", fieldType: { type: "input" }, width: 400, - validations: [{ rule: "required", errorMessage: "Description is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "Private Notes", @@ -316,7 +316,7 @@ const IMPORT_FIELDS = [ ], }, width: 200, - validations: [{ rule: "required", errorMessage: "Categories is required", level: "error" }], + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { label: "Themes",