Merge branch 'master' into add/webhook-section
This commit is contained in:
commit
35e657a3d5
80 changed files with 9110 additions and 1578 deletions
|
@ -33,3 +33,9 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Hide variants and attributes if product has none - #179 by @dominik-zeglen
|
||||
- Add service account section - #188 by @dominik-zeglen
|
||||
- Add webhook section - #206 by @benekex2
|
||||
- Add variant creator - #177 by @dominik-zeglen
|
||||
- Add git hooks - #209 by @dominik-zeglen
|
||||
- Do not send customer invitation email - #211 by @dominik-zeglen
|
||||
- Send address update mutation only once - #210 by @dominik-zeglen
|
||||
- Update sale details design - #207 by @dominik-zeglen
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
502
package-lock.json
generated
502
package-lock.json
generated
|
@ -1742,12 +1742,40 @@
|
|||
"glob-to-regexp": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
|
||||
"integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "2.0.3",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
|
||||
"integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@nodelib/fs.walk": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
|
||||
"integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.scandir": "2.1.3",
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@oclif/color": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oclif/color/-/color-0.0.0.tgz",
|
||||
|
@ -3416,6 +3444,30 @@
|
|||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"aggregate-error": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
|
||||
"integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clean-stack": "^2.0.0",
|
||||
"indent-string": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clean-stack": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"airbnb-js-shims": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz",
|
||||
|
@ -8938,6 +8990,15 @@
|
|||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz",
|
||||
"integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fault": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.3.tgz",
|
||||
|
@ -9967,6 +10028,12 @@
|
|||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||
"dev": true
|
||||
},
|
||||
"get-own-enumerable-property-symbols": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz",
|
||||
"integrity": "sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA==",
|
||||
"dev": true
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
|
||||
|
@ -10911,6 +10978,48 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-3.0.8.tgz",
|
||||
"integrity": "sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
"cosmiconfig": "^5.2.1",
|
||||
"execa": "^1.0.0",
|
||||
"get-stdin": "^7.0.0",
|
||||
"is-ci": "^2.0.0",
|
||||
"opencollective-postinstall": "^2.0.2",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"please-upgrade-node": "^3.2.0",
|
||||
"read-pkg": "^5.1.1",
|
||||
"run-node": "^1.0.0",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stdin": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
|
||||
"integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
||||
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"hyperlinker": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz",
|
||||
|
@ -11536,6 +11645,12 @@
|
|||
"integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=",
|
||||
"dev": true
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||
"dev": true
|
||||
},
|
||||
"is-object": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
|
||||
|
@ -11611,6 +11726,12 @@
|
|||
"has": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
|
||||
"dev": true
|
||||
},
|
||||
"is-relative": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
|
||||
|
@ -12940,6 +13061,319 @@
|
|||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"version": "9.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.4.2.tgz",
|
||||
"integrity": "sha512-OFyGokJSWTn2M6vngnlLXjaHhi8n83VIZZ5/1Z26SULRUWgR3ITWpAEQC9Pnm3MC/EpCxlwts/mQWDHNji2+zA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
"commander": "^2.20.0",
|
||||
"cosmiconfig": "^5.2.1",
|
||||
"debug": "^4.1.1",
|
||||
"dedent": "^0.7.0",
|
||||
"del": "^5.0.0",
|
||||
"execa": "^2.0.3",
|
||||
"listr": "^0.14.3",
|
||||
"log-symbols": "^3.0.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"please-upgrade-node": "^3.1.1",
|
||||
"string-argv": "^0.3.0",
|
||||
"stringify-object": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
|
||||
"integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
|
||||
"dev": true
|
||||
},
|
||||
"array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
|
||||
"integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
|
||||
"dev": true
|
||||
},
|
||||
"del": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz",
|
||||
"integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"globby": "^10.0.1",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"is-glob": "^4.0.1",
|
||||
"is-path-cwd": "^2.2.0",
|
||||
"is-path-inside": "^3.0.1",
|
||||
"p-map": "^3.0.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz",
|
||||
"integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"get-stream": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^3.0.0",
|
||||
"onetime": "^5.1.0",
|
||||
"p-finally": "^2.0.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz",
|
||||
"integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.0",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"merge2": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
|
||||
"integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
|
||||
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
|
||||
"integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
|
||||
"integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.0.3",
|
||||
"glob": "^7.1.3",
|
||||
"ignore": "^5.1.1",
|
||||
"merge2": "^1.2.3",
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
|
||||
"integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
|
||||
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
|
||||
"dev": true
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-inside": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
|
||||
"integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
|
||||
"dev": true
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
|
||||
"dev": true
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
||||
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2"
|
||||
}
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
|
||||
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
|
||||
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
|
||||
"integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
|
||||
"dev": true
|
||||
},
|
||||
"p-map": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
|
||||
"integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"path-key": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz",
|
||||
"integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==",
|
||||
"dev": true
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz",
|
||||
"integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz",
|
||||
"integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"linux-platform-info": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linux-platform-info/-/linux-platform-info-0.0.3.tgz",
|
||||
|
@ -14588,6 +15022,12 @@
|
|||
"is-wsl": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"opencollective-postinstall": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
|
||||
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
|
||||
"dev": true
|
||||
},
|
||||
"opn": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz",
|
||||
|
@ -15071,6 +15511,12 @@
|
|||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz",
|
||||
"integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
|
@ -15205,6 +15651,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"please-upgrade-node": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
|
||||
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"plop": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/plop/-/plop-2.4.0.tgz",
|
||||
|
@ -17080,6 +17535,12 @@
|
|||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
|
||||
"dev": true
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.0.tgz",
|
||||
|
@ -17124,6 +17585,18 @@
|
|||
"is-promise": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"run-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
|
||||
"integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
|
||||
"dev": true
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
|
||||
"integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"run-queue": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
|
||||
|
@ -17239,6 +17712,12 @@
|
|||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
|
||||
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
|
||||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
|
@ -18040,6 +18519,12 @@
|
|||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-argv": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
|
||||
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
|
||||
"dev": true
|
||||
},
|
||||
"string-length": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||
|
@ -18164,6 +18649,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"stringify-object": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
|
||||
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-own-enumerable-property-symbols": "^3.0.0",
|
||||
"is-obj": "^1.0.1",
|
||||
"is-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
|
@ -18182,6 +18678,12 @@
|
|||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"strip-final-newline": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"dev": true
|
||||
},
|
||||
"style-loader": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz",
|
||||
|
|
17
package.json
17
package.json
|
@ -115,8 +115,10 @@
|
|||
"file-loader": "^1.1.11",
|
||||
"fork-ts-checker-webpack-plugin": "^0.5.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^3.0.8",
|
||||
"jest": "^24.8.0",
|
||||
"jest-file": "^1.0.0",
|
||||
"lint-staged": "^9.4.2",
|
||||
"plop": "^2.4.0",
|
||||
"react-intl-po": "^2.2.2",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
|
@ -157,6 +159,18 @@
|
|||
"^lodash-es(.*)$": "lodash/$1"
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "npm run check-types"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"tslint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -p",
|
||||
"extract-json-messages": "rimraf build/locale && babel src 'src/**/*.{ts,tsx}' -o build/dashboard.bundle.js",
|
||||
|
@ -164,9 +178,8 @@
|
|||
"extract-messages": "npm run extract-json-messages && npm run extract-pot-messages",
|
||||
"build-messages": "rip po2json 'locale/**/*.po' -m 'build/locale/**/*.json' -o 'locale' -c 'description'",
|
||||
"build-types": "apollo client:codegen --target=typescript types --globalTypesFile=src/types/globalTypes.ts",
|
||||
"check-types": "tsc --noEmit",
|
||||
"generate-component": "plop --plopfile .plop/plopfile.js",
|
||||
"lint": "tslint 'src/**/*.{ts,tsx}'",
|
||||
"lint-fix": "tslint 'src/**/*.{ts,tsx}' --fix",
|
||||
"start": "webpack-dev-server --open -d",
|
||||
"storybook": "start-storybook -p 3000 -c src/storybook/",
|
||||
"build-storybook": "build-storybook -c src/storybook/ -o build/storybook",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||
import {
|
||||
AttributeInputTypeEnum,
|
||||
AttributeValueType
|
||||
|
@ -35,7 +36,10 @@ export const attribute: AttributeDetailsFragment = {
|
|||
visibleInStorefront: true
|
||||
};
|
||||
|
||||
export const attributes: AttributeList_attributes_edges_node[] = [
|
||||
export const attributes: Array<
|
||||
AttributeList_attributes_edges_node &
|
||||
ProductDetails_product_productType_variantAttributes
|
||||
> = [
|
||||
{
|
||||
node: {
|
||||
__typename: "Attribute" as "Attribute",
|
||||
|
|
|
@ -60,7 +60,7 @@ const CategoryBackground: React.FC<CategoryBackgroundProps> = props => {
|
|||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Background image (optional)",
|
||||
defaultMessage: "Background Image (optional)",
|
||||
description: "section header"
|
||||
})}
|
||||
toolbar={
|
||||
|
|
|
@ -78,7 +78,7 @@ const CollectionCreatePage: React.StatelessComponent<
|
|||
</AppHeader>
|
||||
<PageHeader
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Add collection",
|
||||
defaultMessage: "Add Collection",
|
||||
description: "page header"
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -80,7 +80,7 @@ export const CollectionImage = withStyles(styles)(
|
|||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Background image (optional)",
|
||||
defaultMessage: "Background Image (optional)",
|
||||
description: "section header"
|
||||
})}
|
||||
toolbar={
|
||||
|
|
|
@ -5,28 +5,39 @@ import FormLabel from "@material-ui/core/FormLabel";
|
|||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const styles = createStyles({
|
||||
formControl: {
|
||||
padding: 0,
|
||||
width: "100%"
|
||||
},
|
||||
formLabel: {
|
||||
marginLeft: "-5px",
|
||||
paddingBottom: "10px"
|
||||
},
|
||||
radioLabel: {
|
||||
"& > span": {
|
||||
padding: "6px"
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
formLabel: {
|
||||
marginBottom: theme.spacing.unit
|
||||
},
|
||||
radioLabel: {
|
||||
marginBottom: -theme.spacing.unit * 1.5
|
||||
},
|
||||
root: {
|
||||
"& $radioLabel": {
|
||||
"&:last-of-type": {
|
||||
marginBottom: 0
|
||||
}
|
||||
},
|
||||
padding: 0,
|
||||
width: "100%"
|
||||
},
|
||||
rootNoLabel: {
|
||||
marginTop: -theme.spacing.unit * 1.5
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "RadioGroupField"
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
interface RadioGroupFieldChoice {
|
||||
export interface RadioGroupFieldChoice {
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
|
@ -39,16 +50,13 @@ interface RadioGroupFieldProps {
|
|||
hint?: string;
|
||||
label?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
export const RadioGroupField = withStyles(styles, {
|
||||
name: "RadioGroupField"
|
||||
})(
|
||||
({
|
||||
export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
|
||||
const {
|
||||
className,
|
||||
classes,
|
||||
disabled,
|
||||
error,
|
||||
label,
|
||||
|
@ -57,42 +65,45 @@ export const RadioGroupField = withStyles(styles, {
|
|||
onChange,
|
||||
name,
|
||||
hint
|
||||
}: RadioGroupFieldProps & WithStyles<typeof styles>) => {
|
||||
return (
|
||||
<FormControl
|
||||
className={classNames(classes.formControl, className)}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
className={classNames(classes.root, className, {
|
||||
[classes.rootNoLabel]: !label
|
||||
})}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label ? (
|
||||
<FormLabel className={classes.formLabel}>{label}</FormLabel>
|
||||
) : null}
|
||||
<RadioGroup
|
||||
aria-label={name}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
{label ? (
|
||||
<FormLabel className={classes.formLabel}>{label}</FormLabel>
|
||||
) : null}
|
||||
<RadioGroup
|
||||
aria-label={name}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
{choices.length > 0 ? (
|
||||
choices.map(choice => (
|
||||
<FormControlLabel
|
||||
value={choice.value}
|
||||
className={classes.radioLabel}
|
||||
control={<Radio color="primary" />}
|
||||
label={choice.label}
|
||||
key={choice.value}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<MenuItem disabled={true}>
|
||||
<FormattedMessage defaultMessage="No results found" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</RadioGroup>
|
||||
{hint && <FormHelperText>{hint}</FormHelperText>}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
{choices.length > 0 ? (
|
||||
choices.map(choice => (
|
||||
<FormControlLabel
|
||||
value={choice.value}
|
||||
className={classes.radioLabel}
|
||||
control={<Radio color="primary" />}
|
||||
label={choice.label}
|
||||
key={choice.value}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<MenuItem disabled={true}>
|
||||
<FormattedMessage defaultMessage="No results found" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</RadioGroup>
|
||||
{hint && <FormHelperText>{hint}</FormHelperText>}
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
RadioGroupField.displayName = "RadioGroupField";
|
||||
export default RadioGroupField;
|
||||
|
|
|
@ -30,6 +30,9 @@ const styles = (theme: Theme) =>
|
|||
"& > div": {
|
||||
padding: "0 14px"
|
||||
},
|
||||
"& fieldset": {
|
||||
background: theme.palette.background.paper
|
||||
},
|
||||
"& textarea": {
|
||||
"&::placeholder": {
|
||||
opacity: [[1], "!important"] as any
|
||||
|
|
|
@ -88,7 +88,7 @@ const CustomerAddressDialog = withStyles(styles, {})(
|
|||
maxWidth="sm"
|
||||
>
|
||||
<Form initial={initialForm} errors={errors} onSubmit={onConfirm}>
|
||||
{({ change, data, errors, submit }) => {
|
||||
{({ change, data, errors }) => {
|
||||
const handleCountrySelect = createSingleAutocompleteSelectHandler(
|
||||
change,
|
||||
setCountryDisplayName,
|
||||
|
@ -128,7 +128,6 @@ const CustomerAddressDialog = withStyles(styles, {})(
|
|||
transitionState={confirmButtonState}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={submit}
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.save} />
|
||||
|
|
|
@ -66,7 +66,7 @@ const CustomerAddresses = withStyles(styles, { name: "CustomerAddresses" })(
|
|||
<CardContent>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Billing address"
|
||||
defaultMessage="Billing Address"
|
||||
description="subsection header"
|
||||
/>
|
||||
</Typography>
|
||||
|
@ -84,7 +84,7 @@ const CustomerAddresses = withStyles(styles, { name: "CustomerAddresses" })(
|
|||
<CardContent>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Shipping address"
|
||||
defaultMessage="Shipping Address"
|
||||
description="subsection header"
|
||||
/>
|
||||
</Typography>
|
||||
|
|
|
@ -47,7 +47,7 @@ const CustomerCreateAddress = withStyles(styles, {
|
|||
<Card className={classes.overflow}>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Primary address",
|
||||
defaultMessage: "Primary Address",
|
||||
description: "page header"
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -48,7 +48,7 @@ const CustomerCreateDetails = withStyles(styles, {
|
|||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Customer overview",
|
||||
defaultMessage: "Customer Overview",
|
||||
description: "header"
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -89,7 +89,7 @@ const CustomerCreatePage: React.StatelessComponent<CustomerCreatePageProps> = ({
|
|||
</AppHeader>
|
||||
<PageHeader
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Add customer",
|
||||
defaultMessage: "Add Customer",
|
||||
description: "page header"
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -51,7 +51,7 @@ const CustomerOrders = withStyles(styles, { name: "CustomerOrders" })(
|
|||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Recent orders",
|
||||
defaultMessage: "Recent Orders",
|
||||
description: "section header"
|
||||
})}
|
||||
toolbar={
|
||||
|
|
|
@ -79,7 +79,7 @@ export const CustomerCreate: React.StatelessComponent<{}> = () => {
|
|||
firstName: formData.customerFirstName,
|
||||
lastName: formData.customerLastName,
|
||||
note: formData.note,
|
||||
sendPasswordEmail: true
|
||||
sendPasswordEmail: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -133,7 +133,7 @@ export const CustomerDetailsView: React.StatelessComponent<
|
|||
onClose={() => navigate(customerUrl(id), true)}
|
||||
onConfirm={() => removeCustomer()}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete customer",
|
||||
defaultMessage: "Delete Customer",
|
||||
description: "dialog header"
|
||||
})}
|
||||
variant="delete"
|
||||
|
|
|
@ -216,7 +216,7 @@ export const CustomerList: React.StatelessComponent<CustomerListProps> = ({
|
|||
}
|
||||
variant="delete"
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete customers",
|
||||
defaultMessage: "Delete Customers",
|
||||
description: "dialog header"
|
||||
})}
|
||||
>
|
||||
|
|
119
src/discounts/components/DiscountDates/DiscountDates.tsx
Normal file
119
src/discounts/components/DiscountDates/DiscountDates.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
|
||||
import Grid from "@saleor/components/Grid";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { FormErrors } from "../../../types";
|
||||
|
||||
interface DiscountDatesProps {
|
||||
data: {
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
hasEndDate: boolean;
|
||||
startDate: string;
|
||||
startTime: string;
|
||||
};
|
||||
defaultCurrency: string;
|
||||
disabled: boolean;
|
||||
errors: FormErrors<"endDate" | "startDate">;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const DiscountDates = ({
|
||||
data,
|
||||
disabled,
|
||||
errors,
|
||||
onChange
|
||||
}: DiscountDatesProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Active Dates",
|
||||
description: "time during discount is active, header"
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid variant="uniform">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.startDate}
|
||||
helperText={errors.startDate}
|
||||
name={"startDate" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.startDate)}
|
||||
value={data.startDate}
|
||||
type="date"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.startDate}
|
||||
helperText={errors.startDate}
|
||||
name={"startTime" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.startHour)}
|
||||
value={data.startTime}
|
||||
type="time"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<ControlledCheckbox
|
||||
checked={data.hasEndDate}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Set end date",
|
||||
description: "voucher end date, switch button"
|
||||
})}
|
||||
name={"hasEndDate" as keyof FormData}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{data.hasEndDate && (
|
||||
<Grid variant="uniform">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.endDate}
|
||||
helperText={errors.endDate}
|
||||
name={"endDate" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.endDate)}
|
||||
value={data.endDate}
|
||||
type="date"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.endDate}
|
||||
helperText={errors.endDate}
|
||||
name={"endTime" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.endHour)}
|
||||
value={data.endTime}
|
||||
type="time"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default DiscountDates;
|
2
src/discounts/components/DiscountDates/index.ts
Normal file
2
src/discounts/components/DiscountDates/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./DiscountDates";
|
||||
export * from "./DiscountDates";
|
|
@ -11,16 +11,20 @@ import PageHeader from "@saleor/components/PageHeader";
|
|||
import SaveButtonBar from "@saleor/components/SaveButtonBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { UserError } from "../../../types";
|
||||
import { SaleType } from "../../../types/globalTypes";
|
||||
import { SaleType as SaleTypeEnum } from "../../../types/globalTypes";
|
||||
import DiscountDates from "../DiscountDates";
|
||||
import SaleInfo from "../SaleInfo";
|
||||
import SalePricing from "../SalePricing";
|
||||
import SaleType from "../SaleType";
|
||||
|
||||
export interface FormData {
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
hasEndDate: boolean;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime: string;
|
||||
type: SaleTypeEnum;
|
||||
value: string;
|
||||
type: SaleType;
|
||||
}
|
||||
|
||||
export interface SaleCreatePageProps {
|
||||
|
@ -44,9 +48,12 @@ const SaleCreatePage: React.StatelessComponent<SaleCreatePageProps> = ({
|
|||
|
||||
const initialForm: FormData = {
|
||||
endDate: "",
|
||||
endTime: "",
|
||||
hasEndDate: false,
|
||||
name: "",
|
||||
startDate: "",
|
||||
type: SaleType.FIXED,
|
||||
startTime: "",
|
||||
type: SaleTypeEnum.FIXED,
|
||||
value: ""
|
||||
};
|
||||
return (
|
||||
|
@ -71,10 +78,12 @@ const SaleCreatePage: React.StatelessComponent<SaleCreatePageProps> = ({
|
|||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<SalePricing
|
||||
<SaleType data={data} disabled={disabled} onChange={change} />
|
||||
<CardSpacer />
|
||||
<DiscountDates
|
||||
data={data}
|
||||
defaultCurrency={defaultCurrency}
|
||||
disabled={disabled}
|
||||
defaultCurrency={defaultCurrency}
|
||||
errors={formErrors}
|
||||
onChange={change}
|
||||
/>
|
||||
|
|
|
@ -11,23 +11,28 @@ import PageHeader from "@saleor/components/PageHeader";
|
|||
import SaveButtonBar from "@saleor/components/SaveButtonBar";
|
||||
import { Tab, TabContainer } from "@saleor/components/Tab";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { maybe } from "../../../misc";
|
||||
import { maybe, splitDateTime } from "../../../misc";
|
||||
import { ListProps, TabListActions, UserError } from "../../../types";
|
||||
import { SaleType } from "../../../types/globalTypes";
|
||||
import { SaleType as SaleTypeEnum } from "../../../types/globalTypes";
|
||||
import { SaleDetails_sale } from "../../types/SaleDetails";
|
||||
import DiscountCategories from "../DiscountCategories";
|
||||
import DiscountCollections from "../DiscountCollections";
|
||||
import DiscountDates from "../DiscountDates";
|
||||
import DiscountProducts from "../DiscountProducts";
|
||||
import SaleInfo from "../SaleInfo";
|
||||
import SalePricing from "../SalePricing";
|
||||
import SaleSummary from "../SaleSummary";
|
||||
import SaleType from "../SaleType";
|
||||
import SaleValue from "../SaleValue";
|
||||
|
||||
export interface FormData {
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
hasEndDate: boolean;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime: string;
|
||||
type: SaleTypeEnum;
|
||||
value: string;
|
||||
type: SaleType;
|
||||
}
|
||||
|
||||
export enum SaleDetailsPageTab {
|
||||
|
@ -106,10 +111,13 @@ const SaleDetailsPage: React.StatelessComponent<SaleDetailsPageProps> = ({
|
|||
const intl = useIntl();
|
||||
|
||||
const initialForm: FormData = {
|
||||
endDate: maybe(() => (sale.endDate ? sale.endDate : ""), ""),
|
||||
endDate: splitDateTime(maybe(() => sale.endDate, "")).date,
|
||||
endTime: splitDateTime(maybe(() => sale.endDate, "")).time,
|
||||
hasEndDate: maybe(() => !!sale.endDate),
|
||||
name: maybe(() => sale.name, ""),
|
||||
startDate: maybe(() => sale.startDate, ""),
|
||||
type: maybe(() => sale.type, SaleType.FIXED),
|
||||
startDate: splitDateTime(maybe(() => sale.startDate, "")).date,
|
||||
startTime: splitDateTime(maybe(() => sale.startDate, "")).time,
|
||||
type: maybe(() => sale.type, SaleTypeEnum.FIXED),
|
||||
value: maybe(() => sale.value.toString(), "")
|
||||
};
|
||||
return (
|
||||
|
@ -129,9 +137,11 @@ const SaleDetailsPage: React.StatelessComponent<SaleDetailsPageProps> = ({
|
|||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<SalePricing
|
||||
<SaleType data={data} disabled={disabled} onChange={change} />
|
||||
<CardSpacer />
|
||||
<SaleValue
|
||||
currencySymbol={defaultCurrency}
|
||||
data={data}
|
||||
defaultCurrency={defaultCurrency}
|
||||
disabled={disabled}
|
||||
errors={formErrors}
|
||||
onChange={change}
|
||||
|
@ -243,6 +253,14 @@ const SaleDetailsPage: React.StatelessComponent<SaleDetailsPageProps> = ({
|
|||
toolbar={productListToolbar}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
<DiscountDates
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
defaultCurrency={defaultCurrency}
|
||||
errors={formErrors}
|
||||
onChange={change}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SaleSummary defaultCurrency={defaultCurrency} sale={sale} />
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import {
|
||||
createStyles,
|
||||
Theme,
|
||||
WithStyles,
|
||||
withStyles
|
||||
} from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import TextFieldWithChoice from "@saleor/components/TextFieldWithChoice";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { FormErrors } from "../../../types";
|
||||
import { SaleType } from "../../../types/globalTypes";
|
||||
import { FormData } from "../SaleDetailsPage";
|
||||
|
||||
export interface SalePricingProps {
|
||||
data: FormData;
|
||||
defaultCurrency: string;
|
||||
disabled: boolean;
|
||||
errors: FormErrors<"startDate" | "endDate" | "value">;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "grid",
|
||||
gridColumnGap: theme.spacing.unit * 2 + "px",
|
||||
gridTemplateColumns: "1fr 1fr"
|
||||
},
|
||||
subheading: {
|
||||
gridColumnEnd: "span 2",
|
||||
marginBottom: theme.spacing.unit * 2
|
||||
}
|
||||
});
|
||||
|
||||
const SalePricing = withStyles(styles, {
|
||||
name: "SalePricing"
|
||||
})(
|
||||
({
|
||||
classes,
|
||||
data,
|
||||
defaultCurrency,
|
||||
disabled,
|
||||
errors,
|
||||
onChange
|
||||
}: SalePricingProps & WithStyles<typeof styles>) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Pricing",
|
||||
description: "sale pricing, header"
|
||||
})}
|
||||
/>
|
||||
<CardContent className={classes.root}>
|
||||
<TextFieldWithChoice
|
||||
disabled={disabled}
|
||||
ChoiceProps={{
|
||||
label: data.type === SaleType.FIXED ? defaultCurrency : "%",
|
||||
name: "type",
|
||||
values: [
|
||||
{
|
||||
label: defaultCurrency,
|
||||
value: SaleType.FIXED
|
||||
},
|
||||
{
|
||||
label: "%",
|
||||
value: SaleType.PERCENTAGE
|
||||
}
|
||||
]
|
||||
}}
|
||||
error={!!errors.value}
|
||||
helperText={errors.value}
|
||||
name={"value" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Discount Value"
|
||||
})}
|
||||
value={data.value}
|
||||
type="number"
|
||||
fullWidth
|
||||
inputProps={{
|
||||
min: 0
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<Hr />
|
||||
<CardContent className={classes.root}>
|
||||
<Typography className={classes.subheading} variant="subtitle1">
|
||||
<FormattedMessage
|
||||
defaultMessage="Time Frame"
|
||||
description="time during which sale is active"
|
||||
/>
|
||||
</Typography>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.startDate}
|
||||
helperText={errors.startDate}
|
||||
name={"startDate" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.startDate)}
|
||||
value={data.startDate}
|
||||
type="date"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!errors.endDate}
|
||||
helperText={errors.endDate}
|
||||
name={"endDate" as keyof FormData}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(commonMessages.endDate)}
|
||||
value={data.endDate}
|
||||
type="date"
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
SalePricing.displayName = "SalePricing";
|
||||
export default SalePricing;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./SalePricing";
|
||||
export * from "./SalePricing";
|
84
src/discounts/components/SaleType/SaleType.tsx
Normal file
84
src/discounts/components/SaleType/SaleType.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import RadioGroupField, {
|
||||
RadioGroupFieldChoice
|
||||
} from "@saleor/components/RadioGroupField";
|
||||
import { FormChange } from "@saleor/hooks/useForm";
|
||||
import { SaleType as SaleTypeEnum } from "@saleor/types/globalTypes";
|
||||
import { FormData } from "../SaleDetailsPage";
|
||||
|
||||
export interface SaleTypeProps {
|
||||
data: FormData;
|
||||
disabled: boolean;
|
||||
onChange: FormChange;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
root: {
|
||||
"&&": {
|
||||
paddingBottom: theme.spacing.unit * 1.5
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "SaleType"
|
||||
}
|
||||
);
|
||||
|
||||
function createChoices(intl: IntlShape): RadioGroupFieldChoice[] {
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Percentage",
|
||||
description: "discount type"
|
||||
}),
|
||||
value: SaleTypeEnum.PERCENTAGE
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Fixed Amount",
|
||||
description: "discount type"
|
||||
}),
|
||||
value: SaleTypeEnum.FIXED
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const SaleType: React.FC<SaleTypeProps> = props => {
|
||||
const { data, disabled, onChange } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const choices = createChoices(intl);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Discount Type",
|
||||
description: "percentage or fixed, header"
|
||||
})}
|
||||
/>
|
||||
<CardContent className={classes.root}>
|
||||
<RadioGroupField
|
||||
choices={choices}
|
||||
disabled={disabled}
|
||||
name={"type" as keyof FormData}
|
||||
value={data.type}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
SaleType.displayName = "SaleType";
|
||||
export default SaleType;
|
2
src/discounts/components/SaleType/index.ts
Normal file
2
src/discounts/components/SaleType/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./SaleType";
|
||||
export * from "./SaleType";
|
61
src/discounts/components/SaleValue/SaleValue.tsx
Normal file
61
src/discounts/components/SaleValue/SaleValue.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import { FormChange } from "@saleor/hooks/useForm";
|
||||
import { FormErrors } from "@saleor/types";
|
||||
import { SaleType } from "@saleor/types/globalTypes";
|
||||
import { FormData } from "../SaleDetailsPage";
|
||||
|
||||
export interface SaleValueProps {
|
||||
currencySymbol: string;
|
||||
data: FormData;
|
||||
disabled: boolean;
|
||||
errors: FormErrors<"value">;
|
||||
onChange: FormChange;
|
||||
}
|
||||
|
||||
const SaleValue: React.FC<SaleValueProps> = ({
|
||||
currencySymbol,
|
||||
data,
|
||||
disabled,
|
||||
errors,
|
||||
onChange
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Value",
|
||||
description: "sale value, header"
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Discount Value",
|
||||
description: "sale discount"
|
||||
})}
|
||||
error={!!errors.value}
|
||||
name="value"
|
||||
InputProps={{
|
||||
endAdornment: data.type === SaleType.FIXED ? currencySymbol : "%"
|
||||
}}
|
||||
helperText={errors.value}
|
||||
value={data.value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
SaleValue.displayName = "SaleValue";
|
||||
export default SaleValue;
|
2
src/discounts/components/SaleValue/index.ts
Normal file
2
src/discounts/components/SaleValue/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./SaleValue";
|
||||
export * from "./SaleValue";
|
|
@ -23,8 +23,8 @@ import {
|
|||
import { VoucherDetails_voucher } from "../../types/VoucherDetails";
|
||||
import DiscountCategories from "../DiscountCategories";
|
||||
import DiscountCollections from "../DiscountCollections";
|
||||
import DiscountDates from "../DiscountDates";
|
||||
import DiscountProducts from "../DiscountProducts";
|
||||
import VoucherDates from "../VoucherDates";
|
||||
import VoucherInfo from "../VoucherInfo";
|
||||
import VoucherLimits from "../VoucherLimits";
|
||||
import VoucherRequirements from "../VoucherRequirements";
|
||||
|
@ -349,7 +349,7 @@ const VoucherDetailsPage: React.StatelessComponent<VoucherDetailsPageProps> = ({
|
|||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<VoucherDates
|
||||
<DiscountDates
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
defaultCurrency={defaultCurrency}
|
||||
|
|
|
@ -70,7 +70,9 @@ const VoucherRequirements = ({
|
|||
value={data.requirementsPicker}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
{[RequirementsPicker.ORDER, RequirementsPicker.ITEM].includes(
|
||||
data.requirementsPicker
|
||||
) && <FormSpacer />}
|
||||
{data.requirementsPicker === RequirementsPicker.ORDER ? (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -29,14 +31,21 @@ export enum VoucherType {
|
|||
SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT"
|
||||
}
|
||||
|
||||
const VoucherValue = ({
|
||||
data,
|
||||
defaultCurrency,
|
||||
disabled,
|
||||
errors,
|
||||
variant,
|
||||
onChange
|
||||
}: VoucherValueProps) => {
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
hr: {
|
||||
margin: `${theme.spacing.unit * 2}px 0`
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "VoucherValue"
|
||||
}
|
||||
);
|
||||
|
||||
const VoucherValue: React.FC<VoucherValueProps> = props => {
|
||||
const { data, defaultCurrency, disabled, errors, variant, onChange } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const translatedVoucherTypes = translateVoucherTypes(intl);
|
||||
|
@ -81,22 +90,22 @@ const VoucherValue = ({
|
|||
<FormSpacer />
|
||||
{variant === "update" && (
|
||||
<>
|
||||
<Hr className={classes.hr} />
|
||||
<RadioGroupField
|
||||
choices={voucherTypeChoices}
|
||||
disabled={disabled}
|
||||
error={!!errors.type}
|
||||
hint={errors.type}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Discount Specific Information"
|
||||
defaultMessage: "Voucher Specific Information"
|
||||
})}
|
||||
name={"type" as keyof FormData}
|
||||
value={data.type}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
</>
|
||||
)}
|
||||
<Hr />
|
||||
<Hr className={classes.hr} />
|
||||
<FormSpacer />
|
||||
<ControlledCheckbox
|
||||
name={"applyOncePerOrder" as keyof FormData}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "../../config";
|
|||
import SearchCategories from "../../containers/SearchCategories";
|
||||
import SearchCollections from "../../containers/SearchCollections";
|
||||
import SearchProducts from "../../containers/SearchProducts";
|
||||
import { decimal, getMutationState, maybe } from "../../misc";
|
||||
import { decimal, getMutationState, joinDateTime, maybe } from "../../misc";
|
||||
import { productUrl } from "../../products/urls";
|
||||
import { DiscountValueTypeEnum, SaleType } from "../../types/globalTypes";
|
||||
import SaleDetailsPage, {
|
||||
|
@ -273,15 +273,17 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
|
|||
variables: {
|
||||
id,
|
||||
input: {
|
||||
endDate:
|
||||
formData.endDate === ""
|
||||
? null
|
||||
: formData.endDate,
|
||||
endDate: formData.hasEndDate
|
||||
? joinDateTime(
|
||||
formData.endDate,
|
||||
formData.endTime
|
||||
)
|
||||
: null,
|
||||
name: formData.name,
|
||||
startDate:
|
||||
formData.startDate === ""
|
||||
? null
|
||||
: formData.startDate,
|
||||
startDate: joinDateTime(
|
||||
formData.startDate,
|
||||
formData.startTime
|
||||
),
|
||||
type: discountValueTypeEnum(
|
||||
formData.type
|
||||
),
|
||||
|
|
|
@ -57,7 +57,7 @@ export const HomeProductList = withStyles(styles, { name: "HomeProductList" })(
|
|||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Top products",
|
||||
defaultMessage: "Top Products",
|
||||
description: "header",
|
||||
id: "homeProductsListCardHeader"
|
||||
})}
|
||||
|
|
|
@ -238,7 +238,7 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
|
|||
}
|
||||
variant="delete"
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete menu",
|
||||
defaultMessage: "Delete Menu",
|
||||
description: "dialog header",
|
||||
id: "menuDetailsDeleteMenuHeader"
|
||||
})}
|
||||
|
|
|
@ -202,7 +202,7 @@ const MenuList: React.FC<MenuListProps> = ({ params }) => {
|
|||
}
|
||||
variant="delete"
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete menu",
|
||||
defaultMessage: "Delete Menu",
|
||||
description: "dialog header",
|
||||
id: "menuListDeleteMenuHeader"
|
||||
})}
|
||||
|
@ -239,7 +239,7 @@ const MenuList: React.FC<MenuListProps> = ({ params }) => {
|
|||
}
|
||||
variant="delete"
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete menus",
|
||||
defaultMessage: "Delete Menus",
|
||||
description: "dialog header",
|
||||
id: "menuListDeleteMenusHeader"
|
||||
})}
|
||||
|
|
|
@ -83,11 +83,11 @@ const OrderAddressEditDialog = withStyles(styles, {
|
|||
<DialogTitle>
|
||||
{variant === "billing"
|
||||
? intl.formatMessage({
|
||||
defaultMessage: "Edit billing address",
|
||||
defaultMessage: "Edit Billing Address",
|
||||
description: "dialog header"
|
||||
})
|
||||
: intl.formatMessage({
|
||||
defaultMessage: "Edit shipping address",
|
||||
defaultMessage: "Edit Shipping Address",
|
||||
description: "dialog header"
|
||||
})}
|
||||
</DialogTitle>
|
||||
|
|
|
@ -67,7 +67,7 @@ const OrderCancelDialog = withStyles(styles, { name: "OrderCancelDialog" })(
|
|||
<>
|
||||
<DialogTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel order"
|
||||
defaultMessage="Cancel Order"
|
||||
description="dialog header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
|
|
|
@ -187,7 +187,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
|
|||
<div className={classes.sectionHeader}>
|
||||
<Typography className={classes.sectionHeaderTitle}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Contact information"
|
||||
defaultMessage="Contact Information"
|
||||
description="subheader"
|
||||
/>
|
||||
</Typography>
|
||||
|
|
|
@ -79,7 +79,7 @@ const OrderCustomerEditDialog = withStyles(styles, {
|
|||
<Dialog onClose={onClose} open={open} classes={{ paper: classes.dialog }}>
|
||||
<DialogTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit customer details"
|
||||
defaultMessage="Edit Customer Details"
|
||||
description="dialog header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
|
|
|
@ -25,7 +25,7 @@ const OrderDraftCancelDialog: React.StatelessComponent<
|
|||
onConfirm={onConfirm}
|
||||
open={open}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete draft order",
|
||||
defaultMessage: "Delete Daft Order",
|
||||
description: "dialog header"
|
||||
})}
|
||||
variant="delete"
|
||||
|
|
|
@ -64,7 +64,7 @@ const OrderDraftFinalizeDialog: React.StatelessComponent<
|
|||
onConfirm={onConfirm}
|
||||
open={open}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Finalize draft order",
|
||||
defaultMessage: "Finalize Draft Order",
|
||||
description: "dialog header"
|
||||
})}
|
||||
confirmButtonLabel={
|
||||
|
|
|
@ -120,7 +120,7 @@ const OrderFulfillmentDialog = withStyles(styles, {
|
|||
<>
|
||||
<DialogTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Fulfill products"
|
||||
defaultMessage="Fulfill Oroducts"
|
||||
description="dialog header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
|
|
|
@ -37,7 +37,7 @@ const OrderFulfillmentTrackingDialog: React.StatelessComponent<
|
|||
<>
|
||||
<DialogTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add tracking code"
|
||||
defaultMessage="Add Tracking Code"
|
||||
description="dialog header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
|
|
|
@ -43,7 +43,7 @@ export const PageCreate: React.StatelessComponent<PageCreateProps> = () => {
|
|||
<>
|
||||
<WindowTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Create page",
|
||||
defaultMessage: "Create Page",
|
||||
description: "header"
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -60,6 +60,7 @@ export interface ProductUpdatePageProps extends ListActions {
|
|||
saveButtonBarState: ConfirmButtonTransitionState;
|
||||
fetchCategories: (query: string) => void;
|
||||
fetchCollections: (query: string) => void;
|
||||
onVariantsAdd: () => void;
|
||||
onVariantShow: (id: string) => () => void;
|
||||
onImageDelete: (id: string) => () => void;
|
||||
onBack?();
|
||||
|
@ -100,6 +101,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
onSeoClick,
|
||||
onSubmit,
|
||||
onVariantAdd,
|
||||
onVariantsAdd,
|
||||
onVariantShow,
|
||||
isChecked,
|
||||
selected,
|
||||
|
@ -236,6 +238,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
fallbackPrice={product ? product.basePrice : undefined}
|
||||
onRowClick={onVariantShow}
|
||||
onVariantAdd={onVariantAdd}
|
||||
onVariantsAdd={onVariantsAdd}
|
||||
toolbar={toolbar}
|
||||
isChecked={isChecked}
|
||||
selected={selected}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { attributes } from "@saleor/attributes/fixtures";
|
||||
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||
import { ProductErrorCode } from "@saleor/types/globalTypes";
|
||||
import Decorator from "../../../storybook/Decorator";
|
||||
import { createVariants } from "./createVariants";
|
||||
import { AllOrAttribute } from "./form";
|
||||
import ProductVariantCreateContent, {
|
||||
ProductVariantCreateContentProps
|
||||
} from "./ProductVariantCreateContent";
|
||||
import ProductVariantCreateDialog from "./ProductVariantCreateDialog";
|
||||
|
||||
const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
|
||||
|
||||
const price: AllOrAttribute = {
|
||||
all: false,
|
||||
attribute: selectedAttributes[1].id,
|
||||
value: "2.79",
|
||||
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
|
||||
slug: attribute.slug,
|
||||
value: (attributeIndex + 4).toFixed(2)
|
||||
}))
|
||||
};
|
||||
|
||||
const stock: AllOrAttribute = {
|
||||
all: false,
|
||||
attribute: selectedAttributes[1].id,
|
||||
value: "8",
|
||||
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
|
||||
slug: attribute.slug,
|
||||
value: (selectedAttributes.length * 10 - attributeIndex).toString()
|
||||
}))
|
||||
};
|
||||
|
||||
const dataAttributes = selectedAttributes.map(attribute => ({
|
||||
id: attribute.id,
|
||||
values: attribute.values
|
||||
.map(value => value.slug)
|
||||
.filter((_, valueIndex) => valueIndex % 2 !== 1)
|
||||
}));
|
||||
|
||||
const errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] = [
|
||||
{
|
||||
__typename: "BulkProductError",
|
||||
code: ProductErrorCode.UNIQUE,
|
||||
field: "sku",
|
||||
index: 3,
|
||||
message: "Duplicated SKU."
|
||||
}
|
||||
];
|
||||
|
||||
const props: ProductVariantCreateContentProps = {
|
||||
attributes,
|
||||
currencySymbol: "USD",
|
||||
data: {
|
||||
attributes: dataAttributes,
|
||||
price,
|
||||
stock,
|
||||
variants: createVariants({
|
||||
attributes: dataAttributes,
|
||||
price,
|
||||
stock,
|
||||
variants: []
|
||||
})
|
||||
},
|
||||
dispatchFormDataAction: () => undefined,
|
||||
errors: [],
|
||||
onStepClick: () => undefined,
|
||||
step: "values"
|
||||
};
|
||||
|
||||
storiesOf("Views / Products / Create multiple variants", module)
|
||||
.addDecorator(storyFn => (
|
||||
<Card
|
||||
style={{
|
||||
margin: "auto",
|
||||
overflow: "visible",
|
||||
width: 800
|
||||
}}
|
||||
>
|
||||
<CardContent>{storyFn()}</CardContent>
|
||||
</Card>
|
||||
))
|
||||
.addDecorator(Decorator)
|
||||
.add("choose values", () => <ProductVariantCreateContent {...props} />)
|
||||
.add("prices and SKU", () => (
|
||||
<ProductVariantCreateContent {...props} step="prices" />
|
||||
));
|
||||
|
||||
storiesOf("Views / Products / Create multiple variants / summary", module)
|
||||
.addDecorator(storyFn => (
|
||||
<Card
|
||||
style={{
|
||||
margin: "auto",
|
||||
overflow: "visible",
|
||||
width: 800
|
||||
}}
|
||||
>
|
||||
<CardContent>{storyFn()}</CardContent>
|
||||
</Card>
|
||||
))
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => (
|
||||
<ProductVariantCreateContent {...props} step="summary" />
|
||||
))
|
||||
.add("errors", () => (
|
||||
<ProductVariantCreateContent {...props} step="summary" errors={errors} />
|
||||
));
|
||||
|
||||
storiesOf("Views / Products / Create multiple variants", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("interactive", () => (
|
||||
<ProductVariantCreateDialog
|
||||
{...props}
|
||||
defaultPrice="10.99"
|
||||
open={true}
|
||||
onClose={() => undefined}
|
||||
onSubmit={() => undefined}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,147 @@
|
|||
import { Theme } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
|
||||
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||
import { isSelected } from "@saleor/utils/lists";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
import ProductVariantCreatePrices from "./ProductVariantCreatePrices";
|
||||
import ProductVariantCreateSummary from "./ProductVariantCreateSummary";
|
||||
import ProductVariantCreateTabs from "./ProductVariantCreateTabs";
|
||||
import ProductVariantCreateValues from "./ProductVariantCreateValues";
|
||||
import { ProductVariantCreateReducerAction } from "./reducer";
|
||||
import { ProductVariantCreateStep } from "./types";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
maxHeight: 400,
|
||||
overflowX: "hidden",
|
||||
overflowY: "scroll",
|
||||
paddingLeft: theme.spacing.unit * 3,
|
||||
paddingRight: theme.spacing.unit * 2,
|
||||
position: "relative",
|
||||
right: theme.spacing.unit * 3,
|
||||
width: `calc(100% + ${theme.spacing.unit * 3}px)`
|
||||
}
|
||||
}));
|
||||
|
||||
export interface ProductVariantCreateContentProps {
|
||||
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||
currencySymbol: string;
|
||||
data: ProductVariantCreateFormData;
|
||||
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
|
||||
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
|
||||
step: ProductVariantCreateStep;
|
||||
onStepClick: (step: ProductVariantCreateStep) => void;
|
||||
}
|
||||
|
||||
const ProductVariantCreateContent: React.FC<
|
||||
ProductVariantCreateContentProps
|
||||
> = props => {
|
||||
const {
|
||||
attributes,
|
||||
currencySymbol,
|
||||
data,
|
||||
dispatchFormDataAction,
|
||||
errors,
|
||||
step,
|
||||
onStepClick
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
const selectedAttributes = attributes.filter(attribute =>
|
||||
isSelected(
|
||||
attribute.id,
|
||||
data.attributes.map(dataAttribute => dataAttribute.id),
|
||||
(a, b) => a === b
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProductVariantCreateTabs step={step} onStepClick={onStepClick} />
|
||||
<div className={classes.root}>
|
||||
{step === "values" && (
|
||||
<ProductVariantCreateValues
|
||||
attributes={selectedAttributes}
|
||||
data={data}
|
||||
onValueClick={(attributeId, valueId) =>
|
||||
dispatchFormDataAction({
|
||||
attributeId,
|
||||
type: "selectValue",
|
||||
valueId
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{step === "prices" && (
|
||||
<ProductVariantCreatePrices
|
||||
attributes={selectedAttributes}
|
||||
currencySymbol={currencySymbol}
|
||||
data={data}
|
||||
onApplyPriceOrStockChange={(all, type) =>
|
||||
dispatchFormDataAction({
|
||||
all,
|
||||
type: type === "price" ? "applyPriceToAll" : "applyStockToAll"
|
||||
})
|
||||
}
|
||||
onApplyToAllChange={(value, type) =>
|
||||
dispatchFormDataAction({
|
||||
type:
|
||||
type === "price"
|
||||
? "changeApplyPriceToAllValue"
|
||||
: "changeApplyStockToAllValue",
|
||||
value
|
||||
})
|
||||
}
|
||||
onAttributeSelect={(attributeId, type) =>
|
||||
dispatchFormDataAction({
|
||||
attributeId,
|
||||
type:
|
||||
type === "price"
|
||||
? "changeApplyPriceToAttributeId"
|
||||
: "changeApplyStockToAttributeId"
|
||||
})
|
||||
}
|
||||
onAttributeValueChange={(valueId, value, type) =>
|
||||
dispatchFormDataAction({
|
||||
type:
|
||||
type === "price"
|
||||
? "changeAttributeValuePrice"
|
||||
: "changeAttributeValueStock",
|
||||
value,
|
||||
valueId
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{step === "summary" && (
|
||||
<ProductVariantCreateSummary
|
||||
attributes={selectedAttributes}
|
||||
currencySymbol={currencySymbol}
|
||||
data={data}
|
||||
errors={errors}
|
||||
onVariantDataChange={(variantIndex, field, value) =>
|
||||
dispatchFormDataAction({
|
||||
field,
|
||||
type: "changeVariantData",
|
||||
value,
|
||||
variantIndex
|
||||
})
|
||||
}
|
||||
onVariantDelete={variantIndex =>
|
||||
dispatchFormDataAction({
|
||||
type: "deleteVariant",
|
||||
variantIndex
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreateContent.displayName = "ProductVariantCreateContent";
|
||||
export default ProductVariantCreateContent;
|
|
@ -0,0 +1,214 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
|
||||
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
|
||||
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
|
||||
import { createInitialForm, ProductVariantCreateFormData } from "./form";
|
||||
import ProductVariantCreateContent, {
|
||||
ProductVariantCreateContentProps
|
||||
} from "./ProductVariantCreateContent";
|
||||
import reduceProductVariantCreateFormData from "./reducer";
|
||||
import { ProductVariantCreateStep } from "./types";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
button: {
|
||||
marginLeft: theme.spacing.unit * 2
|
||||
},
|
||||
content: {
|
||||
overflowX: "visible",
|
||||
overflowY: "hidden",
|
||||
width: 800
|
||||
},
|
||||
spacer: {
|
||||
flex: 1
|
||||
}
|
||||
}));
|
||||
|
||||
function canHitNext(
|
||||
step: ProductVariantCreateStep,
|
||||
data: ProductVariantCreateFormData
|
||||
): boolean {
|
||||
switch (step) {
|
||||
case "values":
|
||||
return data.attributes.every(attribute => attribute.values.length > 0);
|
||||
case "prices":
|
||||
if (data.price.all) {
|
||||
if (data.price.value === "") {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
data.price.attribute === "" ||
|
||||
data.price.values.some(attributeValue => attributeValue.value === "")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.stock.all) {
|
||||
if (data.stock.value === "") {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
data.stock.attribute === "" ||
|
||||
data.stock.values.some(attributeValue => attributeValue.value === "")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
case "summary":
|
||||
return data.variants.every(variant => variant.sku !== "");
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProductVariantCreateDialogProps
|
||||
extends Omit<
|
||||
ProductVariantCreateContentProps,
|
||||
"data" | "dispatchFormDataAction" | "step" | "onStepClick"
|
||||
> {
|
||||
defaultPrice: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: ProductVariantBulkCreateInput[]) => void;
|
||||
}
|
||||
|
||||
const ProductVariantCreateDialog: React.FC<
|
||||
ProductVariantCreateDialogProps
|
||||
> = props => {
|
||||
const {
|
||||
attributes,
|
||||
defaultPrice,
|
||||
errors: apiErrors,
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
...contentProps
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
const [step, setStep] = React.useState<ProductVariantCreateStep>("values");
|
||||
|
||||
function handleNextStep() {
|
||||
switch (step) {
|
||||
case "values":
|
||||
setStep("prices");
|
||||
break;
|
||||
case "prices":
|
||||
setStep("summary");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrevStep() {
|
||||
switch (step) {
|
||||
case "prices":
|
||||
setStep("values");
|
||||
break;
|
||||
case "summary":
|
||||
setStep("prices");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const [data, dispatchFormDataAction] = React.useReducer(
|
||||
reduceProductVariantCreateFormData,
|
||||
createInitialForm(attributes, defaultPrice)
|
||||
);
|
||||
|
||||
const reloadForm = () =>
|
||||
dispatchFormDataAction({
|
||||
data: createInitialForm(attributes, defaultPrice),
|
||||
type: "reload"
|
||||
});
|
||||
|
||||
React.useEffect(reloadForm, [attributes.length]);
|
||||
|
||||
useModalDialogOpen(open, {
|
||||
onClose: () => {
|
||||
reloadForm();
|
||||
setStep("values");
|
||||
}
|
||||
});
|
||||
|
||||
const errors = useModalDialogErrors(apiErrors, open);
|
||||
|
||||
return (
|
||||
<Dialog open={open} maxWidth="md">
|
||||
<DialogTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Assign Attribute"
|
||||
description="dialog header"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogContent className={classes.content}>
|
||||
<ProductVariantCreateContent
|
||||
{...contentProps}
|
||||
attributes={attributes}
|
||||
data={data}
|
||||
dispatchFormDataAction={dispatchFormDataAction}
|
||||
errors={errors}
|
||||
step={step}
|
||||
onStepClick={step => setStep(step)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button className={classes.button} onClick={onClose}>
|
||||
<FormattedMessage defaultMessage="Cancel" description="button" />
|
||||
</Button>
|
||||
<div className={classes.spacer} />
|
||||
{step !== "values" && (
|
||||
<Button
|
||||
className={classes.button}
|
||||
color="primary"
|
||||
onClick={handlePrevStep}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Previous"
|
||||
description="previous step, button"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{step !== "summary" ? (
|
||||
<Button
|
||||
className={classes.button}
|
||||
color="primary"
|
||||
disabled={!canHitNext(step, data)}
|
||||
variant="contained"
|
||||
onClick={handleNextStep}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Next" description="button" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classes.button}
|
||||
color="primary"
|
||||
disabled={!canHitNext(step, data)}
|
||||
variant="contained"
|
||||
onClick={() => onSubmit(data.variants)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create"
|
||||
description="create multiple variants, button"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog";
|
||||
export default ProductVariantCreateDialog;
|
|
@ -0,0 +1,313 @@
|
|||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import Grid from "@saleor/components/Grid";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import SingleSelectField from "@saleor/components/SingleSelectField";
|
||||
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
hr: {
|
||||
marginBottom: theme.spacing.unit,
|
||||
marginTop: theme.spacing.unit / 2
|
||||
},
|
||||
hrAttribute: {
|
||||
marginTop: theme.spacing.unit * 2
|
||||
},
|
||||
label: {
|
||||
alignSelf: "center"
|
||||
},
|
||||
shortInput: {
|
||||
width: "50%"
|
||||
}
|
||||
}));
|
||||
|
||||
export type PriceOrStock = "price" | "stock";
|
||||
export interface ProductVariantCreatePricesProps {
|
||||
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||
currencySymbol: string;
|
||||
data: ProductVariantCreateFormData;
|
||||
onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void;
|
||||
onApplyToAllChange: (value: string, type: PriceOrStock) => void;
|
||||
onAttributeSelect: (id: string, type: PriceOrStock) => void;
|
||||
onAttributeValueChange: (
|
||||
id: string,
|
||||
value: string,
|
||||
type: PriceOrStock
|
||||
) => void;
|
||||
}
|
||||
|
||||
const ProductVariantCreatePrices: React.FC<
|
||||
ProductVariantCreatePricesProps
|
||||
> = props => {
|
||||
const {
|
||||
attributes,
|
||||
currencySymbol,
|
||||
data,
|
||||
onApplyPriceOrStockChange,
|
||||
onApplyToAllChange,
|
||||
onAttributeSelect,
|
||||
onAttributeValueChange
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const attributeChoices = attributes.map(attribute => ({
|
||||
label: attribute.name,
|
||||
value: attribute.id
|
||||
}));
|
||||
const priceAttributeValues = data.price.all
|
||||
? null
|
||||
: data.price.attribute
|
||||
? attributes
|
||||
.find(attribute => attribute.id === data.price.attribute)
|
||||
.values.filter(value =>
|
||||
data.attributes
|
||||
.find(attribute => attribute.id === data.price.attribute)
|
||||
.values.includes(value.slug)
|
||||
)
|
||||
: [];
|
||||
const stockAttributeValues = data.stock.all
|
||||
? null
|
||||
: data.stock.attribute
|
||||
? attributes
|
||||
.find(attribute => attribute.id === data.stock.attribute)
|
||||
.values.filter(value =>
|
||||
data.attributes
|
||||
.find(attribute => attribute.id === data.stock.attribute)
|
||||
.values.includes(value.slug)
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography color="textSecondary" variant="h5">
|
||||
<FormattedMessage
|
||||
defaultMessage="Price"
|
||||
description="variant price, header"
|
||||
/>
|
||||
</Typography>
|
||||
<Hr className={classes.hr} />
|
||||
<RadioGroup value={data.price.all ? "applyToAll" : "applyToAttribute"}>
|
||||
<FormControlLabel
|
||||
value="applyToAll"
|
||||
control={<Radio color="primary" />}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Apply single price to all SKUs"
|
||||
})}
|
||||
onChange={() => onApplyPriceOrStockChange(true, "price")}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
className={classes.shortInput}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: currencySymbol
|
||||
}}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Price",
|
||||
id: "productVariantCreatePricesPriceInputLabel"
|
||||
})}
|
||||
value={data.price.value}
|
||||
onChange={event => onApplyToAllChange(event.target.value, "price")}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<FormControlLabel
|
||||
value="applyToAttribute"
|
||||
control={<Radio color="primary" />}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Apply unique prices by attribute to each SKU"
|
||||
})}
|
||||
onChange={() => onApplyPriceOrStockChange(false, "price")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
{!data.price.all && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Grid variant="uniform">
|
||||
<div className={classes.label}>
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose attribute"
|
||||
description="variant attribute"
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<SingleSelectField
|
||||
choices={attributeChoices}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Attribute",
|
||||
description: "variant attribute"
|
||||
})}
|
||||
value={data.price.attribute}
|
||||
onChange={event =>
|
||||
onAttributeSelect(event.target.value, "price")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Hr className={classes.hrAttribute} />
|
||||
{priceAttributeValues &&
|
||||
priceAttributeValues.map(attributeValue => (
|
||||
<React.Fragment key={attributeValue.id}>
|
||||
<FormSpacer />
|
||||
<Grid variant="uniform">
|
||||
<div className={classes.label}>
|
||||
<Typography>{attributeValue.name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Price",
|
||||
description: "variant price",
|
||||
id: "productVariantCreatePricesSetPricePlaceholder"
|
||||
})}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: currencySymbol
|
||||
}}
|
||||
fullWidth
|
||||
value={
|
||||
data.price.values.find(
|
||||
value => value.slug === attributeValue.slug
|
||||
).value
|
||||
}
|
||||
onChange={event =>
|
||||
onAttributeValueChange(
|
||||
attributeValue.slug,
|
||||
event.target.value,
|
||||
"price"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<FormSpacer />
|
||||
<Typography color="textSecondary" variant="h5">
|
||||
<FormattedMessage
|
||||
defaultMessage="Stock"
|
||||
description="variant stock, header"
|
||||
/>
|
||||
</Typography>
|
||||
<Hr className={classes.hr} />
|
||||
<RadioGroup value={data.stock.all ? "applyToAll" : "applyToAttribute"}>
|
||||
<FormControlLabel
|
||||
value="applyToAll"
|
||||
control={<Radio color="primary" />}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Apply single stock to all SKUs"
|
||||
})}
|
||||
onChange={() => onApplyPriceOrStockChange(true, "stock")}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
className={classes.shortInput}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Stock",
|
||||
id: "productVariantCreatePricesStockInputLabel"
|
||||
})}
|
||||
value={data.stock.value}
|
||||
onChange={event => onApplyToAllChange(event.target.value, "stock")}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<FormControlLabel
|
||||
value="applyToAttribute"
|
||||
control={<Radio color="primary" />}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Apply unique stock by attribute to each SKU"
|
||||
})}
|
||||
onChange={() => onApplyPriceOrStockChange(false, "stock")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
{!data.stock.all && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Grid variant="uniform">
|
||||
<div className={classes.label}>
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose attribute"
|
||||
description="variant attribute"
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<SingleSelectField
|
||||
choices={attributeChoices}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Attribute",
|
||||
description: "variant attribute"
|
||||
})}
|
||||
value={data.stock.attribute}
|
||||
onChange={event =>
|
||||
onAttributeSelect(event.target.value, "stock")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Hr className={classes.hrAttribute} />
|
||||
{stockAttributeValues &&
|
||||
stockAttributeValues.map(attributeValue => (
|
||||
<React.Fragment key={attributeValue.id}>
|
||||
<FormSpacer />
|
||||
<Grid variant="uniform">
|
||||
<div className={classes.label}>
|
||||
<Typography>{attributeValue.name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Stock",
|
||||
description: "variant stock",
|
||||
id: "productVariantCreatePricesSetStockPlaceholder"
|
||||
})}
|
||||
fullWidth
|
||||
value={
|
||||
data.stock.values.find(
|
||||
value => value.slug === attributeValue.slug
|
||||
).value
|
||||
}
|
||||
onChange={event =>
|
||||
onAttributeValueChange(
|
||||
attributeValue.slug,
|
||||
event.target.value,
|
||||
"stock"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices";
|
||||
export default ProductVariantCreatePrices;
|
|
@ -0,0 +1,301 @@
|
|||
import blue from "@material-ui/core/colors/blue";
|
||||
import cyan from "@material-ui/core/colors/cyan";
|
||||
import green from "@material-ui/core/colors/green";
|
||||
import purple from "@material-ui/core/colors/purple";
|
||||
import yellow from "@material-ui/core/colors/yellow";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import { maybe } from "@saleor/misc";
|
||||
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
|
||||
import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
import { VariantField } from "./reducer";
|
||||
|
||||
export interface ProductVariantCreateSummaryProps {
|
||||
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||
currencySymbol: string;
|
||||
data: ProductVariantCreateFormData;
|
||||
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
|
||||
onVariantDataChange: (
|
||||
variantIndex: number,
|
||||
field: VariantField,
|
||||
value: string
|
||||
) => void;
|
||||
onVariantDelete: (variantIndex: number) => void;
|
||||
}
|
||||
|
||||
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
attributeValue: {
|
||||
display: "inline-block",
|
||||
marginRight: theme.spacing.unit
|
||||
},
|
||||
col: {
|
||||
...theme.typography.body2,
|
||||
fontSize: 14,
|
||||
paddingLeft: theme.spacing.unit,
|
||||
paddingRight: theme.spacing.unit
|
||||
},
|
||||
colHeader: {
|
||||
...theme.typography.body2,
|
||||
fontSize: 14
|
||||
},
|
||||
colName: {
|
||||
"&&": {
|
||||
paddingLeft: "0 !important"
|
||||
},
|
||||
"&:not($colHeader)": {
|
||||
paddingTop: theme.spacing.unit * 2
|
||||
}
|
||||
},
|
||||
colPrice: {},
|
||||
colSku: {},
|
||||
colStock: {},
|
||||
delete: {
|
||||
marginTop: theme.spacing.unit / 2
|
||||
},
|
||||
errorRow: {},
|
||||
hr: {
|
||||
marginBottom: theme.spacing.unit,
|
||||
marginTop: theme.spacing.unit / 2
|
||||
},
|
||||
input: {
|
||||
"& input": {
|
||||
padding: "16px 12px 17px"
|
||||
},
|
||||
marginTop: theme.spacing.unit / 2
|
||||
},
|
||||
row: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 180px 120px 180px 64px",
|
||||
padding: `${theme.spacing.unit}px 0`
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "ProductVariantCreateSummary"
|
||||
}
|
||||
);
|
||||
|
||||
function getVariantName(
|
||||
variant: ProductVariantBulkCreateInput,
|
||||
attributes: ProductDetails_product_productType_variantAttributes[]
|
||||
): string[] {
|
||||
return attributes.reduce(
|
||||
(acc, attribute) => [
|
||||
...acc,
|
||||
attribute.values.find(
|
||||
value =>
|
||||
value.slug ===
|
||||
variant.attributes.find(
|
||||
variantAttribute => variantAttribute.id === attribute.id
|
||||
).values[0]
|
||||
).name
|
||||
],
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
const ProductVariantCreateSummary: React.FC<
|
||||
ProductVariantCreateSummaryProps
|
||||
> = props => {
|
||||
const {
|
||||
attributes,
|
||||
currencySymbol,
|
||||
data,
|
||||
errors,
|
||||
onVariantDataChange,
|
||||
onVariantDelete
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography color="textSecondary" variant="h5">
|
||||
<FormattedMessage
|
||||
defaultMessage="You will create variants below"
|
||||
description="header"
|
||||
/>
|
||||
</Typography>
|
||||
<Hr className={classes.hr} />
|
||||
<div>
|
||||
<div className={classes.row}>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.col,
|
||||
classes.colHeader,
|
||||
classes.colName
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Variant"
|
||||
description="variant name"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.col,
|
||||
classes.colHeader,
|
||||
classes.colPrice
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Price"
|
||||
description="variant price"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.col,
|
||||
classes.colHeader,
|
||||
classes.colStock
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Inventory"
|
||||
description="variant stock amount"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.col,
|
||||
classes.colHeader,
|
||||
classes.colSku
|
||||
)}
|
||||
>
|
||||
<FormattedMessage defaultMessage="SKU" />
|
||||
</div>
|
||||
</div>
|
||||
{data.variants.map((variant, variantIndex) => {
|
||||
const variantErrors = errors.filter(
|
||||
error => error.index === variantIndex
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(classes.row, {
|
||||
[classes.errorRow]: variantErrors.length > 0
|
||||
})}
|
||||
key={variant.attributes
|
||||
.map(attribute => attribute.values[0])
|
||||
.join(":")}
|
||||
>
|
||||
<div className={classNames(classes.col, classes.colName)}>
|
||||
{getVariantName(variant, attributes).map(
|
||||
(value, valueIndex) => (
|
||||
<span
|
||||
className={classes.attributeValue}
|
||||
style={{
|
||||
color: colors[valueIndex % colors.length]
|
||||
}}
|
||||
key={value}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames(classes.col, classes.colPrice)}>
|
||||
<TextField
|
||||
InputProps={{
|
||||
endAdornment: currencySymbol
|
||||
}}
|
||||
className={classes.input}
|
||||
error={
|
||||
!!variantErrors.find(
|
||||
error => error.field === "priceOverride"
|
||||
)
|
||||
}
|
||||
helperText={maybe(
|
||||
() =>
|
||||
variantErrors.find(
|
||||
error => error.field === "priceOverride"
|
||||
).message
|
||||
)}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
fullWidth
|
||||
value={variant.priceOverride}
|
||||
onChange={event =>
|
||||
onVariantDataChange(
|
||||
variantIndex,
|
||||
"price",
|
||||
event.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(classes.col, classes.colStock)}>
|
||||
<TextField
|
||||
className={classes.input}
|
||||
error={
|
||||
!!variantErrors.find(error => error.field === "quantity")
|
||||
}
|
||||
helperText={maybe(
|
||||
() =>
|
||||
variantErrors.find(error => error.field === "quantity")
|
||||
.message
|
||||
)}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
type: "number"
|
||||
}}
|
||||
fullWidth
|
||||
value={variant.quantity}
|
||||
onChange={event =>
|
||||
onVariantDataChange(
|
||||
variantIndex,
|
||||
"stock",
|
||||
event.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(classes.col, classes.colSku)}>
|
||||
<TextField
|
||||
className={classes.input}
|
||||
error={!!variantErrors.find(error => error.field === "sku")}
|
||||
helperText={maybe(
|
||||
() =>
|
||||
variantErrors.find(error => error.field === "sku").message
|
||||
)}
|
||||
fullWidth
|
||||
value={variant.sku}
|
||||
onChange={event =>
|
||||
onVariantDataChange(variantIndex, "sku", event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.col}>
|
||||
<IconButton
|
||||
className={classes.delete}
|
||||
color="primary"
|
||||
onClick={() => onVariantDelete(variantIndex)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary";
|
||||
export default ProductVariantCreateSummary;
|
|
@ -0,0 +1,109 @@
|
|||
import { Theme } from "@material-ui/core/styles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
|
||||
import { ProductVariantCreateStep } from "./types";
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
value: ProductVariantCreateStep;
|
||||
}
|
||||
function getSteps(intl: IntlShape): Step[] {
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Select Values",
|
||||
description: "attribute values, variant creation step"
|
||||
}),
|
||||
value: "values"
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Prices and SKU",
|
||||
description: "variant creation step"
|
||||
}),
|
||||
value: "prices"
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Summary",
|
||||
description: "variant creation step"
|
||||
}),
|
||||
value: "summary"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
label: {
|
||||
fontSize: 14,
|
||||
textAlign: "center"
|
||||
},
|
||||
root: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: theme.spacing.unit * 3
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
paddingBottom: theme.spacing.unit,
|
||||
userSelect: "none"
|
||||
},
|
||||
tabActive: {
|
||||
fontWeight: 600
|
||||
},
|
||||
tabVisited: {
|
||||
borderBottom: `3px solid ${theme.palette.primary.main}`,
|
||||
cursor: "pointer"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "ProductVariantCreateTabs"
|
||||
}
|
||||
);
|
||||
|
||||
export interface ProductVariantCreateTabsProps {
|
||||
step: ProductVariantCreateStep;
|
||||
onStepClick: (step: ProductVariantCreateStep) => void;
|
||||
}
|
||||
|
||||
const ProductVariantCreateTabs: React.FC<
|
||||
ProductVariantCreateTabsProps
|
||||
> = props => {
|
||||
const { step: currentStep, onStepClick } = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const steps = getSteps(intl);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{steps.map((step, stepIndex) => {
|
||||
const visitedStep =
|
||||
steps.findIndex(step => step.value === currentStep) >= stepIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(classes.tab, {
|
||||
[classes.tabActive]: step.value === currentStep,
|
||||
[classes.tabVisited]: visitedStep
|
||||
})}
|
||||
onClick={visitedStep ? () => onStepClick(step.value) : undefined}
|
||||
key={step.value}
|
||||
>
|
||||
<Typography className={classes.label} variant="caption">
|
||||
{step.label}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs";
|
||||
export default ProductVariantCreateTabs;
|
|
@ -0,0 +1,79 @@
|
|||
import { Theme } from "@material-ui/core/styles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import makeStyles from "@material-ui/styles/makeStyles";
|
||||
import React from "react";
|
||||
|
||||
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||
import Debounce from "@saleor/components/Debounce";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import { maybe } from "@saleor/misc";
|
||||
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||
import { isSelected } from "@saleor/utils/lists";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
|
||||
export interface ProductVariantCreateValuesProps {
|
||||
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||
data: ProductVariantCreateFormData;
|
||||
onValueClick: (attributeId: string, valueId: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
hr: {
|
||||
marginBottom: theme.spacing.unit,
|
||||
marginTop: theme.spacing.unit / 2
|
||||
},
|
||||
valueContainer: {
|
||||
display: "grid",
|
||||
gridColumnGap: theme.spacing.unit * 3 + "px",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
marginBottom: theme.spacing.unit * 3
|
||||
}
|
||||
}));
|
||||
|
||||
const ProductVariantCreateValues: React.FC<
|
||||
ProductVariantCreateValuesProps
|
||||
> = props => {
|
||||
const { attributes, data, onValueClick } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{attributes.map(attribute => (
|
||||
<React.Fragment key={attribute.id}>
|
||||
<Typography color="textSecondary" variant="h5">
|
||||
{maybe<React.ReactNode>(() => attribute.name, <Skeleton />)}
|
||||
</Typography>
|
||||
<Hr className={classes.hr} />
|
||||
<div className={classes.valueContainer}>
|
||||
{attribute.values.map(value => (
|
||||
<Debounce
|
||||
debounceFn={() => onValueClick(attribute.id, value.slug)}
|
||||
time={100}
|
||||
key={value.slug}
|
||||
>
|
||||
{change => (
|
||||
<ControlledCheckbox
|
||||
checked={isSelected(
|
||||
value.slug,
|
||||
data.attributes.find(
|
||||
dataAttribute => attribute.id === dataAttribute.id
|
||||
).values,
|
||||
(a, b) => a === b
|
||||
)}
|
||||
name={`value:${value.slug}`}
|
||||
label={value.name}
|
||||
onChange={change}
|
||||
/>
|
||||
)}
|
||||
</Debounce>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProductVariantCreateValues.displayName = "ProductVariantCreateValues";
|
||||
export default ProductVariantCreateValues;
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,220 @@
|
|||
import {
|
||||
createVariantFlatMatrixDimension,
|
||||
createVariants
|
||||
} from "./createVariants";
|
||||
import { attributes, thirdStep } from "./fixtures";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
|
||||
describe("Creates variant matrix", () => {
|
||||
it("with proper size", () => {
|
||||
const attributes = thirdStep.attributes;
|
||||
|
||||
const matrix = createVariantFlatMatrixDimension([[]], attributes);
|
||||
|
||||
expect(matrix).toHaveLength(
|
||||
attributes.reduce((acc, attribute) => acc * attribute.values.length, 1)
|
||||
);
|
||||
});
|
||||
|
||||
it("with constant price and stock", () => {
|
||||
const price = "49.99";
|
||||
const stock = 80;
|
||||
|
||||
const data: ProductVariantCreateFormData = {
|
||||
...thirdStep,
|
||||
price: {
|
||||
...thirdStep.price,
|
||||
all: true,
|
||||
value: price
|
||||
},
|
||||
stock: {
|
||||
...thirdStep.stock,
|
||||
all: true,
|
||||
value: stock.toString()
|
||||
}
|
||||
};
|
||||
|
||||
const variants = createVariants(data);
|
||||
expect(variants).toHaveLength(
|
||||
thirdStep.attributes.reduce(
|
||||
(acc, attribute) => acc * attribute.values.length,
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
variants.forEach(variant => {
|
||||
expect(variant.priceOverride).toBe(price);
|
||||
expect(variant.quantity).toBe(stock);
|
||||
});
|
||||
});
|
||||
|
||||
it("with constant stock and attribute dependent price", () => {
|
||||
const price = 49.99;
|
||||
const stock = 80;
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.id === thirdStep.attributes[0].id
|
||||
);
|
||||
|
||||
const data: ProductVariantCreateFormData = {
|
||||
...thirdStep,
|
||||
price: {
|
||||
...thirdStep.price,
|
||||
all: false,
|
||||
attribute: attribute.id,
|
||||
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||
slug: attributeValue,
|
||||
value: (price * (attributeValueIndex + 1)).toString()
|
||||
}))
|
||||
},
|
||||
stock: {
|
||||
...thirdStep.stock,
|
||||
all: true,
|
||||
value: stock.toString()
|
||||
}
|
||||
};
|
||||
|
||||
const variants = createVariants(data);
|
||||
expect(variants).toHaveLength(
|
||||
thirdStep.attributes.reduce(
|
||||
(acc, attribute) => acc * attribute.values.length,
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
variants.forEach(variant => {
|
||||
expect(variant.quantity).toBe(stock);
|
||||
});
|
||||
|
||||
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||
variants
|
||||
.filter(
|
||||
variant =>
|
||||
variant.attributes.find(
|
||||
variantAttribute => variantAttribute.id === attribute.id
|
||||
).values[0] === attributeValue
|
||||
)
|
||||
.forEach(variant => {
|
||||
expect(variant.priceOverride).toBe(
|
||||
(price * (attributeValueIndex + 1)).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("with constant price and attribute dependent stock", () => {
|
||||
const price = "49.99";
|
||||
const stock = 80;
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.id === thirdStep.attributes[0].id
|
||||
);
|
||||
|
||||
const data: ProductVariantCreateFormData = {
|
||||
...thirdStep,
|
||||
price: {
|
||||
...thirdStep.price,
|
||||
all: true,
|
||||
value: price
|
||||
},
|
||||
stock: {
|
||||
...thirdStep.stock,
|
||||
all: false,
|
||||
attribute: attribute.id,
|
||||
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||
slug: attributeValue,
|
||||
value: (stock * (attributeValueIndex + 1)).toString()
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const variants = createVariants(data);
|
||||
expect(variants).toHaveLength(
|
||||
thirdStep.attributes.reduce(
|
||||
(acc, attribute) => acc * attribute.values.length,
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
variants.forEach(variant => {
|
||||
expect(variant.priceOverride).toBe(price);
|
||||
});
|
||||
|
||||
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||
variants
|
||||
.filter(
|
||||
variant =>
|
||||
variant.attributes.find(
|
||||
variantAttribute => variantAttribute.id === attribute.id
|
||||
).values[0] === attributeValue
|
||||
)
|
||||
.forEach(variant => {
|
||||
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("with attribute dependent price and stock", () => {
|
||||
const price = 49.99;
|
||||
const stock = 80;
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.id === thirdStep.attributes[0].id
|
||||
);
|
||||
|
||||
const data: ProductVariantCreateFormData = {
|
||||
...thirdStep,
|
||||
price: {
|
||||
...thirdStep.price,
|
||||
all: false,
|
||||
attribute: attribute.id,
|
||||
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||
slug: attributeValue,
|
||||
value: (price * (attributeValueIndex + 1)).toString()
|
||||
}))
|
||||
},
|
||||
stock: {
|
||||
...thirdStep.stock,
|
||||
all: false,
|
||||
attribute: attribute.id,
|
||||
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||
slug: attributeValue,
|
||||
value: (stock * (attributeValueIndex + 1)).toString()
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const variants = createVariants(data);
|
||||
expect(variants).toHaveLength(
|
||||
thirdStep.attributes.reduce(
|
||||
(acc, attribute) => acc * attribute.values.length,
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||
variants
|
||||
.filter(
|
||||
variant =>
|
||||
variant.attributes.find(
|
||||
variantAttribute => variantAttribute.id === attribute.id
|
||||
).values[0] === attributeValue
|
||||
)
|
||||
.forEach(variant => {
|
||||
expect(variant.priceOverride).toBe(
|
||||
(price * (attributeValueIndex + 1)).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||
variants
|
||||
.filter(
|
||||
variant =>
|
||||
variant.attributes.find(
|
||||
variantAttribute => variantAttribute.id === attribute.id
|
||||
).values[0] === attributeValue
|
||||
)
|
||||
.forEach(variant => {
|
||||
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
|
||||
import {
|
||||
AllOrAttribute,
|
||||
Attribute,
|
||||
ProductVariantCreateFormData
|
||||
} from "./form";
|
||||
|
||||
interface CreateVariantAttributeValueInput {
|
||||
attributeId: string;
|
||||
attributeValueSlug: string;
|
||||
}
|
||||
type CreateVariantInput = CreateVariantAttributeValueInput[];
|
||||
|
||||
function getAttributeValuePriceOrStock(
|
||||
attributes: CreateVariantInput,
|
||||
priceOrStock: AllOrAttribute
|
||||
): string {
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.attributeId === priceOrStock.attribute
|
||||
);
|
||||
|
||||
const attributeValue = priceOrStock.values.find(
|
||||
attributeValue => attribute.attributeValueSlug === attributeValue.slug
|
||||
);
|
||||
|
||||
return attributeValue.value;
|
||||
}
|
||||
|
||||
function createVariant(
|
||||
data: ProductVariantCreateFormData,
|
||||
attributes: CreateVariantInput
|
||||
): ProductVariantBulkCreateInput {
|
||||
const priceOverride = data.price.all
|
||||
? data.price.value
|
||||
: getAttributeValuePriceOrStock(attributes, data.price);
|
||||
const quantity = parseInt(
|
||||
data.stock.all
|
||||
? data.stock.value
|
||||
: getAttributeValuePriceOrStock(attributes, data.stock),
|
||||
10
|
||||
);
|
||||
|
||||
return {
|
||||
attributes: attributes.map(attribute => ({
|
||||
id: attribute.attributeId,
|
||||
values: [attribute.attributeValueSlug]
|
||||
})),
|
||||
priceOverride,
|
||||
quantity,
|
||||
sku: ""
|
||||
};
|
||||
}
|
||||
|
||||
function addAttributeToVariant(
|
||||
attribute: Attribute,
|
||||
variant: CreateVariantInput
|
||||
): CreateVariantInput[] {
|
||||
return attribute.values.map(attributeValueSlug => [
|
||||
...variant,
|
||||
{
|
||||
attributeId: attribute.id,
|
||||
attributeValueSlug
|
||||
}
|
||||
]);
|
||||
}
|
||||
function addVariantAttributeInput(
|
||||
data: CreateVariantInput[],
|
||||
attribute: Attribute
|
||||
): CreateVariantInput[] {
|
||||
const variants = data
|
||||
.map(variant => addAttributeToVariant(attribute, variant))
|
||||
.reduce((acc, variantInput) => [...acc, ...variantInput]);
|
||||
|
||||
return variants;
|
||||
}
|
||||
|
||||
export function createVariantFlatMatrixDimension(
|
||||
variants: CreateVariantInput[],
|
||||
attributes: Attribute[]
|
||||
): CreateVariantInput[] {
|
||||
if (attributes.length > 0) {
|
||||
return createVariantFlatMatrixDimension(
|
||||
addVariantAttributeInput(variants, attributes[0]),
|
||||
attributes.slice(1)
|
||||
);
|
||||
} else {
|
||||
return variants;
|
||||
}
|
||||
}
|
||||
|
||||
export function createVariants(
|
||||
data: ProductVariantCreateFormData
|
||||
): ProductVariantBulkCreateInput[] {
|
||||
if (
|
||||
(!data.price.all && !data.price.attribute) ||
|
||||
(!data.stock.all && !data.stock.attribute)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const variants = createVariantFlatMatrixDimension([[]], data.attributes).map(
|
||||
variant => createVariant(data, variant)
|
||||
);
|
||||
|
||||
return variants;
|
||||
}
|
110
src/products/components/ProductVariantCreateDialog/fixtures.ts
Normal file
110
src/products/components/ProductVariantCreateDialog/fixtures.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { createVariants } from "./createVariants";
|
||||
import {
|
||||
AllOrAttribute,
|
||||
createInitialForm,
|
||||
ProductVariantCreateFormData
|
||||
} from "./form";
|
||||
|
||||
export const attributes = [
|
||||
{
|
||||
id: "attr-1",
|
||||
values: Array(9)
|
||||
.fill(0)
|
||||
.map((_, index) => `val-1-${index + 1}`)
|
||||
},
|
||||
{
|
||||
id: "attr-2",
|
||||
values: Array(6)
|
||||
.fill(0)
|
||||
.map((_, index) => `val-2-${index + 1}`)
|
||||
},
|
||||
{
|
||||
id: "attr-3",
|
||||
values: Array(4)
|
||||
.fill(0)
|
||||
.map((_, index) => `val-3-${index + 1}`)
|
||||
},
|
||||
{
|
||||
id: "attr-4",
|
||||
values: Array(11)
|
||||
.fill(0)
|
||||
.map((_, index) => `val-4-${index + 1}`)
|
||||
}
|
||||
];
|
||||
|
||||
export const secondStep: ProductVariantCreateFormData = {
|
||||
...createInitialForm([], "10.99"),
|
||||
attributes: [
|
||||
{
|
||||
id: attributes[0].id,
|
||||
values: []
|
||||
},
|
||||
{
|
||||
id: attributes[1].id,
|
||||
values: []
|
||||
},
|
||||
{
|
||||
id: attributes[3].id,
|
||||
values: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const thirdStep: ProductVariantCreateFormData = {
|
||||
...secondStep,
|
||||
attributes: [
|
||||
{
|
||||
id: attributes[0].id,
|
||||
values: [0, 6].map(index => attributes[0].values[index])
|
||||
},
|
||||
{
|
||||
id: attributes[1].id,
|
||||
values: [1, 3].map(index => attributes[1].values[index])
|
||||
},
|
||||
{
|
||||
id: attributes[3].id,
|
||||
values: [0, 4].map(index => attributes[3].values[index])
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const price: AllOrAttribute = {
|
||||
all: false,
|
||||
attribute: thirdStep.attributes[1].id,
|
||||
value: "",
|
||||
values: [
|
||||
{
|
||||
slug: thirdStep.attributes[1].values[0],
|
||||
value: "24.99"
|
||||
},
|
||||
{
|
||||
slug: thirdStep.attributes[1].values[1],
|
||||
value: "26.99"
|
||||
}
|
||||
]
|
||||
};
|
||||
const stock: AllOrAttribute = {
|
||||
all: false,
|
||||
attribute: thirdStep.attributes[2].id,
|
||||
value: "",
|
||||
values: [
|
||||
{
|
||||
slug: thirdStep.attributes[2].values[0],
|
||||
value: "50"
|
||||
},
|
||||
{
|
||||
slug: thirdStep.attributes[2].values[1],
|
||||
value: "35"
|
||||
}
|
||||
]
|
||||
};
|
||||
export const fourthStep: ProductVariantCreateFormData = {
|
||||
...thirdStep,
|
||||
price,
|
||||
stock,
|
||||
variants: createVariants({
|
||||
...thirdStep,
|
||||
price,
|
||||
stock
|
||||
})
|
||||
};
|
46
src/products/components/ProductVariantCreateDialog/form.ts
Normal file
46
src/products/components/ProductVariantCreateDialog/form.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
|
||||
|
||||
export interface AttributeValue {
|
||||
slug: string;
|
||||
value: string;
|
||||
}
|
||||
export interface AllOrAttribute {
|
||||
all: boolean;
|
||||
attribute: string;
|
||||
value: string;
|
||||
values: AttributeValue[];
|
||||
}
|
||||
export interface Attribute {
|
||||
id: string;
|
||||
values: string[];
|
||||
}
|
||||
export interface ProductVariantCreateFormData {
|
||||
attributes: Attribute[];
|
||||
price: AllOrAttribute;
|
||||
stock: AllOrAttribute;
|
||||
variants: ProductVariantBulkCreateInput[];
|
||||
}
|
||||
|
||||
export const createInitialForm = (
|
||||
attributes: ProductDetails_product_productType_variantAttributes[],
|
||||
price: string
|
||||
): ProductVariantCreateFormData => ({
|
||||
attributes: attributes.map(attribute => ({
|
||||
id: attribute.id,
|
||||
values: []
|
||||
})),
|
||||
price: {
|
||||
all: true,
|
||||
attribute: undefined,
|
||||
value: price || "",
|
||||
values: []
|
||||
},
|
||||
stock: {
|
||||
all: true,
|
||||
attribute: undefined,
|
||||
value: "",
|
||||
values: []
|
||||
},
|
||||
variants: []
|
||||
});
|
|
@ -0,0 +1,185 @@
|
|||
import { attributes, fourthStep, secondStep, thirdStep } from "./fixtures";
|
||||
import reducer, { VariantField } from "./reducer";
|
||||
|
||||
function execActions<TState, TAction>(
|
||||
initialState: TState,
|
||||
reducer: (state: TState, action: TAction) => TState,
|
||||
actions: TAction[]
|
||||
): TState {
|
||||
return actions.reduce((acc, action) => reducer(acc, action), initialState);
|
||||
}
|
||||
|
||||
describe("Reducer is able to", () => {
|
||||
it("select attribute values", () => {
|
||||
const state = execActions(secondStep, reducer, [
|
||||
{
|
||||
attributeId: attributes[0].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[0].values[0]
|
||||
},
|
||||
{
|
||||
attributeId: attributes[0].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[0].values[6]
|
||||
},
|
||||
{
|
||||
attributeId: attributes[1].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[1].values[1]
|
||||
},
|
||||
{
|
||||
attributeId: attributes[1].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[1].values[3]
|
||||
},
|
||||
{
|
||||
attributeId: attributes[3].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[3].values[0]
|
||||
},
|
||||
{
|
||||
attributeId: attributes[3].id,
|
||||
type: "selectValue",
|
||||
valueId: attributes[3].values[4]
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.attributes[0].values).toHaveLength(2);
|
||||
expect(state.attributes[1].values).toHaveLength(2);
|
||||
expect(state.attributes[2].values).toHaveLength(2);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("select price for all variants", () => {
|
||||
const value = "45.99";
|
||||
const state = execActions(thirdStep, reducer, [
|
||||
{
|
||||
all: true,
|
||||
type: "applyPriceToAll"
|
||||
},
|
||||
{
|
||||
type: "changeApplyPriceToAllValue",
|
||||
value
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.price.all).toBeTruthy();
|
||||
expect(state.price.value).toBe(value);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("select stock for all variants", () => {
|
||||
const value = 45.99;
|
||||
const state = execActions(thirdStep, reducer, [
|
||||
{
|
||||
all: true,
|
||||
type: "applyStockToAll"
|
||||
},
|
||||
{
|
||||
type: "changeApplyStockToAllValue",
|
||||
value: value.toString()
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.stock.all).toBeTruthy();
|
||||
expect(state.stock.value).toBe(value.toString());
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("select price to each attribute value", () => {
|
||||
const attribute = thirdStep.attributes[0];
|
||||
const value = 45.99;
|
||||
const state = execActions(thirdStep, reducer, [
|
||||
{
|
||||
all: false,
|
||||
type: "applyPriceToAll"
|
||||
},
|
||||
{
|
||||
attributeId: attribute.id,
|
||||
type: "changeApplyPriceToAttributeId"
|
||||
},
|
||||
{
|
||||
type: "changeAttributeValuePrice",
|
||||
value: value.toString(),
|
||||
valueId: attribute.values[0]
|
||||
},
|
||||
{
|
||||
type: "changeAttributeValuePrice",
|
||||
value: (value + 6).toString(),
|
||||
valueId: attribute.values[1]
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.price.all).toBeFalsy();
|
||||
expect(state.price.values).toHaveLength(
|
||||
state.attributes.find(attribute => state.price.attribute === attribute.id)
|
||||
.values.length
|
||||
);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("select stock to each attribute value", () => {
|
||||
const attribute = thirdStep.attributes[0];
|
||||
const value = 13;
|
||||
const state = execActions(thirdStep, reducer, [
|
||||
{
|
||||
all: false,
|
||||
type: "applyStockToAll"
|
||||
},
|
||||
{
|
||||
attributeId: attribute.id,
|
||||
type: "changeApplyStockToAttributeId"
|
||||
},
|
||||
{
|
||||
type: "changeAttributeValueStock",
|
||||
value: value.toString(),
|
||||
valueId: attribute.values[0]
|
||||
},
|
||||
{
|
||||
type: "changeAttributeValueStock",
|
||||
value: (value + 6).toString(),
|
||||
valueId: attribute.values[1]
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.stock.all).toBeFalsy();
|
||||
expect(state.stock.values).toHaveLength(
|
||||
state.attributes.find(attribute => state.stock.attribute === attribute.id)
|
||||
.values.length
|
||||
);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("modify individual variant price", () => {
|
||||
const field: VariantField = "price";
|
||||
const value = "49.99";
|
||||
const variantIndex = 3;
|
||||
|
||||
const state = execActions(fourthStep, reducer, [
|
||||
{
|
||||
field,
|
||||
type: "changeVariantData",
|
||||
value,
|
||||
variantIndex
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.variants[variantIndex].priceOverride).toBe(value);
|
||||
expect(state.variants[variantIndex - 1].priceOverride).toBe(
|
||||
fourthStep.variants[variantIndex - 1].priceOverride
|
||||
);
|
||||
});
|
||||
|
||||
it("delete variant", () => {
|
||||
const variantIndex = 3;
|
||||
|
||||
const state = execActions(fourthStep, reducer, [
|
||||
{
|
||||
type: "deleteVariant",
|
||||
variantIndex
|
||||
}
|
||||
]);
|
||||
|
||||
expect(state.variants.length).toBe(fourthStep.variants.length - 1);
|
||||
});
|
||||
});
|
363
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal file
363
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal file
|
@ -0,0 +1,363 @@
|
|||
//#region
|
||||
import {
|
||||
add,
|
||||
remove,
|
||||
removeAtIndex,
|
||||
toggle,
|
||||
updateAtIndex
|
||||
} from "@saleor/utils/lists";
|
||||
import { createVariants } from "./createVariants";
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
|
||||
export type ProductVariantCreateReducerActionType =
|
||||
| "applyPriceToAll"
|
||||
| "applyPriceToAttribute"
|
||||
| "applyStockToAll"
|
||||
| "applyStockToAttribute"
|
||||
| "changeApplyPriceToAllValue"
|
||||
| "changeApplyPriceToAttributeId"
|
||||
| "changeApplyStockToAllValue"
|
||||
| "changeApplyStockToAttributeId"
|
||||
| "changeAttributeValuePrice"
|
||||
| "changeAttributeValueStock"
|
||||
| "changeVariantData"
|
||||
| "deleteVariant"
|
||||
| "reload"
|
||||
| "selectValue";
|
||||
|
||||
export type VariantField = "stock" | "price" | "sku";
|
||||
export interface ProductVariantCreateReducerAction {
|
||||
all?: boolean;
|
||||
attributeId?: string;
|
||||
data?: ProductVariantCreateFormData;
|
||||
field?: VariantField;
|
||||
type: ProductVariantCreateReducerActionType;
|
||||
value?: string;
|
||||
valueId?: string;
|
||||
variantIndex?: number;
|
||||
}
|
||||
//#endregion
|
||||
function selectValue(
|
||||
prevState: ProductVariantCreateFormData,
|
||||
attributeId: string,
|
||||
valueSlug: string
|
||||
): ProductVariantCreateFormData {
|
||||
const attribute = prevState.attributes.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
const values = toggle(valueSlug, attribute.values, (a, b) => a === b);
|
||||
const updatedAttributes = add(
|
||||
{
|
||||
id: attributeId,
|
||||
values
|
||||
},
|
||||
remove(attribute, prevState.attributes, (a, b) => a.id === b.id)
|
||||
);
|
||||
|
||||
const priceValues =
|
||||
prevState.price.attribute === attributeId
|
||||
? toggle(
|
||||
{
|
||||
slug: valueSlug,
|
||||
value: ""
|
||||
},
|
||||
prevState.price.values,
|
||||
(a, b) => a.slug === b.slug
|
||||
)
|
||||
: prevState.price.values;
|
||||
|
||||
const stockValues =
|
||||
prevState.stock.attribute === attributeId
|
||||
? toggle(
|
||||
{
|
||||
slug: valueSlug,
|
||||
value: ""
|
||||
},
|
||||
prevState.stock.values,
|
||||
(a, b) => a.slug === b.slug
|
||||
)
|
||||
: prevState.stock.values;
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
attributes: updatedAttributes,
|
||||
price: {
|
||||
...prevState.price,
|
||||
values: priceValues
|
||||
},
|
||||
stock: {
|
||||
...prevState.stock,
|
||||
values: stockValues
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function applyPriceToAll(
|
||||
state: ProductVariantCreateFormData,
|
||||
value: boolean
|
||||
): ProductVariantCreateFormData {
|
||||
const data = {
|
||||
...state,
|
||||
price: {
|
||||
...state.price,
|
||||
all: value
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function applyStockToAll(
|
||||
state: ProductVariantCreateFormData,
|
||||
value: boolean
|
||||
): ProductVariantCreateFormData {
|
||||
const data = {
|
||||
...state,
|
||||
stock: {
|
||||
...state.stock,
|
||||
all: value
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeAttributeValuePrice(
|
||||
state: ProductVariantCreateFormData,
|
||||
attributeValueSlug: string,
|
||||
price: string
|
||||
): ProductVariantCreateFormData {
|
||||
const index = state.price.values.findIndex(
|
||||
value => value.slug === attributeValueSlug
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Value with id ${attributeValueSlug} not found`);
|
||||
}
|
||||
|
||||
const values = updateAtIndex(
|
||||
{
|
||||
slug: attributeValueSlug,
|
||||
value: price
|
||||
},
|
||||
state.price.values,
|
||||
index
|
||||
);
|
||||
|
||||
const data = {
|
||||
...state,
|
||||
price: {
|
||||
...state.price,
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeAttributeValueStock(
|
||||
state: ProductVariantCreateFormData,
|
||||
attributeValueSlug: string,
|
||||
stock: string
|
||||
): ProductVariantCreateFormData {
|
||||
const index = state.stock.values.findIndex(
|
||||
value => value.slug === attributeValueSlug
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Value with id ${attributeValueSlug} not found`);
|
||||
}
|
||||
|
||||
const values = updateAtIndex(
|
||||
{
|
||||
slug: attributeValueSlug,
|
||||
value: stock
|
||||
},
|
||||
state.stock.values,
|
||||
index
|
||||
);
|
||||
|
||||
const data = {
|
||||
...state,
|
||||
stock: {
|
||||
...state.stock,
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeApplyPriceToAttributeId(
|
||||
state: ProductVariantCreateFormData,
|
||||
attributeId: string
|
||||
): ProductVariantCreateFormData {
|
||||
const attribute = state.attributes.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
const values = attribute.values.map(slug => ({
|
||||
slug,
|
||||
value: ""
|
||||
}));
|
||||
const data = {
|
||||
...state,
|
||||
price: {
|
||||
...state.price,
|
||||
attribute: attributeId,
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeApplyStockToAttributeId(
|
||||
state: ProductVariantCreateFormData,
|
||||
attributeId: string
|
||||
): ProductVariantCreateFormData {
|
||||
const attribute = state.attributes.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
const values = attribute.values.map(slug => ({
|
||||
slug,
|
||||
value: ""
|
||||
}));
|
||||
|
||||
const data = {
|
||||
...state,
|
||||
stock: {
|
||||
...state.stock,
|
||||
attribute: attributeId,
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeApplyPriceToAllValue(
|
||||
state: ProductVariantCreateFormData,
|
||||
value: string
|
||||
): ProductVariantCreateFormData {
|
||||
const data = {
|
||||
...state,
|
||||
price: {
|
||||
...state.price,
|
||||
value
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeApplyStockToAllValue(
|
||||
state: ProductVariantCreateFormData,
|
||||
value: string
|
||||
): ProductVariantCreateFormData {
|
||||
const data = {
|
||||
...state,
|
||||
stock: {
|
||||
...state.stock,
|
||||
value
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
variants: createVariants(data)
|
||||
};
|
||||
}
|
||||
|
||||
function changeVariantData(
|
||||
state: ProductVariantCreateFormData,
|
||||
field: VariantField,
|
||||
value: string,
|
||||
variantIndex: number
|
||||
): ProductVariantCreateFormData {
|
||||
const variant = state.variants[variantIndex];
|
||||
if (field === "price") {
|
||||
variant.priceOverride = value;
|
||||
} else if (field === "sku") {
|
||||
variant.sku = value;
|
||||
} else {
|
||||
variant.quantity = parseInt(value, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
variants: updateAtIndex(variant, state.variants, variantIndex)
|
||||
};
|
||||
}
|
||||
|
||||
function deleteVariant(
|
||||
state: ProductVariantCreateFormData,
|
||||
variantIndex: number
|
||||
): ProductVariantCreateFormData {
|
||||
return {
|
||||
...state,
|
||||
variants: removeAtIndex(state.variants, variantIndex)
|
||||
};
|
||||
}
|
||||
|
||||
function reduceProductVariantCreateFormData(
|
||||
prevState: ProductVariantCreateFormData,
|
||||
action: ProductVariantCreateReducerAction
|
||||
) {
|
||||
switch (action.type) {
|
||||
case "selectValue":
|
||||
return selectValue(prevState, action.attributeId, action.valueId);
|
||||
|
||||
case "applyPriceToAll":
|
||||
return applyPriceToAll(prevState, action.all);
|
||||
case "applyStockToAll":
|
||||
return applyStockToAll(prevState, action.all);
|
||||
case "changeAttributeValuePrice":
|
||||
return changeAttributeValuePrice(prevState, action.valueId, action.value);
|
||||
case "changeAttributeValueStock":
|
||||
return changeAttributeValueStock(prevState, action.valueId, action.value);
|
||||
case "changeApplyPriceToAttributeId":
|
||||
return changeApplyPriceToAttributeId(prevState, action.attributeId);
|
||||
case "changeApplyStockToAttributeId":
|
||||
return changeApplyStockToAttributeId(prevState, action.attributeId);
|
||||
case "changeApplyPriceToAllValue":
|
||||
return changeApplyPriceToAllValue(prevState, action.value);
|
||||
case "changeApplyStockToAllValue":
|
||||
return changeApplyStockToAllValue(prevState, action.value);
|
||||
case "changeVariantData":
|
||||
return changeVariantData(
|
||||
prevState,
|
||||
action.field,
|
||||
action.value,
|
||||
action.variantIndex
|
||||
);
|
||||
case "deleteVariant":
|
||||
return deleteVariant(prevState, action.variantIndex);
|
||||
case "reload":
|
||||
return action.data;
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
export default reduceProductVariantCreateFormData;
|
|
@ -0,0 +1 @@
|
|||
export type ProductVariantCreateStep = "values" | "prices" | "summary";
|
|
@ -69,6 +69,7 @@ interface ProductVariantsProps extends ListActions, WithStyles<typeof styles> {
|
|||
fallbackPrice?: ProductVariant_costPrice;
|
||||
onRowClick: (id: string) => () => void;
|
||||
onVariantAdd?();
|
||||
onVariantsAdd?();
|
||||
}
|
||||
|
||||
const numberOfColumns = 5;
|
||||
|
@ -81,6 +82,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
|||
fallbackPrice,
|
||||
onRowClick,
|
||||
onVariantAdd,
|
||||
onVariantsAdd,
|
||||
isChecked,
|
||||
selected,
|
||||
toggle,
|
||||
|
@ -98,7 +100,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
|||
description: "section header"
|
||||
})}
|
||||
toolbar={
|
||||
<>
|
||||
hasVariants ? (
|
||||
<Button
|
||||
onClick={onVariantAdd}
|
||||
variant="text"
|
||||
|
@ -110,7 +112,19 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
|||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onVariantsAdd}
|
||||
variant="text"
|
||||
color="primary"
|
||||
data-tc="button-add-variants"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create variants"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{!variants.length && (
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
TypedProductImageCreateMutation,
|
||||
TypedProductImageDeleteMutation,
|
||||
TypedProductUpdateMutation,
|
||||
TypedProductVariantBulkCreateMutation,
|
||||
TypedProductVariantBulkDeleteMutation,
|
||||
TypedSimpleProductUpdateMutation
|
||||
} from "../mutations";
|
||||
|
@ -25,6 +26,10 @@ import {
|
|||
ProductImageReorderVariables
|
||||
} from "../types/ProductImageReorder";
|
||||
import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate";
|
||||
import {
|
||||
ProductVariantBulkCreate,
|
||||
ProductVariantBulkCreateVariables
|
||||
} from "../types/ProductVariantBulkCreate";
|
||||
import {
|
||||
ProductVariantBulkDelete,
|
||||
ProductVariantBulkDeleteVariables
|
||||
|
@ -38,6 +43,10 @@ import ProductImagesReorderProvider from "./ProductImagesReorder";
|
|||
interface ProductUpdateOperationsProps {
|
||||
product: ProductDetails_product;
|
||||
children: (props: {
|
||||
bulkProductVariantCreate: PartialMutationProviderOutput<
|
||||
ProductVariantBulkCreate,
|
||||
ProductVariantBulkCreateVariables
|
||||
>;
|
||||
bulkProductVariantDelete: PartialMutationProviderOutput<
|
||||
ProductVariantBulkDelete,
|
||||
ProductVariantBulkDeleteVariables
|
||||
|
@ -67,6 +76,7 @@ interface ProductUpdateOperationsProps {
|
|||
SimpleProductUpdateVariables
|
||||
>;
|
||||
}) => React.ReactNode;
|
||||
onBulkProductVariantCreate?: (data: ProductVariantBulkCreate) => void;
|
||||
onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void;
|
||||
onDelete?: (data: ProductDelete) => void;
|
||||
onImageCreate?: (data: ProductImageCreate) => void;
|
||||
|
@ -80,6 +90,7 @@ const ProductUpdateOperations: React.StatelessComponent<
|
|||
> = ({
|
||||
product,
|
||||
children,
|
||||
onBulkProductVariantCreate,
|
||||
onBulkProductVariantDelete,
|
||||
onDelete,
|
||||
onImageDelete,
|
||||
|
@ -112,31 +123,40 @@ const ProductUpdateOperations: React.StatelessComponent<
|
|||
<TypedProductVariantBulkDeleteMutation
|
||||
onCompleted={onBulkProductVariantDelete}
|
||||
>
|
||||
{(...bulkProductVariantDelete) =>
|
||||
children({
|
||||
bulkProductVariantDelete: getMutationProviderData(
|
||||
...bulkProductVariantDelete
|
||||
),
|
||||
createProductImage: getMutationProviderData(
|
||||
...createProductImage
|
||||
),
|
||||
deleteProduct: getMutationProviderData(
|
||||
...deleteProduct
|
||||
),
|
||||
deleteProductImage: getMutationProviderData(
|
||||
...deleteProductImage
|
||||
),
|
||||
reorderProductImages: getMutationProviderData(
|
||||
...reorderProductImages
|
||||
),
|
||||
updateProduct: getMutationProviderData(
|
||||
...updateProduct
|
||||
),
|
||||
updateSimpleProduct: getMutationProviderData(
|
||||
...updateSimpleProduct
|
||||
)
|
||||
})
|
||||
}
|
||||
{(...bulkProductVariantDelete) => (
|
||||
<TypedProductVariantBulkCreateMutation
|
||||
onCompleted={onBulkProductVariantCreate}
|
||||
>
|
||||
{(...bulkProductVariantCreate) =>
|
||||
children({
|
||||
bulkProductVariantCreate: getMutationProviderData(
|
||||
...bulkProductVariantCreate
|
||||
),
|
||||
bulkProductVariantDelete: getMutationProviderData(
|
||||
...bulkProductVariantDelete
|
||||
),
|
||||
createProductImage: getMutationProviderData(
|
||||
...createProductImage
|
||||
),
|
||||
deleteProduct: getMutationProviderData(
|
||||
...deleteProduct
|
||||
),
|
||||
deleteProductImage: getMutationProviderData(
|
||||
...deleteProductImage
|
||||
),
|
||||
reorderProductImages: getMutationProviderData(
|
||||
...reorderProductImages
|
||||
),
|
||||
updateProduct: getMutationProviderData(
|
||||
...updateProduct
|
||||
),
|
||||
updateSimpleProduct: getMutationProviderData(
|
||||
...updateSimpleProduct
|
||||
)
|
||||
})
|
||||
}
|
||||
</TypedProductVariantBulkCreateMutation>
|
||||
)}
|
||||
</TypedProductVariantBulkDeleteMutation>
|
||||
)}
|
||||
</TypedSimpleProductUpdateMutation>
|
||||
|
|
|
@ -45,6 +45,10 @@ import {
|
|||
productBulkPublish,
|
||||
productBulkPublishVariables
|
||||
} from "./types/productBulkPublish";
|
||||
import {
|
||||
ProductVariantBulkCreate,
|
||||
ProductVariantBulkCreateVariables
|
||||
} from "./types/ProductVariantBulkCreate";
|
||||
import {
|
||||
ProductVariantBulkDelete,
|
||||
ProductVariantBulkDeleteVariables
|
||||
|
@ -319,26 +323,8 @@ export const TypedVariantUpdateMutation = TypedMutation<
|
|||
|
||||
export const variantCreateMutation = gql`
|
||||
${fragmentVariant}
|
||||
mutation VariantCreate(
|
||||
$attributes: [AttributeValueInput]!
|
||||
$costPrice: Decimal
|
||||
$priceOverride: Decimal
|
||||
$product: ID!
|
||||
$sku: String
|
||||
$quantity: Int
|
||||
$trackInventory: Boolean!
|
||||
) {
|
||||
productVariantCreate(
|
||||
input: {
|
||||
attributes: $attributes
|
||||
costPrice: $costPrice
|
||||
priceOverride: $priceOverride
|
||||
product: $product
|
||||
sku: $sku
|
||||
quantity: $quantity
|
||||
trackInventory: $trackInventory
|
||||
}
|
||||
) {
|
||||
mutation VariantCreate($input: ProductVariantCreateInput!) {
|
||||
productVariantCreate(input: $input) {
|
||||
errors {
|
||||
field
|
||||
message
|
||||
|
@ -458,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation<
|
|||
productBulkPublishVariables
|
||||
>(productBulkPublishMutation);
|
||||
|
||||
export const ProductVariantBulkCreateMutation = gql`
|
||||
mutation ProductVariantBulkCreate(
|
||||
$id: ID!
|
||||
$inputs: [ProductVariantBulkCreateInput]!
|
||||
) {
|
||||
productVariantBulkCreate(product: $id, variants: $inputs) {
|
||||
bulkProductErrors {
|
||||
field
|
||||
message
|
||||
code
|
||||
index
|
||||
}
|
||||
errors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const TypedProductVariantBulkCreateMutation = TypedMutation<
|
||||
ProductVariantBulkCreate,
|
||||
ProductVariantBulkCreateVariables
|
||||
>(ProductVariantBulkCreateMutation);
|
||||
|
||||
export const ProductVariantBulkDeleteMutation = gql`
|
||||
mutation ProductVariantBulkDelete($ids: [ID!]!) {
|
||||
productVariantBulkDelete(ids: $ids) {
|
||||
|
|
|
@ -260,6 +260,17 @@ const productDetailsQuery = gql`
|
|||
query ProductDetails($id: ID!) {
|
||||
product(id: $id) {
|
||||
...Product
|
||||
productType {
|
||||
variantAttributes {
|
||||
id
|
||||
name
|
||||
values {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -139,11 +139,26 @@ export interface ProductDetails_product_variants {
|
|||
stockQuantity: number;
|
||||
}
|
||||
|
||||
export interface ProductDetails_product_productType_variantAttributes_values {
|
||||
__typename: "AttributeValue";
|
||||
id: string;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
}
|
||||
|
||||
export interface ProductDetails_product_productType_variantAttributes {
|
||||
__typename: "Attribute";
|
||||
id: string;
|
||||
name: string | null;
|
||||
values: (ProductDetails_product_productType_variantAttributes_values | null)[] | null;
|
||||
}
|
||||
|
||||
export interface ProductDetails_product_productType {
|
||||
__typename: "ProductType";
|
||||
id: string;
|
||||
name: string;
|
||||
hasVariants: boolean;
|
||||
variantAttributes: (ProductDetails_product_productType_variantAttributes | null)[] | null;
|
||||
}
|
||||
|
||||
export interface ProductDetails_product {
|
||||
|
|
38
src/products/types/ProductVariantBulkCreate.ts
Normal file
38
src/products/types/ProductVariantBulkCreate.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { ProductVariantBulkCreateInput, ProductErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: ProductVariantBulkCreate
|
||||
// ====================================================
|
||||
|
||||
export interface ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors {
|
||||
__typename: "BulkProductError";
|
||||
field: string | null;
|
||||
message: string | null;
|
||||
code: ProductErrorCode | null;
|
||||
index: number | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantBulkCreate_productVariantBulkCreate_errors {
|
||||
__typename: "Error";
|
||||
field: string | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantBulkCreate_productVariantBulkCreate {
|
||||
__typename: "ProductVariantBulkCreate";
|
||||
bulkProductErrors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] | null;
|
||||
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantBulkCreate {
|
||||
productVariantBulkCreate: ProductVariantBulkCreate_productVariantBulkCreate | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantBulkCreateVariables {
|
||||
id: string;
|
||||
inputs: (ProductVariantBulkCreateInput | null)[];
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeValueInput } from "./../../types/globalTypes";
|
||||
import { ProductVariantCreateInput } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: VariantCreate
|
||||
|
@ -122,11 +122,5 @@ export interface VariantCreate {
|
|||
}
|
||||
|
||||
export interface VariantCreateVariables {
|
||||
attributes: (AttributeValueInput | null)[];
|
||||
costPrice?: any | null;
|
||||
priceOverride?: any | null;
|
||||
product: string;
|
||||
sku?: string | null;
|
||||
quantity?: number | null;
|
||||
trackInventory: boolean;
|
||||
input: ProductVariantCreateInput;
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
|
|||
export const productPath = (id: string) => urlJoin(productSection + id);
|
||||
export type ProductUrlDialog = "remove";
|
||||
export type ProductUrlQueryParams = BulkAction &
|
||||
Dialog<"remove" | "remove-variants">;
|
||||
Dialog<"create-variants" | "remove" | "remove-variants">;
|
||||
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
|
||||
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
|
|||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import useShop from "@saleor/hooks/useShop";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog";
|
||||
import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
|
||||
import SearchCategories from "../../../containers/SearchCategories";
|
||||
import SearchCollections from "../../../containers/SearchCollections";
|
||||
|
@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
params.ids
|
||||
);
|
||||
const intl = useIntl();
|
||||
const shop = useShop();
|
||||
|
||||
const openModal = (action: ProductUrlDialog) =>
|
||||
navigate(
|
||||
|
@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
const handleVariantAdd = () =>
|
||||
navigate(productVariantAddUrl(id));
|
||||
|
||||
const handleBulkProductVariantCreate = (
|
||||
data: ProductVariantBulkCreate
|
||||
) => {
|
||||
if (data.productVariantBulkCreate.errors.length === 0) {
|
||||
navigate(productUrl(id), true);
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkProductVariantDelete = (
|
||||
data: ProductVariantBulkDelete
|
||||
) => {
|
||||
|
@ -125,10 +138,19 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleVariantCreatorOpen = () =>
|
||||
navigate(
|
||||
productUrl(id, {
|
||||
...params,
|
||||
action: "create-variants"
|
||||
})
|
||||
);
|
||||
|
||||
const product = data ? data.product : undefined;
|
||||
return (
|
||||
<ProductUpdateOperations
|
||||
product={product}
|
||||
onBulkProductVariantCreate={handleBulkProductVariantCreate}
|
||||
onBulkProductVariantDelete={handleBulkProductVariantDelete}
|
||||
onDelete={handleDelete}
|
||||
onImageCreate={handleImageCreate}
|
||||
|
@ -136,6 +158,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
onUpdate={handleUpdate}
|
||||
>
|
||||
{({
|
||||
bulkProductVariantCreate,
|
||||
bulkProductVariantDelete,
|
||||
createProductImage,
|
||||
deleteProduct,
|
||||
|
@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
onImageReorder={handleImageReorder}
|
||||
onSubmit={handleSubmit}
|
||||
onVariantAdd={handleVariantAdd}
|
||||
onVariantsAdd={handleVariantCreatorOpen}
|
||||
onVariantShow={variantId => () =>
|
||||
navigate(
|
||||
productVariantEditUrl(product.id, variantId)
|
||||
|
@ -328,6 +352,37 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
/>
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
<ProductVariantCreateDialog
|
||||
defaultPrice={maybe(() =>
|
||||
data.product.basePrice.amount.toFixed(2)
|
||||
)}
|
||||
errors={maybe(
|
||||
() =>
|
||||
bulkProductVariantCreate.opts.data
|
||||
.productVariantBulkCreate.bulkProductErrors,
|
||||
[]
|
||||
)}
|
||||
open={params.action === "create-variants"}
|
||||
attributes={maybe(
|
||||
() => data.product.productType.variantAttributes,
|
||||
[]
|
||||
)}
|
||||
currencySymbol={maybe(() => shop.defaultCurrency)}
|
||||
onClose={() =>
|
||||
navigate(
|
||||
productUrl(id, {
|
||||
...params,
|
||||
action: undefined
|
||||
})
|
||||
)
|
||||
}
|
||||
onSubmit={inputs =>
|
||||
bulkProductVariantCreate.mutate({
|
||||
id,
|
||||
inputs
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -58,18 +58,20 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
|
|||
) =>
|
||||
variantCreate({
|
||||
variables: {
|
||||
attributes: formData.attributes
|
||||
.filter(attribute => attribute.value !== "")
|
||||
.map(attribute => ({
|
||||
id: attribute.id,
|
||||
values: [attribute.value]
|
||||
})),
|
||||
costPrice: decimal(formData.costPrice),
|
||||
priceOverride: decimal(formData.priceOverride),
|
||||
product: productId,
|
||||
quantity: formData.quantity || null,
|
||||
sku: formData.sku,
|
||||
trackInventory: true
|
||||
input: {
|
||||
attributes: formData.attributes
|
||||
.filter(attribute => attribute.value !== "")
|
||||
.map(attribute => ({
|
||||
id: attribute.id,
|
||||
values: [attribute.value]
|
||||
})),
|
||||
costPrice: decimal(formData.costPrice),
|
||||
priceOverride: decimal(formData.priceOverride),
|
||||
product: productId,
|
||||
quantity: formData.quantity || null,
|
||||
sku: formData.sku,
|
||||
trackInventory: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const handleVariantClick = (id: string) =>
|
||||
|
|
|
@ -81,7 +81,7 @@ const ShippingZonesList = withStyles(styles, { name: "ShippingZonesList" })(
|
|||
<CardTitle
|
||||
height="const"
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Shipping by zone",
|
||||
defaultMessage: "Shipping By Zone",
|
||||
description: "sort shipping methods by zone, section header"
|
||||
})}
|
||||
toolbar={
|
||||
|
|
|
@ -220,7 +220,7 @@ const ShippingZoneDetailsDialogs: React.StatelessComponent<
|
|||
}
|
||||
open={params.action === "unassign-country"}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete from shipping zone",
|
||||
defaultMessage: "Delete from Shipping Zone",
|
||||
description: "unassign country, dialog header"
|
||||
})}
|
||||
variant="delete"
|
||||
|
|
|
@ -49,7 +49,7 @@ const SiteSettingsAddress = withStyles(styles, { name: "SiteSettingsAddress" })(
|
|||
<Card className={classes.root}>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Store information",
|
||||
defaultMessage: "Store Information",
|
||||
description: "section header"
|
||||
})}
|
||||
/>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,6 +30,7 @@ const props: ProductUpdatePageProps = {
|
|||
onSubmit: () => undefined,
|
||||
onVariantAdd: () => undefined,
|
||||
onVariantShow: () => undefined,
|
||||
onVariantsAdd: () => undefined,
|
||||
placeholderImage,
|
||||
product,
|
||||
saveButtonBarState: "default",
|
||||
|
|
10
src/theme.ts
10
src/theme.ts
|
@ -60,7 +60,7 @@ export default (colors: IThemeColors): Theme =>
|
|||
},
|
||||
flat: {
|
||||
"& span": {
|
||||
color: colors.primary
|
||||
color: colors.font.gray
|
||||
}
|
||||
},
|
||||
flatPrimary: {
|
||||
|
@ -106,7 +106,7 @@ export default (colors: IThemeColors): Theme =>
|
|||
MuiFormControlLabel: {
|
||||
root: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "50px 6fr"
|
||||
gridTemplateColumns: "48px 1fr"
|
||||
}
|
||||
},
|
||||
MuiFormLabel: {
|
||||
|
@ -281,8 +281,7 @@ export default (colors: IThemeColors): Theme =>
|
|||
"& fieldset": {
|
||||
"&&:not($error)": {
|
||||
borderColor: colors.input.border
|
||||
},
|
||||
background: colors.background.paper
|
||||
}
|
||||
},
|
||||
"& legend": {
|
||||
display: "none"
|
||||
|
@ -454,7 +453,8 @@ export default (colors: IThemeColors): Theme =>
|
|||
ripple: {
|
||||
"&$rippleVisible": {
|
||||
backgroundColor: fade(colors.primary, 0.2)
|
||||
}
|
||||
},
|
||||
borderRadius: "100%"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -193,6 +193,20 @@ export enum PermissionEnum {
|
|||
MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS",
|
||||
}
|
||||
|
||||
export enum ProductErrorCode {
|
||||
ALREADY_EXISTS = "ALREADY_EXISTS",
|
||||
ATTRIBUTE_ALREADY_ASSIGNED = "ATTRIBUTE_ALREADY_ASSIGNED",
|
||||
ATTRIBUTE_CANNOT_BE_ASSIGNED = "ATTRIBUTE_CANNOT_BE_ASSIGNED",
|
||||
ATTRIBUTE_VARIANTS_DISABLED = "ATTRIBUTE_VARIANTS_DISABLED",
|
||||
GRAPHQL_ERROR = "GRAPHQL_ERROR",
|
||||
INVALID = "INVALID",
|
||||
NOT_FOUND = "NOT_FOUND",
|
||||
NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE",
|
||||
REQUIRED = "REQUIRED",
|
||||
UNIQUE = "UNIQUE",
|
||||
VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT",
|
||||
}
|
||||
|
||||
export enum ProductOrderField {
|
||||
DATE = "DATE",
|
||||
MINIMAL_PRICE = "MINIMAL_PRICE",
|
||||
|
@ -624,6 +638,27 @@ export interface ProductTypeInput {
|
|||
taxCode?: string | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantBulkCreateInput {
|
||||
attributes: (AttributeValueInput | null)[];
|
||||
costPrice?: any | null;
|
||||
priceOverride?: any | null;
|
||||
sku: string;
|
||||
quantity?: number | null;
|
||||
trackInventory?: boolean | null;
|
||||
weight?: any | null;
|
||||
}
|
||||
|
||||
export interface ProductVariantCreateInput {
|
||||
attributes: (AttributeValueInput | null)[];
|
||||
costPrice?: any | null;
|
||||
priceOverride?: any | null;
|
||||
sku?: string | null;
|
||||
quantity?: number | null;
|
||||
trackInventory?: boolean | null;
|
||||
weight?: any | null;
|
||||
product: string;
|
||||
}
|
||||
|
||||
export interface ProductVariantInput {
|
||||
attributes?: (AttributeValueInput | null)[] | null;
|
||||
costPrice?: any | null;
|
||||
|
|
|
@ -3,6 +3,7 @@ const path = require("path");
|
|||
const webpack = require("webpack");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
require('dotenv').config();
|
||||
|
||||
const resolve = path.resolve.bind(path, __dirname);
|
||||
|
||||
|
|
Loading…
Reference in a new issue