Add in multi-input and multi-select fields, fix/enhance data validation

This commit is contained in:
2025-02-19 17:11:17 -05:00
parent 527dec4d49
commit 43d7775d08
9 changed files with 919 additions and 326 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -38,6 +38,8 @@ export enum ColumnType {
matchedCheckbox,
matchedSelect,
matchedSelectOptions,
matchedMultiInput,
matchedMultiSelect,
}
export type MatchedOptions<T> = {
@@ -63,6 +65,19 @@ export type MatchedSelectOptionsColumn<T> = {
value: T
matchedOptions: MatchedOptions<T>[]
}
export type MatchedMultiInputColumn<T> = {
type: ColumnType.matchedMultiInput
index: number
header: string
value: T
}
export type MatchedMultiSelectColumn<T> = {
type: ColumnType.matchedMultiSelect
index: number
header: string
value: T
matchedOptions: MatchedOptions<T>[]
}
export type Column<T extends string> =
| EmptyColumn
@@ -71,6 +86,8 @@ export type Column<T extends string> =
| MatchedSwitchColumn<T>
| MatchedSelectColumn<T>
| MatchedSelectOptionsColumn<T>
| MatchedMultiInputColumn<T>
| MatchedMultiSelectColumn<T>
export type Columns<T extends string> = Column<T>[]

View File

@@ -25,12 +25,37 @@ export const normalizeTableData = <T extends string>(columns: Columns<T>, 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

View File

@@ -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 = <T extends string>(
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<T>[]
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<T>
})
: 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 }
}

View File

@@ -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<T extends string> = {
initialData: (Data<T> & 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<string>["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 (
<Select
defaultOpen={isEditing}
value={value as string || ""}
onValueChange={(newValue) => {
onChange(newValue)
setIsEditing(false)
}}
>
<SelectTrigger
className={`w-full ${
(error?.level === "error" || isRequiredAndEmpty)
? "border-destructive text-destructive"
: ""
}`}
>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
{field.fieldType.options.map((option: SelectOption) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="space-y-1">
<Popover open={isEditing} onOpenChange={(open) => {
if (!open) handleBlur()
setIsEditing(open)
}}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={isEditing}
className={cn(
"w-full justify-between",
currentError ? "border-destructive text-destructive" : "border-input"
)}
>
{value
? field.fieldType.options.find((option) => option.value === value)?.label
: "Select..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="Search options..." className="h-9" />
<CommandList>
<CommandEmpty>No options found.</CommandEmpty>
<CommandGroup>
{field.fieldType.options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
onChange(currentValue)
setIsEditing(false)
}}
>
{option.label}
<Check
className={cn(
"ml-auto h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{currentError && (
<p className="text-xs text-destructive">{currentError.message}</p>
)}
</div>
)
case "multi-select":
const selectedValues = Array.isArray(value) ? value : value ? [value] : []
return (
<Popover open={isEditing} onOpenChange={(open) => {
if (!open) handleBlur()
setIsEditing(open)
}}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={isEditing}
className={cn(
"w-full justify-between",
currentError ? "border-destructive text-destructive" : "border-input"
)}
>
{selectedValues.length > 0
? `${selectedValues.length} selected`
: "Select multiple..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="Search options..." className="h-9" />
<CommandList>
<CommandEmpty>No options found.</CommandEmpty>
<CommandGroup>
{field.fieldType.options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
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
}}
>
<div className="flex items-center gap-2">
<Checkbox
checked={selectedValues.includes(option.value)}
className="pointer-events-none"
/>
{option.label}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
case "checkbox":
return (
@@ -124,33 +282,52 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
/>
</div>
)
default:
case "multi-input":
return (
<Input
value={inputValue}
onChange={(e) => 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 (
<div className="space-y-1">
<Input
value={inputValue}
onChange={(e) => {
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 && (
<p className="text-xs text-destructive">{currentError.message}</p>
)}
</div>
)
}
}
@@ -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)}
<div className={cn(!value && "text-muted-foreground")}>
{value ? getDisplayValue(value, field.fieldType) : ""}
</div>
{(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
)}
{currentError && (
<div className="absolute left-0 -bottom-5 text-xs text-destructive">
{currentError.message}
</div>
)}
</div>
)
}
// Add this component for the column header with copy down functionality
const ColumnHeader = <T extends string>({
field,
data,
onCopyDown
}: {
field: Field<T>,
data: (Data<T> & Meta)[],
onCopyDown: (key: T) => void
}) => {
return (
<div className="flex items-center gap-2">
<div className="flex-1 overflow-hidden text-ellipsis">
{field.label}
</div>
{data.length > 1 && (
<Button
variant="ghost"
size="icon"
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
onCopyDown(field.key as T)
}}
title="Copy first row's value down"
>
<ArrowDown className="h-3 w-3" />
</Button>
)}
</div>
)
}
// Add this component for the copy down confirmation dialog
const CopyDownDialog = ({
isOpen,
onClose,
onConfirm,
fieldLabel
}: {
isOpen: boolean
onClose: () => void
onConfirm: () => void
fieldLabel: string
}) => {
return (
<AlertDialog open={isOpen} onOpenChange={onClose}>
<AlertDialogPortal>
<AlertDialogOverlay className="z-[1400]" />
<AlertDialogContent className="z-[1500]">
<AlertDialogHeader>
<AlertDialogTitle>
Confirm Copy Down
</AlertDialogTitle>
<AlertDialogDescription>
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.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={() => {
onConfirm()
onClose()
}}>
Copy Down
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialog>
)
}
export const ValidationStep = <T extends string>({ initialData, file, onBack }: Props<T>) => {
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
const { toast } = useToast()
@@ -181,6 +445,7 @@ export const ValidationStep = <T extends string>({ 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 = <T extends string>({ 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<ColumnDef<Data<T> & Meta>[]>(() => {
const baseColumns: ColumnDef<Data<T> & Meta>[] = [
{
@@ -244,7 +528,15 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
},
...fields.map((field: Field<T>): ColumnDef<Data<T> & Meta> => ({
accessorKey: field.key,
header: field.label,
header: () => (
<div className="group">
<ColumnHeader
field={field}
data={data}
onCopyDown={(key) => copyValueDown(key, field.label)}
/>
</div>
),
cell: ({ row, column }) => {
const value = row.getValue(column.id)
const error = row.original.__errors?.[column.id]
@@ -259,7 +551,6 @@ export const ValidationStep = <T extends string>({ 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 = <T extends string>({ initialData, file, onBack }:
})),
]
return baseColumns
}, [fields, updateRows])
}, [fields, updateRows, data, copyValueDown])
const table = useReactTable({
data: filteredData,
@@ -383,6 +674,12 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
return (
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
<CopyDownDialog
isOpen={!!copyDownField}
onClose={() => setCopyDownField(null)}
onConfirm={executeCopyDown}
fieldLabel={copyDownField?.label || ""}
/>
<AlertDialog open={showSubmitAlert} onOpenChange={setShowSubmitAlert}>
<AlertDialogPortal>
<AlertDialogOverlay className="z-[1400]" />

View File

@@ -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<T extends string> = Data<T> & Meta
export const generateColumns = <T extends string>(fields: Fields<T>): RDGColumn<RowType<T>>[] => [
{
key: SELECT_COLUMN_KEY,
name: "",
width: 35,
minWidth: 35,
maxWidth: 35,
resizable: false,
sortable: false,
frozen: true,
cellClass: "rdg-checkbox",
formatter: (props: FormatterProps<RowType<T>>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection()
return (
<Checkbox
bg="white"
aria-label="Select"
isChecked={isRowSelected}
onChange={(event) => {
onRowSelectionChange({
row: props.row,
checked: Boolean(event.target.checked),
isShiftClick: (event.nativeEvent as MouseEvent).shiftKey,
})
}}
/>
)
},
},
...fields.map(
(column: Field<T>): RDGColumn<RowType<T>> => ({
key: column.key,
name: column.label,
minWidth: 150,
resizable: true,
headerRenderer: () => (
<div className="flex gap-1 items-center relative">
<div className="flex-1 overflow-hidden text-ellipsis">
{column.label}
</div>
{column.description && (
<div className="flex-none">
<CgInfo className="h-4 w-4" />
</div>
)}
</div>
),
editable: column.fieldType.type !== "checkbox",
editor: ({ row, onRowChange, onClose }: RenderEditCellProps<RowType<T>>) => {
let component
switch (column.fieldType.type) {
case "select":
component = (
<Select
defaultOpen
value={row[column.key] as string}
onValueChange={(value) => {
onRowChange({ ...row, [column.key]: value }, true)
}}
>
<SelectTrigger className="w-full border-0 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent
position="popper"
className="z-[1000]"
align="start"
side="bottom"
>
{column.fieldType.options.map((option: SelectOption) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
break
default:
component = (
<div className="pl-2">
<Input
ref={autoFocusAndSelect}
variant="unstyled"
autoFocus
size="small"
value={row[column.key] as string}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onRowChange({ ...row, [column.key]: event.target.value })
}}
onBlur={() => onClose(true)}
/>
</div>
)
}
return component
},
editorOptions: {
editOnClick: true,
},
formatter: ({ row, onRowChange }: FormatterProps<RowType<T>>) => {
let component
switch (column.fieldType.type) {
case "checkbox":
component = (
<div
className="flex items-center h-full"
onClick={(event) => {
event.stopPropagation()
}}
>
<Switch
isChecked={row[column.key] as boolean}
onChange={() => {
onRowChange({ ...row, [column.key]: !row[column.key as T] })
}}
/>
</div>
)
break
case "select":
component = (
<div className="min-w-full min-h-full overflow-hidden text-ellipsis">
{column.fieldType.options.find((option: SelectOption) => option.value === row[column.key as T])?.label || null}
</div>
)
break
default:
component = (
<div className="min-w-full min-h-full overflow-hidden text-ellipsis">
{row[column.key as T]}
</div>
)
}
if (row.__errors?.[column.key]) {
return (
<div className="relative group">
{component}
<div className="absolute left-0 -top-8 z-50 hidden group-hover:block bg-popover text-popover-foreground text-sm p-2 rounded shadow">
{row.__errors?.[column.key]?.message}
</div>
</div>
)
}
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 ""
}
},
}),
),
]

View File

@@ -69,8 +69,8 @@ export type Field<T extends string> = {
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 = {

View File

@@ -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",