diff --git a/client/.babelrc b/client/.babelrc index d4150c1..90565b8 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -4,6 +4,7 @@ "@babel/preset-react" ], "plugins": [ - ["@babel/transform-runtime"] + [ "@babel/transform-runtime" ], + [ "@babel/plugin-proposal-class-properties" ] ] } \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 8d1216e..4e81389 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,5 +1,5 @@ { - "name": "gitea-apps", + "name": "gitea-kan", "version": "0.0.0", "lockfileVersion": 1, "requires": true, @@ -8,7 +8,6 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } @@ -71,7 +70,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.2.tgz", "integrity": "sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ==", - "dev": true, "requires": { "@babel/types": "^7.7.2", "jsesc": "^2.5.1", @@ -82,14 +80,12 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -97,7 +93,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.0.tgz", "integrity": "sha512-k50CQxMlYTYo+GGyUGFwpxKVtxVJi9yh61sXZji3zYHccK9RYliZGSTOgci85T+r+0VFN2nWbGM04PIqwfrpMg==", - "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -146,6 +141,165 @@ "@babel/types": "^7.7.0" } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz", + "integrity": "sha512-l+OnKACG4uiDHQ/aJT8dwpR+LhCJALxL0mJ6nzjB25e5IPwqV1VOsY7ah6UB1DG+VOXAIMtuC54rFJGiHkxjgA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4" + }, + "dependencies": { + "@babel/generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz", + "integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz", + "integrity": "sha512-9KcA1X2E3OjXl/ykfMMInBK+uVdfIVakVe7W7Lg3wfXUNyS3Q1HWLFRwZIjhqiCGbslummPDnmb7vIekS0C1vw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz", + "integrity": "sha512-VB7gWZ2fDkSuqW6b1AKXkJWO5NyNI3bFL/kK79/30moK57blr6NbH8xcl2XcKCwOmJosftWunZqfO84IGq3ZZg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz", + "integrity": "sha512-pP0tfgg9hsZWo5ZboYGuBn/bbYT/hdLPVSS4NMmiRJdwWhP0IznPwN9AE1JwyGsjSPLC364I0Qh5p+EPkGPNpg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/parser": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz", + "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==", + "dev": true + }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, "@babel/helper-create-regexp-features-plugin": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.2.tgz", @@ -212,7 +366,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz", "integrity": "sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.7.0", "@babel/template": "^7.7.0", @@ -223,7 +376,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz", "integrity": "sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw==", - "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -250,7 +402,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.0.tgz", "integrity": "sha512-Dv3hLKIC1jyfTkClvyEkYP2OlkzNvWs5+Q8WgPbxM5LMeorons7iPP91JM+DU7tRbhqA1ZeooPaMFvQrn23RHw==", - "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -332,7 +483,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz", "integrity": "sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA==", - "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -364,7 +514,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -375,7 +524,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -384,7 +532,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -395,7 +542,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -405,8 +551,7 @@ "@babel/parser": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.3.tgz", - "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==", - "dev": true + "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.7.0", @@ -419,6 +564,16 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz", + "integrity": "sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.0.tgz", @@ -1007,11 +1162,26 @@ } } }, + "@babel/runtime-corejs2": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.4.tgz", + "integrity": "sha512-hKNcmHQbBSJFnZ82ewYtWDZ3fXkP/l1XcfRtm7c8gHPM/DMecJtFFBEp7KMLZTuHwwb7RfemHdsEnd7L916Z6A==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, "@babel/template": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.0.tgz", "integrity": "sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -1022,7 +1192,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.2.tgz", "integrity": "sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw==", - "dev": true, "requires": { "@babel/code-frame": "^7.5.5", "@babel/generator": "^7.7.2", @@ -1039,7 +1208,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -1047,14 +1215,12 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -1062,13 +1228,44 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz", "integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, + "@emotion/is-prop-valid": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz", + "integrity": "sha512-6ZODuZSFofbxSbcxwsFz+6ioPjb0ISJRRPLZ+WIbjcU2IMU0Io+RGQjjaTgOvNQl007KICBm7zXQaYQEC1r6Bg==", + "requires": { + "@emotion/memoize": "0.7.3" + } + }, + "@emotion/memoize": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.3.tgz", + "integrity": "sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow==" + }, + "@emotion/unitless": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.4.tgz", + "integrity": "sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ==" + }, + "@fortawesome/fontawesome-free": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz", + "integrity": "sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==", + "dev": true + }, + "@lourenci/react-kanban": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lourenci/react-kanban/-/react-kanban-0.15.0.tgz", + "integrity": "sha512-/2XjB26iXcvpwDwlT3sz8/ptQ7QyTpMGlrPf1f02+V1Z4jdbVMo6Luz1sGlHe/TP68N8yz69/YT9qwqHZ6YYmQ==", + "requires": { + "react-beautiful-dnd": "^11.0.0" + } + }, "@redux-saga/core": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", @@ -1583,6 +1780,22 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-styled-components": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.6.tgz", + "integrity": "sha512-gyQj/Zf1kQti66100PhrCRjI5ldjaze9O0M3emXRPAN80Zsf8+e1thpTpaXJXVHXtaM4/+dJEgZHyS9Its+8SA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-module-imports": "^7.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -1958,6 +2171,11 @@ "map-obj": "^1.0.0" } }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, "caniuse-lite": { "version": "1.0.30001010", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001010.tgz", @@ -2102,7 +2320,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -2110,8 +2327,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -2217,8 +2433,7 @@ "core-js": { "version": "2.6.10", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", - "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==", - "dev": true + "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==" }, "core-js-compat": { "version": "3.4.1", @@ -2322,6 +2537,19 @@ "urix": "^0.1.0" } }, + "css-box-model": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.0.tgz", + "integrity": "sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" + }, "css-loader": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz", @@ -2353,6 +2581,16 @@ "regexpu-core": "^1.0.0" } }, + "css-to-react-native": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.3.2.tgz", + "integrity": "sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^3.3.0" + } + }, "cssesc": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", @@ -2653,8 +2891,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-scope": { "version": "4.0.3", @@ -2690,8 +2927,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "events": { "version": "3.0.0", @@ -3748,6 +3984,12 @@ "which": "^1.2.14" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "globule": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", @@ -3798,8 +4040,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -4235,6 +4476,11 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, + "is-what": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.4.0.tgz", + "integrity": "sha512-oFdBRuSY9PocqPoUUseDXek4I+A1kWGigZGhuG+7GEkp0tRkek11adc0HbTEVsNvtojV7rp0uhf5LWtGvHzoOQ==" + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -4411,8 +4657,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "loose-envify": { "version": "1.4.0", @@ -4524,6 +4769,11 @@ "p-is-promise": "^2.0.0" } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", @@ -4552,6 +4802,14 @@ "trim-newlines": "^1.0.0" } }, + "merge-anything": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.2.tgz", + "integrity": "sha512-/so+4seX7fdAhPI3m3bxwc60vhotzY9uM+1Z6C3GKeJBYzxt/lIrbs5uT9iwgM5aLi5kpJIPT7JzJfrrfloWHA==", + "requires": { + "is-what": "^3.3.1" + } + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -5406,8 +5664,7 @@ "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, "private": { "version": "0.1.8", @@ -5532,6 +5789,11 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5561,6 +5823,21 @@ "prop-types": "^15.6.2" } }, + "react-beautiful-dnd": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.5.tgz", + "integrity": "sha512-7llby9U+jIfkINcyxPHVWU0HFYzqxMemUYgGHsFsbx4fZo1n/pW6sYKYzhxGxR3Ap5HxqswcQkKUZX4uEUWhlw==", + "requires": { + "@babel/runtime-corejs2": "^7.4.5", + "css-box-model": "^1.1.2", + "memoize-one": "^5.0.4", + "raf-schd": "^4.0.0", + "react-redux": "^7.0.3", + "redux": "^4.0.1", + "tiny-invariant": "^1.0.4", + "use-memo-one": "^1.1.0" + } + }, "react-dom": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", @@ -6603,6 +6880,46 @@ "schema-utils": "^1.0.0" } }, + "styled-components": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", + "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@emotion/is-prop-valid": "^0.8.1", + "@emotion/unitless": "^0.7.0", + "babel-plugin-styled-components": ">= 1", + "css-to-react-native": "^2.2.2", + "memoize-one": "^5.0.0", + "merge-anything": "^2.2.4", + "prop-types": "^15.5.4", + "react-is": "^16.6.0", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + }, + "stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -6711,8 +7028,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -7016,6 +7332,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -7042,8 +7363,7 @@ "uuid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8-compile-cache": { "version": "2.0.3", diff --git a/client/package.json b/client/package.json index f4f82de..29b7f2a 100644 --- a/client/package.json +++ b/client/package.json @@ -20,9 +20,11 @@ "homepage": "https://forge.cadoles.com/wpetit/gitea-apps#readme", "devDependencies": { "@babel/core": "^7.7.2", + "@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/plugin-transform-runtime": "^7.7.4", "@babel/preset-env": "^7.7.1", "@babel/preset-react": "^7.7.4", + "@fortawesome/fontawesome-free": "^5.11.2", "babel-loader": "^8.0.6", "css-loader": "^1.0.1", "extract-loader": "^3.1.0", @@ -38,6 +40,7 @@ "webpack-cli": "^3.1.2" }, "dependencies": { + "@lourenci/react-kanban": "^0.15.0", "bulma": "^0.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", @@ -45,6 +48,8 @@ "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "redux": "^4.0.4", - "redux-saga": "^1.1.3" + "redux-saga": "^1.1.3", + "styled-components": "^4.4.1", + "uuid": "^3.3.3" } } diff --git a/client/src/components/App.jsx b/client/src/components/App.jsx index 8fee259..f44c550 100644 --- a/client/src/components/App.jsx +++ b/client/src/components/App.jsx @@ -1,23 +1,29 @@ import React from 'react'; import { HashRouter as Router, Route, Redirect, Switch } from "react-router-dom"; +import { ConnectedHomePage as HomePage } from './HomePage/HomePage'; +import { ConnectedBoardPage as BoardPage } from './BoardPage/BoardPage'; +import { ConnectedEditBoardPage as EditBoardPage } from './BoardPage/EditBoardPage'; import { store } from '../store/store'; import { Provider } from 'react-redux'; -import { HomePage } from './HomePage/HomePage'; -import { BoardPage } from './BoardPage/BoardPage'; export class App extends React.Component { render() { return ( - - - - - - } /> - - - + + + + + + + + { + window.location = "/logout"; + return null; + }} /> + } /> + + ); } diff --git a/client/src/components/BoardPage/BoardPage.jsx b/client/src/components/BoardPage/BoardPage.jsx index 35f3b9e..e388ef1 100644 --- a/client/src/components/BoardPage/BoardPage.jsx +++ b/client/src/components/BoardPage/BoardPage.jsx @@ -1,12 +1,103 @@ import React from 'react'; import { Page } from '../Page'; +import { connect } from 'react-redux'; +import Board from '@lourenci/react-kanban'; +import { fetchIssues } from '../../store/actions/issues'; +import { fetchBoards } from '../../store/actions/boards'; +import { buildKanboard, moveCard } from '../../store/actions/kanboards'; export class BoardPage extends React.Component { + render() { return ( - +
+
+ {this.renderBoard()} +
+
); } -} \ No newline at end of file + + renderBoard() { + const { kanboard } = this.props; + if (!kanboard) { + return

Loading

+ } + return ( + + {kanboard} + + ); + } + + renderCard(card) { + return ( +
+
+
+
+
+
{card.title}
+
+
+
+
+
+ ); + } + + renderLaneHeader(lane) { + return ( +

{lane.title}

+ ) + } + + onCardDragEnd(source, dest) { + const { board } = this.props; + this.props.dispatch(moveCard( + board.id, + source.fromLaneId, + source.fromPosition, + dest.toLaneId, + dest.toPosition + )); + } + + componentDidMount() { + const { board } = this.props; + if (!board) { + this.requestBoardsUpdate(); + return + } + + this.requestBuildKanboard(); + } + + componentDidUpdate(prevProps) { + if (prevProps.board !== this.props.board) this.requestBuildKanboard(); + } + + requestBoardsUpdate() { + this.props.dispatch(fetchBoards()); + } + + requestBuildKanboard() { + const { board } = this.props; + if (!board) return; + this.props.dispatch(buildKanboard(board)); + } + +} + +export const ConnectedBoardPage = connect(function(state, props) { + const boardID = props.match.params.id; + return { + board: state.boards.byID[boardID], + kanboard: state.kanboards.byID[boardID] + }; +})(BoardPage); \ No newline at end of file diff --git a/client/src/components/BoardPage/EditBoardPage.jsx b/client/src/components/BoardPage/EditBoardPage.jsx new file mode 100644 index 0000000..cac4547 --- /dev/null +++ b/client/src/components/BoardPage/EditBoardPage.jsx @@ -0,0 +1,390 @@ +import React from 'react'; +import { Page } from '../Page'; +import { connect } from 'react-redux'; +import { selectFlagsIsLoading } from '../../store/selectors/flags'; +import { fetchBoards, saveBoard } from '../../store/actions/boards'; +import { fetchProjects } from '../../store/actions/projects'; +import uuidv4 from 'uuid/v4'; + +export class EditBoardPage extends React.Component { + + state = { + edited: false, + board: { + id: uuidv4(), + title: "", + description: "", + projects: [], + lanes: [] + }, + } + + static getDerivedStateFromProps(props, state) { + const { board, isLoading } = props; + + if (isLoading || !board || state.edited) return state; + + return { + edited: false, + board: { + id: board.id, + title: board.title, + description: board.description, + projects: [ ...board.projects ], + lanes: [ ...board.lanes.map(l => ({ ...l })) ] + } + }; + } + + constructor(props) { + super(props); + this.onBoardTitleChange = this.onBoardAttrChange.bind(this, 'title'); + this.onBoardDescriptionChange = this.onBoardAttrChange.bind(this, 'description'); + this.onBoardLaneTitleChange = this.onBoardLaneAttrChange.bind(this, 'title'); + this.onBoardLaneIssueLabelChange = this.onBoardLaneAttrChange.bind(this, 'issueLabel'); + } + + render() { + const { isLoading } = this.props; + const { board } = this.state; + + if (isLoading) { + return ( +

Loading...

+ ) + }; + + return ( + +
+
+
+ { + board.id ? +

Éditer le tableau

: +

Nouveau tableau

+ } +
+ +
+ +
+
+
+ +
+ +
+
+
+ { this.renderProjectSelect() } +
+ { this.renderLanesSection() } +
+

+ + Annuler + +

+

+ + Enregistrer + +

+
+
+
+
+
+ ); + } + + renderProjectSelect() { + const { projects } = this.props; + const { board } = this.state; + + const projectSelectField = (projectIndex, value, withDeleteAddon) => { + return ( +
+
+
+ +
+
+ { + withDeleteAddon ? +
+ +
: + null + } +
+ ); + } + + return ( + + + { + board.projects.map((p, i) => { + return projectSelectField(i, p, true); + }) + } + { projectSelectField(board.projects.length, '', false) } + + ) + } + + + + renderLanesSection() { + + const { board } = this.state; + + const laneSection = (laneIndex, lane) => { + return ( + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+
+
+ ); + }; + + const lanes = board.lanes.map((l, i) => laneSection(i, l)) + + return ( + +
+
+ +
+
+
+

+ + Ajouter une voie + +

+
+
+
+ { lanes } +
+ ) + } + + onBoardLaneAdd() { + this.setState(state => { + const lanes = [ + ...state.board.lanes, + { id: uuidv4(), title: "", issueLabel: "" } + ]; + return { + ...state, + edited: true, + board: { + ...state.board, + lanes + } + } + }); + } + + onBoardProjectDelete(projectIndex) { + this.setState(state => { + const projects = [ ...state.board.projects ] + projects.splice(projectIndex, 1); + return { + ...state, + edited: true, + board: { + ...state.board, + projects + } + } + }); + } + + onBoardLaneMove(laneIndex, direction) { + this.setState(state => { + const lanes = [ ...state.board.lanes ]; + + const nextLaneIndex = laneIndex+direction; + if (nextLaneIndex < 0 || nextLaneIndex >= lanes.length) { + return state; + } + + const l = lanes[laneIndex]; + lanes.splice(laneIndex, 1); + lanes.splice(nextLaneIndex, 0, l); + + return { + ...state, + edited: true, + board: { + ...state.board, + lanes + } + } + }); + } + + onBoardLaneDelete(laneIndex) { + this.setState(state => { + const lanes = [ ...state.board.lanes ] + lanes.splice(laneIndex, 1); + return { + ...state, + edited: true, + board: { + ...state.board, + lanes + } + } + }); + } + + onBoardProjectChange(projectIndex, evt) { + const value = evt.target.value; + this.setState(state => { + const projects = [ ...state.board.projects ]; + projects[projectIndex] = value; + return { + ...state, + edited: true, + board: { + ...state.board, + projects + } + } + }); + } + + onBoardAttrChange(attrName, evt) { + const value = evt.target.value; + this.setState(state => { + return { + ...state, + edited: true, + board: { + ...state.board, + [attrName]: value, + } + } + }); + } + + onBoardLaneAttrChange(attrName, laneIndex, evt) { + const value = evt.target.value; + this.setState(state => { + const lanes = [ ...state.board.lanes ]; + lanes[laneIndex] = { + ...state.board.lanes[laneIndex], + [attrName]: value + }; + return { + ...state, + edited: true, + board: { + ...state.board, + lanes + } + } + }); + } + + onSaveBoardClick() { + const { board } = this.state; + this.props.dispatch(saveBoard(board)); + this.props.history.push('/'); + } + + componentDidMount() { + this.props.dispatch(fetchBoards()); + this.props.dispatch(fetchProjects()); + } + +} + +export const ConnectedEditBoardPage = connect(function(state, props) { + const boardID = props.match.params.id; + const board = boardID ? state.boards.byID[boardID] : null; + + const projects = Object.keys(state.projects.byName).sort(); + + const isLoading = selectFlagsIsLoading(state, 'FETCH_BOARDS', 'FETCH_PROJECTS'); + + return { board, isLoading, projects }; +})(EditBoardPage); \ No newline at end of file diff --git a/client/src/components/HomePage/BoardCard.jsx b/client/src/components/HomePage/BoardCard.jsx new file mode 100644 index 0000000..ee0cfeb --- /dev/null +++ b/client/src/components/HomePage/BoardCard.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +export class BoardCard extends React.PureComponent { + render() { + const { board } = this.props; + return ( +
+
+
+
+ +

{board.title}

+
+
+
+
+
+
+
+ + + + + + + + + + +
+
+
+ ); + } + +} \ No newline at end of file diff --git a/client/src/components/HomePage/HomePage.jsx b/client/src/components/HomePage/HomePage.jsx index 8a2b3e5..621881b 100644 --- a/client/src/components/HomePage/HomePage.jsx +++ b/client/src/components/HomePage/HomePage.jsx @@ -1,12 +1,76 @@ import React from 'react'; import { Page } from '../Page'; +import { BoardCard } from './BoardCard'; +import { connect } from 'react-redux'; +import { fetchBoards } from '../../store/actions/boards'; export class HomePage extends React.Component { render() { return ( - +
+
+
+
+
+ + + + + Nouveau tableau + +
+
+
+ { this.renderBoards() } +
); } -} \ No newline at end of file + + renderBoards() { + const { boards } = this.props; + const rows = Object.values(boards) + .reduce((boardRows, board, index) => { + if (index % 3 === 0) { + boardRows.push([]); + } + boardRows[boardRows.length-1].push(board); + return boardRows; + }, []) + .map((row, rowIndex) => { + const tiles = row.map((board) => { + return ( +
+
+ +
+
+ ); + }); + return ( +
+ {tiles} +
+ ); + }); + ; + + return ( +
+ { rows } +
+ ); + } + + componentDidMount() { + this.props.dispatch(fetchBoards()); + } + +} + +export const ConnectedHomePage = connect(function(state) { + return { + boards: state.boards.byID, + }; +})(HomePage); \ No newline at end of file diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx new file mode 100644 index 0000000..2d9be9d --- /dev/null +++ b/client/src/components/Navbar.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +export class Navbar extends React.Component { + render() { + return ( + + ); + } +} \ No newline at end of file diff --git a/client/src/components/Page.jsx b/client/src/components/Page.jsx index 8fe8dab..c786d72 100644 --- a/client/src/components/Page.jsx +++ b/client/src/components/Page.jsx @@ -1,11 +1,13 @@ import React from 'react'; +import { Navbar } from './Navbar'; export class Page extends React.Component { render() { return ( -
- -
+ + + {this.props.children} + ); } } \ No newline at end of file diff --git a/client/src/index.html b/client/src/index.html index a17ec7b..447cd2d 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -3,7 +3,7 @@ - Gitea Kan + GiteaKan diff --git a/client/src/index.js b/client/src/index.js index 448780a..a8269ce 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,6 +4,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { App } from './components/App'; +import '@fortawesome/fontawesome-free/js/fontawesome' +import '@fortawesome/fontawesome-free/js/solid' +import '@fortawesome/fontawesome-free/js/regular' +import '@fortawesome/fontawesome-free/js/brands' + ReactDOM.render( , document.getElementById('app') diff --git a/client/src/sass/_all.scss b/client/src/sass/_all.scss index 32ddb7e..4bc3024 100644 --- a/client/src/sass/_all.scss +++ b/client/src/sass/_all.scss @@ -1 +1,3 @@ -@import 'bulma/bulma.sass'; \ No newline at end of file +@import 'bulma/bulma.sass'; +@import '_base.scss'; +@import '_kanboard.scss'; \ No newline at end of file diff --git a/client/src/sass/_base.scss b/client/src/sass/_base.scss new file mode 100644 index 0000000..76be71a --- /dev/null +++ b/client/src/sass/_base.scss @@ -0,0 +1,12 @@ +html, body { + height: 100%; +} + +.is-fullheight { + height: 100%; +} + +#app { + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/client/src/sass/_kanboard.scss b/client/src/sass/_kanboard.scss new file mode 100644 index 0000000..321d558 --- /dev/null +++ b/client/src/sass/_kanboard.scss @@ -0,0 +1,40 @@ + +.kanboard-container { + & > div { + padding: 0; + height: 100%; + + & > div { + width: 100%; + height: 100%; + display: flex; + + // Lanes + & > div { + &:first-child { + margin-left: 0; + } + flex-grow: 1; + flex-basis: 100%; + background-color: transparent; + + // Card container + & > div > div > div { + width: 100%; + } + } + } + } + + [data-react-beautiful-dnd-droppable] { + height: 100%; + } + + .kanboard-card { + margin-bottom: $size-small; + } + + .kanboard-lane-title { + margin-bottom: $size-small; + } +} \ No newline at end of file diff --git a/client/src/store/actions/boards.js b/client/src/store/actions/boards.js new file mode 100644 index 0000000..49d4fe1 --- /dev/null +++ b/client/src/store/actions/boards.js @@ -0,0 +1,15 @@ +export const FETCH_BOARDS_REQUEST = "FETCH_BOARDS_REQUEST"; +export const FETCH_BOARDS_SUCCESS = "FETCH_BOARDS_SUCCESS"; +export const FETCH_BOARDS_FAILURE = "FETCH_BOARDS_FAILURE"; + +export function fetchBoards() { + return { type: FETCH_BOARDS_REQUEST }; +}; + +export const SAVE_BOARD_REQUEST = "SAVE_BOARD_REQUEST"; +export const SAVE_BOARD_SUCCESS = "SAVE_BOARD_SUCCESS"; +export const SAVE_BOARD_FAILURE = "SAVE_BOARD_FAILURE"; + +export function saveBoard(board) { + return { type: SAVE_BOARD_REQUEST, board }; +}; \ No newline at end of file diff --git a/client/src/store/actions/issues.js b/client/src/store/actions/issues.js new file mode 100644 index 0000000..3187322 --- /dev/null +++ b/client/src/store/actions/issues.js @@ -0,0 +1,23 @@ +export const FETCH_ISSUES_REQUEST = "FETCH_ISSUES_REQUEST"; +export const FETCH_ISSUES_SUCCESS = "FETCH_ISSUES_SUCCESS"; +export const FETCH_ISSUES_FAILURE = "FETCH_ISSUES_FAILURE"; + +export function fetchIssues(project) { + return { type: FETCH_ISSUES_REQUEST, project }; +}; + +export const ADD_LABEL_REQUEST = "ADD_LABEL_REQUEST"; +export const ADD_LABEL_SUCCESS = "ADD_LABEL_SUCCESS"; +export const ADD_LABEL_FAILURE = "ADD_LABEL_FAILURE"; + +export function addLabel(project, issueNumber, label) { + return { type: ADD_LABEL_REQUEST, project, issueNumber, label }; +} + +export const REMOVE_LABEL_REQUEST = "REMOVE_LABEL_REQUEST"; +export const REMOVE_LABEL_SUCCESS = "REMOVE_LABEL_SUCCESS"; +export const REMOVE_LABEL_FAILURE = "REMOVE_LABEL_FAILURE"; + +export function removeLabel(project, issueNumber, label) { + return { type: REMOVE_LABEL_REQUEST, project, issueNumber, label }; +} \ No newline at end of file diff --git a/client/src/store/actions/kanboards.js b/client/src/store/actions/kanboards.js new file mode 100644 index 0000000..1a0b2aa --- /dev/null +++ b/client/src/store/actions/kanboards.js @@ -0,0 +1,13 @@ +export const BUILD_KANBOARD_REQUEST = "BUILD_KANBOARD_REQUEST"; +export const BUILD_KANBOARD_SUCCESS = "BUILD_KANBOARD_SUCCESS"; +export const BUILD_KANBOARD_FAILURE = "BUILD_KANBOARD_FAILURE"; + +export function buildKanboard(board) { + return { type: BUILD_KANBOARD_REQUEST, board }; +}; + +export const MOVE_CARD = "MOVE_CARD"; + +export function moveCard(boardID, fromLaneID, fromPosition, toLaneID, toPosition) { + return { type: MOVE_CARD, boardID, fromLaneID, fromPosition, toLaneID, toPosition }; +}; \ No newline at end of file diff --git a/client/src/store/actions/logout.js b/client/src/store/actions/logout.js new file mode 100644 index 0000000..b54b03a --- /dev/null +++ b/client/src/store/actions/logout.js @@ -0,0 +1,5 @@ +export const LOGOUT = "LOGOUT"; + +export function logout() { + return { type: LOGOUT }; +}; \ No newline at end of file diff --git a/client/src/store/actions/projects.js b/client/src/store/actions/projects.js new file mode 100644 index 0000000..5c992cf --- /dev/null +++ b/client/src/store/actions/projects.js @@ -0,0 +1,7 @@ +export const FETCH_PROJECTS_REQUEST = "FETCH_PROJECTS_REQUEST"; +export const FETCH_PROJECTS_SUCCESS = "FETCH_PROJECTS_SUCCESS"; +export const FETCH_PROJECTS_FAILURE = "FETCH_PROJECTS_FAILURE"; + +export function fetchProjects() { + return { type: FETCH_PROJECTS_REQUEST }; +}; \ No newline at end of file diff --git a/client/src/store/reducers/boards.js b/client/src/store/reducers/boards.js new file mode 100644 index 0000000..7202f28 --- /dev/null +++ b/client/src/store/reducers/boards.js @@ -0,0 +1,41 @@ +import { SAVE_BOARD_SUCCESS, FETCH_BOARDS_SUCCESS } from "../actions/boards"; + +export const defaultState = { + byID: {}, +}; + +export function boardsReducer(state = defaultState, action) { + switch(action.type) { + case SAVE_BOARD_SUCCESS: + return handleSaveBoardSuccess(state, action); + case FETCH_BOARDS_SUCCESS: + return handleFetchBoardsSuccess(state, action); + default: + return state; + } +} + +function handleSaveBoardSuccess(state, action) { + return { + ...state, + byID: { + ...state.byID, + [action.board.id.toString()]: { + ...action.board, + } + } + }; +} + +function handleFetchBoardsSuccess(state, action) { + const boardsByID = action.boards.reduce((byID, board) => { + byID[board.id] = board; + return byID; + }, {}); + return { + ...state, + byID: { + ...boardsByID, + } + }; +} \ No newline at end of file diff --git a/client/src/store/reducers/flags.js b/client/src/store/reducers/flags.js new file mode 100644 index 0000000..6b1c6fa --- /dev/null +++ b/client/src/store/reducers/flags.js @@ -0,0 +1,22 @@ +const defaultState = { + actions: {} +}; + +export function flagsReducer(state = defaultState, action) { + const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type); + + if(!matches) return state; + + const actionPrefix = matches[1]; + + return { + ...state, + actions: { + ...state.actions, + [actionPrefix]: { + isLoading: matches[2] === 'REQUEST' + } + } + }; + +} \ No newline at end of file diff --git a/client/src/store/reducers/issues.js b/client/src/store/reducers/issues.js index 5fed7be..572dfae 100644 --- a/client/src/store/reducers/issues.js +++ b/client/src/store/reducers/issues.js @@ -1,5 +1,27 @@ +import { FETCH_ISSUES_SUCCESS } from "../actions/issues"; -export function issuesReducer(state = {}, action) { +const defaultState = { + byProject: {} +}; - return state; +export function issuesReducer(state = defaultState, action) { + switch(action.type) { + case FETCH_ISSUES_SUCCESS: + return handleFetchIssuesSuccess(state, action); + default: + return state; + } + +} + +function handleFetchIssuesSuccess(state, action) { + return { + ...state, + byProject: { + ...state.byProject, + [action.project]: [ + ...action.issues, + ] + } + } } \ No newline at end of file diff --git a/client/src/store/reducers/kanboards.js b/client/src/store/reducers/kanboards.js new file mode 100644 index 0000000..f193ce6 --- /dev/null +++ b/client/src/store/reducers/kanboards.js @@ -0,0 +1,74 @@ +import { BUILD_KANBOARD_SUCCESS, MOVE_CARD } from "../actions/kanboards"; + +export const defaultState = { + byID: {}, +}; + +export function kanboardsReducer(state = defaultState, action) { + switch(action.type) { + case BUILD_KANBOARD_SUCCESS: + return handleBuildKanboardSuccess(state, action); + case MOVE_CARD: + return handleMoveCard(state, action); + default: + return state; + } +} + +function handleBuildKanboardSuccess(state, action) { + return { + ...state, + byID: { + ...state.byID, + [action.kanboard.id]: { + ...action.kanboard, + } + } + }; +} + +function handleMoveCard(state, action) { + const { + boardID, fromLaneID, + fromPosition, toLaneID, + toPosition + } = action; + + const kanboard = state.byID[boardID]; + + const lanes = [ ...kanboard.lanes ]; + const fromLane = lanes[fromLaneID]; + const toLane = lanes[toLaneID]; + const card = fromLane.cards[fromPosition]; + + const fromCards = [ ...fromLane.cards ]; + if (fromLaneID !== toLaneID) { + fromCards.splice(fromPosition, 1); + lanes[fromLaneID] = { + ...fromLane, + cards: fromCards, + }; + + const toCards = [ ...toLane.cards ]; + toCards.splice(toPosition, 0, card); + lanes[toLaneID] = { + ...toLane, + cards: toCards, + }; + } else { + fromCards.splice(fromPosition, 1); + fromCards.splice(toPosition, 0, card); + console.log(fromCards) + } + + return { + ...state, + byID: { + ...state.byID, + [boardID]: { + ...state.byID[boardID], + lanes, + }, + } + }; +} \ No newline at end of file diff --git a/client/src/store/reducers/projects.js b/client/src/store/reducers/projects.js new file mode 100644 index 0000000..802b88b --- /dev/null +++ b/client/src/store/reducers/projects.js @@ -0,0 +1,27 @@ +import { FETCH_PROJECTS_SUCCESS } from "../actions/projects"; + +export const defaultState = { + byName: {}, +}; + +export function projectsReducer(state = defaultState, action) { + switch(action.type) { + case FETCH_PROJECTS_SUCCESS: + return handleFetchProjectsSuccess(state, action); + default: + return state; + } +} + +function handleFetchProjectsSuccess(state, action) { + const projectsByName = action.projects.reduce((byName, project) => { + byName[project.full_name] = project; + return byName; + }, {}); + return { + ...state, + byName: { + ...projectsByName, + } + }; +} \ No newline at end of file diff --git a/client/src/store/reducers/root.js b/client/src/store/reducers/root.js index 0b8000e..9820c0f 100644 --- a/client/src/store/reducers/root.js +++ b/client/src/store/reducers/root.js @@ -1,6 +1,14 @@ import { combineReducers } from 'redux'; import { issuesReducer } from './issues'; +import { boardsReducer } from './boards'; +import { flagsReducer } from './flags'; +import { projectsReducer } from './projects'; +import { kanboardsReducer } from './kanboards'; export const rootReducer = combineReducers({ issues: issuesReducer, + boards: boardsReducer, + kanboards: kanboardsReducer, + flags: flagsReducer, + projects: projectsReducer }); \ No newline at end of file diff --git a/client/src/store/sagas/boards.js b/client/src/store/sagas/boards.js new file mode 100644 index 0000000..a05fcb3 --- /dev/null +++ b/client/src/store/sagas/boards.js @@ -0,0 +1,31 @@ +import { put, call } from 'redux-saga/effects'; +import { FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS, SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE } from '../actions/boards'; +import { api } from '../../util/api'; + +const boardsLocalStorageKey = 'giteakan.boards'; + +export function* fetchBoardsSaga() { + let boards; + + try { + boards = yield call(api.fetchBoards) + } catch(error) { + yield put({ type: FETCH_BOARDS_FAILURE, error }); + return + } + + yield put({ type: FETCH_BOARDS_SUCCESS, boards }); +} + +export function* saveBoardSaga(action) { + let { board } = action; + + try { + board = yield call(api.saveBoard, board) + } catch(error) { + yield put({ type: SAVE_BOARD_FAILURE, error }); + return + } + + yield put({ type: SAVE_BOARD_SUCCESS, board }); +} diff --git a/client/src/store/sagas/failure.js b/client/src/store/sagas/failure.js index d6274af..b6b19f5 100644 --- a/client/src/store/sagas/failure.js +++ b/client/src/store/sagas/failure.js @@ -1,4 +1,10 @@ +import { GiteaUnauthorizedError } from "../../util/gitea"; +import { LOGOUT } from "../actions/logout"; +import { put } from 'redux-saga/effects'; -export function* handleFailedActionSaga(action) { - console.error(action.error); +export function* failuresSaga(action) { + const err = action.error; + if (err instanceof GiteaUnauthorizedError) { + yield put({ type: LOGOUT }); + } } diff --git a/client/src/store/sagas/issues.js b/client/src/store/sagas/issues.js new file mode 100644 index 0000000..98b5939 --- /dev/null +++ b/client/src/store/sagas/issues.js @@ -0,0 +1,59 @@ +import { put, call, retry } from 'redux-saga/effects'; +import { FETCH_ISSUES_SUCCESS, FETCH_ISSUES_FAILURE, ADD_LABEL_FAILURE, ADD_LABEL_SUCCESS, REMOVE_LABEL_FAILURE, REMOVE_LABEL_SUCCESS } from '../actions/issues'; +import { gitea } from '../../util/gitea'; + +export function* fetchIssuesSaga(action) { + const { project } = action; + + let issues; + try { + issues = yield call(gitea.fetchIssues.bind(gitea), action.project); + } catch(error) { + yield put({ type: FETCH_ISSUES_FAILURE, project, error }); + return; + } + + yield put({ type: FETCH_ISSUES_SUCCESS, project, issues }); +} + +export function* addLabelSaga(action) { + const { project, issueNumber, label } = action; + const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project); + const giteaLabel = labels.find(l => l.name === label) + + if (!giteaLabel) { + yield put({ type: ADD_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) }); + return; + } + + try { + yield retry(5, 250, gitea.addIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id); + } catch(error) { + yield put({ type: ADD_LABEL_FAILURE, error }); + return; + } + + yield put({ type: ADD_LABEL_SUCCESS, project, issueNumber, label }); +} + +export function* removeLabelSaga(action) { + const { project, issueNumber, label } = action; + const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project); + const giteaLabel = labels.find(l => l.name === label) + + if (!giteaLabel) { + yield put({ type: REMOVE_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) }); + return; + } + + try { + yield retry(5, 250, gitea.removeIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id); + } catch(error) { + yield put({ type: REMOVE_LABEL_FAILURE, error }); + return; + } + + + yield put({ type: REMOVE_LABEL_SUCCESS, project, issueNumber, label }); + +} diff --git a/client/src/store/sagas/kanboards.js b/client/src/store/sagas/kanboards.js new file mode 100644 index 0000000..3dce6ff --- /dev/null +++ b/client/src/store/sagas/kanboards.js @@ -0,0 +1,98 @@ +import { select, put } from 'redux-saga/effects'; +import { fetchIssues, addLabel, removeLabel } from '../actions/issues'; +import { fetchIssuesSaga } from './issues'; +import { BUILD_KANBOARD_SUCCESS } from '../actions/kanboards'; + +export function* moveCardSaga(action) { + const { + boardID, fromLaneID, + fromPosition, toLaneID, + toPosition, + } = action; + + const { board, kanboard} = yield select(state => { + return { + kanboard: state.kanboards.byID[boardID], + board: state.boards.byID[boardID] + } + }); + + const toLane = kanboard.lanes[toLaneID]; + const card = toLane.cards[toPosition]; + + if (!card) return; + + yield put(addLabel(card.project, card.number, board.lanes[toLaneID].issueLabel)); + yield put(removeLabel(card.project, card.number, board.lanes[fromLaneID].issueLabel)); + +} + +export function* buildKanboardSaga(action) { + + const { board } = action; + + let kanboard; + try { + + for (let p, i = 0; (p = board.projects[i]); i++) { + yield* fetchIssuesSaga(fetchIssues(p)); + } + + const issues = yield select(state => state.issues); + + kanboard = createKanboard(board, issues); + + } catch(error) { + yield put({ type: BUILD_KANBOARD_FAILURE, error }); + return + } + + yield put({ type: BUILD_KANBOARD_SUCCESS, kanboard }); + +} + +function createCards(projects, issues, lane) { + return projects.reduce((laneCards, p) => { + + const projectIssues = p in issues.byProject ? issues.byProject[p] : []; + + return projectIssues.reduce((projectCards, issue) => { + const hasLabel = issue.labels.some(l => l.name === lane.issueLabel); + + if (hasLabel) { + projectCards.push({ + id: issue.id, + title: `#${issue.number} - ${issue.title}`, + description: "", + project: p, + labels: issue.labels, + assignee: issue.assignee, + number: issue.number + }); + } + + return projectCards; + + }, laneCards); + + }, []); +} + +function createLane(projects, issues, lane, index) { + return { + id: index, + title: lane.title, + cards: createCards(projects, issues, lane) + } +} + +function createKanboard(board, issues) { + if (!board) return null; + + const kanboard = { + id: board.id, + lanes: board.lanes.map(createLane.bind(null, board.projects, issues)), + }; + + return kanboard; +} diff --git a/client/src/store/sagas/logout.js b/client/src/store/sagas/logout.js new file mode 100644 index 0000000..6c5dced --- /dev/null +++ b/client/src/store/sagas/logout.js @@ -0,0 +1,3 @@ +export function* logoutSaga() { + window.location = '/logout'; +} \ No newline at end of file diff --git a/client/src/store/sagas/projects.js b/client/src/store/sagas/projects.js new file mode 100644 index 0000000..3a1a0d8 --- /dev/null +++ b/client/src/store/sagas/projects.js @@ -0,0 +1,16 @@ +import { put, call } from 'redux-saga/effects'; +import { FETCH_PROJECTS_SUCCESS, FETCH_PROJECTS_FAILURE } from '../actions/projects'; +import { gitea } from '../../util/gitea'; + +export function* fetchProjectsSaga() { + + let projects; + try { + projects = yield call(gitea.fetchUserProjects.bind(gitea)) + } catch(error) { + yield put({ type: FETCH_PROJECTS_FAILURE, error }); + return; + } + + yield put({ type: FETCH_PROJECTS_SUCCESS, projects }); +} diff --git a/client/src/store/sagas/root.js b/client/src/store/sagas/root.js index b025386..e6c7199 100644 --- a/client/src/store/sagas/root.js +++ b/client/src/store/sagas/root.js @@ -1,9 +1,28 @@ -import { all, takeEvery } from 'redux-saga/effects'; -import { handleFailedActionSaga } from './failure'; +import { all, takeEvery, takeLatest } from 'redux-saga/effects'; +import { failuresSaga } from './failure'; +import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST } from '../actions/boards'; +import { fetchBoardsSaga, saveBoardSaga } from './boards'; +import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST } from '../actions/issues'; +import { fetchIssuesSaga, addLabelSaga, removeLabelSaga } from './issues'; +import { FETCH_PROJECTS_REQUEST } from '../actions/projects'; +import { fetchProjectsSaga } from './projects'; +import { LOGOUT } from '../actions/logout'; +import { logoutSaga } from './logout'; +import { BUILD_KANBOARD_REQUEST, MOVE_CARD } from '../actions/kanboards'; +import { buildKanboardSaga, moveCardSaga } from './kanboards'; export function* rootSaga() { yield all([ - takeEvery(patternFromRegExp(/^.*_FAILURE/), handleFailedActionSaga), + takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), + takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga), + takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga), + takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga), + takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga), + takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga), + takeEvery(MOVE_CARD, moveCardSaga), + takeEvery(ADD_LABEL_REQUEST, addLabelSaga), + takeEvery(REMOVE_LABEL_REQUEST, removeLabelSaga), + takeLatest(LOGOUT, logoutSaga) ]); } diff --git a/client/src/store/selectors/flags.js b/client/src/store/selectors/flags.js new file mode 100644 index 0000000..3e4632e --- /dev/null +++ b/client/src/store/selectors/flags.js @@ -0,0 +1,7 @@ +export function selectFlagsIsLoading(state, ...actionPrefixes) { + const { actions } = state.flags; + return actionPrefixes.reduce((isLoading, prefix) => { + if (!(prefix in actions)) return isLoading; + return isLoading || actions[prefix].isLoading; + }, false); +}; \ No newline at end of file diff --git a/client/src/util/api.js b/client/src/util/api.js new file mode 100644 index 0000000..ec664f0 --- /dev/null +++ b/client/src/util/api.js @@ -0,0 +1,24 @@ + +export class APIClient { + + saveBoard(board) { + return fetch(`/api/boards`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(board) + }) + .then(res => res.json()) + ; + } + + fetchBoards() { + return fetch(`/api/boards`) + .then(res => res.json()) + ; + } + +} + +export const api = new APIClient(); \ No newline at end of file diff --git a/client/src/util/gitea.js b/client/src/util/gitea.js index da3dfe4..809d892 100644 --- a/client/src/util/gitea.js +++ b/client/src/util/gitea.js @@ -1,7 +1,65 @@ + +export class GiteaUnauthorizedError extends Error { + constructor(...args) { + super(...args) + Error.captureStackTrace(this, GiteaUnauthorizedError) + } +} + export class GiteaClient { - constructor() { - + fetchIssues(project) { + return fetch(`/gitea/api/v1/repos/${project}/issues`) + .then(this.assertAuthorization) + .then(res => res.json()) + ; + } + + fetchUserProjects() { + return fetch(`/gitea/api/v1/user/repos`) + .then(this.assertOk) + .then(this.assertAuthorization) + .then(res => res.json()) + ; + } + + addIssueLabel(project, issueNumber, labelID) { + return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ labels: [labelID] }), + }) + .then(this.assertOk) + .then(this.assertAuthorization) + .then(res => res.json()) + } + + fetchProjectLabels(project) { + return fetch(`/gitea/api/v1/repos/${project}/labels`) + .then(this.assertOk) + .then(this.assertAuthorization) + .then(res => res.json()) + ; + } + + removeIssueLabel(project, issueNumber, labelID) { + return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels/${labelID}`, { + method: 'DELETE' + }) + .then(this.assertOk) + .then(this.assertAuthorization) + } + + assertOk(res) { + if (!res.ok) return Promise.reject(new Error('Request failed')); + return res; + } + + assertAuthorization(res) { + if (res.status === 401 || res.status === 404) return Promise.reject(new GiteaUnauthorizedError()); + return res; } } diff --git a/cmd/server/container.go b/cmd/server/container.go index 00a92c0..297cfce 100644 --- a/cmd/server/container.go +++ b/cmd/server/container.go @@ -2,11 +2,15 @@ package main import ( "forge.cadoles.com/wpetit/gitea-kan/internal/config" + "forge.cadoles.com/wpetit/gitea-kan/internal/repository" + stormRepo "forge.cadoles.com/wpetit/gitea-kan/internal/repository/storm" + "github.com/asdine/storm" "github.com/gorilla/sessions" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/service" "gitlab.com/wpetit/goweb/service/session" "gitlab.com/wpetit/goweb/session/gorilla" + "go.etcd.io/bbolt" ) func getServiceContainer(conf *config.Config) (*service.Container, error) { @@ -30,5 +34,25 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) { // Create and expose config service provider ctn.Provide(config.ServiceName, config.ServiceProvider(conf)) + // Load Storm database + db, err := storm.Open(conf.Data.DBPath, storm.BoltOptions( + 0660, + &bbolt.Options{}, + )) + if err != nil { + return nil, errors.Wrap(err, "could not open database") + } + + boardsRepository := stormRepo.NewBoardRepository(db) + if err := boardsRepository.Init(); err != nil { + return nil, errors.Wrap(err, "could not init boards repository") + } + + ctn.Provide(repository.ServiceName, repository.ServiceProvider( + repository.NewRepository( + boardsRepository, + ), + )) + return ctn, nil } diff --git a/go.mod b/go.mod index c2fd876..2157aca 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module forge.cadoles.com/wpetit/gitea-kan go 1.13 require ( - github.com/davecgh/go-spew v1.1.1 + github.com/asdine/storm v2.1.2+incompatible + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/google/uuid v1.1.1 // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect @@ -15,6 +16,7 @@ require ( github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect github.com/stretchr/testify v1.4.0 // indirect gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273 + go.etcd.io/bbolt v1.3.3 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index ae75178..a2eebae 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= +github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -37,6 +39,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273 h1:YtMGT0pEGTQ5MAglg6rvu8pQVQJEtskoeEw+csUqf2o= gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273/go.mod h1:5Y/eVplFvdsd6zMdA3bx8KON6Ab1n90+cQeX5uJ6jIE= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -47,6 +51,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/internal/config/config.go b/internal/config/config.go index 7f8bed4..25dc7e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ type Config struct { Debug bool HTTP HTTPConfig Gitea GiteaConfig + Data DataConfig } type HTTPConfig struct { @@ -29,6 +30,10 @@ type GiteaConfig struct { APIBaseURL string } +type DataConfig struct { + DBPath string +} + // NewFromFile retrieves the configuration from the given file func NewFromFile(filepath string) (*Config, error) { config := NewDefault() @@ -52,9 +57,12 @@ func NewDefault() *Config { Debug: false, HTTP: HTTPConfig{ Address: ":3000", - PublicDir: "${GITEA_APP_PUBDIR}", + PublicDir: "${GITEAKAN_HTTP_PUBDIR}", }, Gitea: GiteaConfig{}, + Data: DataConfig{ + DBPath: "${GITEAKAN_DATA_DBPATH}", + }, } } diff --git a/internal/repository/board.go b/internal/repository/board.go new file mode 100644 index 0000000..c7c1a37 --- /dev/null +++ b/internal/repository/board.go @@ -0,0 +1,26 @@ +package repository + +type BoardRepository interface { + List() ([]*Board, error) + Get(BoardID) (*Board, error) + Save(*Board) error + Delete(BoardID) error +} + +type BoardID string + +type Board struct { + ID BoardID `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Lanes []*BoardLane `json:"lanes"` + Projects []string `json:"projects"` +} + +type BoardLaneID string + +type BoardLane struct { + ID BoardLaneID `json:"id"` + Title string `json:"title"` + IssueLabel string `json:"issueLabel"` +} diff --git a/internal/repository/provider.go b/internal/repository/provider.go new file mode 100644 index 0000000..3af1a46 --- /dev/null +++ b/internal/repository/provider.go @@ -0,0 +1,9 @@ +package repository + +import "gitlab.com/wpetit/goweb/service" + +func ServiceProvider(repository *Repository) service.Provider { + return func(ctn *service.Container) (interface{}, error) { + return repository, nil + } +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..105bc2c --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,13 @@ +package repository + +type Repository struct { + boards BoardRepository +} + +func (r *Repository) Boards() BoardRepository { + return r.boards +} + +func NewRepository(boards BoardRepository) *Repository { + return &Repository{boards} +} diff --git a/internal/repository/service.go b/internal/repository/service.go new file mode 100644 index 0000000..7777f5d --- /dev/null +++ b/internal/repository/service.go @@ -0,0 +1,33 @@ +package repository + +import ( + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/service" +) + +const ServiceName service.Name = "repository" + +// From retrieves the repository service in the given container +func From(container *service.Container) (*Repository, error) { + service, err := container.Service(ServiceName) + if err != nil { + return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) + } + + srv, ok := service.(*Repository) + if !ok { + return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) + } + + return srv, nil +} + +// Must retrieves the repository service in the given container or panic otherwise +func Must(container *service.Container) *Repository { + srv, err := From(container) + if err != nil { + panic(err) + } + + return srv +} diff --git a/internal/repository/storm/board.go b/internal/repository/storm/board.go new file mode 100644 index 0000000..922bc5a --- /dev/null +++ b/internal/repository/storm/board.go @@ -0,0 +1,69 @@ +package storm + +import ( + "forge.cadoles.com/wpetit/gitea-kan/internal/repository" + "github.com/asdine/storm" + "github.com/pkg/errors" +) + +type BoardRepository struct { + db *storm.DB +} + +type boardItem struct { + ID string `storm:"id"` + Board *repository.Board +} + +func (r *BoardRepository) Init() error { + if err := r.db.Init(&boardItem{}); err != nil { + return errors.Wrap(err, "could not init 'boardItem' collection") + } + + if err := r.db.ReIndex(&boardItem{}); err != nil { + return errors.Wrap(err, "could not reindex 'boardItem' collection") + } + + return nil +} + +func (r *BoardRepository) List() ([]*repository.Board, error) { + boardItems := make([]*boardItem, 0) + + if err := r.db.All(&boardItems); err != nil { + return nil, errors.Wrap(err, "could not retrieve board items") + } + + boards := make([]*repository.Board, 0, len(boardItems)) + + for _, b := range boardItems { + boards = append(boards, b.Board) + } + + return boards, nil +} + +func (r *BoardRepository) Get(id repository.BoardID) (*repository.Board, error) { + return nil, nil +} + +func (r *BoardRepository) Save(board *repository.Board) error { + b := &boardItem{ + ID: string(board.ID), + Board: board, + } + + if err := r.db.Save(b); err != nil { + return errors.Wrap(err, "could not save board item") + } + + return nil +} + +func (r *BoardRepository) Delete(id repository.BoardID) error { + return nil +} + +func NewBoardRepository(db *storm.DB) *BoardRepository { + return &BoardRepository{db} +} diff --git a/internal/route/board.go b/internal/route/board.go new file mode 100644 index 0000000..39e12a3 --- /dev/null +++ b/internal/route/board.go @@ -0,0 +1,46 @@ +package route + +import ( + "encoding/json" + "net/http" + + "forge.cadoles.com/wpetit/gitea-kan/internal/repository" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/middleware/container" +) + +func serveBoards(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + repo := repository.Must(ctn) + + boards, err := repo.Boards().List() + if err != nil { + panic(errors.Wrap(err, "could not retrieve boards list")) + } + + encoder := json.NewEncoder(w) + if err := encoder.Encode(boards); err != nil { + panic(errors.Wrap(err, "could not encode boards list")) + } +} + +func saveBoard(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + repo := repository.Must(ctn) + + decoder := json.NewDecoder(r.Body) + board := &repository.Board{} + + if err := decoder.Decode(board); err != nil { + panic(errors.Wrap(err, "could not decode board")) + } + + if err := repo.Boards().Save(board); err != nil { + panic(errors.Wrap(err, "could not save board")) + } + + encoder := json.NewEncoder(w) + if err := encoder.Encode(board); err != nil { + panic(errors.Wrap(err, "could not encode board")) + } +} diff --git a/internal/route/proxy.go b/internal/route/proxy.go index 094226f..b16fc78 100644 --- a/internal/route/proxy.go +++ b/internal/route/proxy.go @@ -6,8 +6,6 @@ import ( "net/http/httputil" "net/url" - "github.com/davecgh/go-spew/spew" - "forge.cadoles.com/wpetit/gitea-kan/internal/config" "forge.cadoles.com/wpetit/gitea-kan/internal/middleware" "github.com/pkg/errors" @@ -32,12 +30,13 @@ func proxyAPIRequest(w http.ResponseWriter, r *http.Request) { accessToken := sess.Get(middleware.SessionOAuth2AccessToken) proxy := httputil.NewSingleHostReverseProxy(apiBaseURL) - proxy.Director = func(r *http.Request) { - r.Host = apiBaseURL.Host - r.URL.Scheme = apiBaseURL.Scheme - r.URL.Host = apiBaseURL.Host - r.Header.Add("Authorization", fmt.Sprintf("token %s", accessToken)) - spew.Dump(r) + proxy.Director = func(rr *http.Request) { + rr.Host = apiBaseURL.Host + rr.URL.Scheme = apiBaseURL.Scheme + rr.URL.Host = apiBaseURL.Host + rr.Method = r.Method + rr.Header.Add("Accept", "application/json") + rr.Header.Add("Authorization", fmt.Sprintf("token %s", accessToken)) } proxy.ServeHTTP(w, r) diff --git a/internal/route/route.go b/internal/route/route.go index 55a0eb6..c102ca3 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -19,7 +19,9 @@ func Mount(r *chi.Mux, config *config.Config) { r.Group(func(r chi.Router) { r.Use(middleware.Authenticate) r.Get("/logout", handleLogout) - r.Get("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest)).ServeHTTP) + r.Get("/api/boards", serveBoards) + r.Post("/api/boards", saveBoard) + r.Handle("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest))) r.Get("/*", static.Dir(config.HTTP.PublicDir, "", html5PushStateHandler)) }) }) diff --git a/modd.conf b/modd.conf index 6ab6a30..91033fd 100644 --- a/modd.conf +++ b/modd.conf @@ -5,7 +5,10 @@ modd.conf !mage_output_file.go { prep: make build prep: [ -e data/server.conf ] || ( mkdir -p data && bin/server -dump-config > data/server.conf ) - daemon: GITEA_APP_PUBDIR=./client/dist bin/server -config ./data/server.conf + daemon: GITEAKAN_HTTP_PUBDIR=./client/dist \ + GITEAKAN_DATA_DBPATH=./data/data.db \ + bin/server \ + -config ./data/server.conf } **/*.go {