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 (
+
+ );
+ }
+
+ 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() }
+
+
+
+
+
+ );
+ }
+
+ 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 (
+
+
+ { 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 (
+
+ );
+ }
+
+}
\ 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 (
-
+
+
+ { 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 {