Add in multi-input and multi-select fields, fix/enhance data validation
This commit is contained in:
447
inventory/package-lock.json
generated
447
inventory/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>[]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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]" />
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
]
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user