Merge branch 'develop' into dist/ubuntu/bionic/develop
This commit is contained in:
commit
bd133fa9d9
|
@ -4,6 +4,43 @@
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-4ighan5Anlj4tK/tdUHs4Mi1njqXZ7AxRCVolz/H702DjPphAJfm+FRkIadPTmwz+OLO+d+tX+6V1VBshf02rg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/zen-observable": "^0.8.0",
|
||||||
|
"@wry/context": "^0.5.2",
|
||||||
|
"@wry/equality": "^0.1.9",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"graphql-tag": "^2.10.4",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"optimism": "^0.12.1",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"symbol-observable": "^1.2.0",
|
||||||
|
"ts-invariant": "^0.4.4",
|
||||||
|
"tslib": "^1.10.0",
|
||||||
|
"zen-observable": "^0.8.14"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@wry/context": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimism": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==",
|
||||||
|
"requires": {
|
||||||
|
"@wry/context": "^0.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/code-frame": {
|
"@babel/code-frame": {
|
||||||
"version": "7.10.1",
|
"version": "7.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
||||||
|
@ -1230,7 +1267,8 @@
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "13.13.12",
|
"version": "13.13.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz",
|
||||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw=="
|
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.3",
|
"version": "15.7.3",
|
||||||
|
@ -1551,15 +1589,6 @@
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@wry/context": {
|
|
||||||
"version": "0.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz",
|
|
||||||
"integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": ">=6",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@wry/equality": {
|
"@wry/equality": {
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
||||||
|
@ -1744,93 +1773,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apollo-cache": {
|
|
||||||
"version": "1.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz",
|
|
||||||
"integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-utilities": "^1.3.4",
|
|
||||||
"tslib": "^1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-cache-inmemory": {
|
|
||||||
"version": "1.6.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz",
|
|
||||||
"integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-cache": "^1.3.5",
|
|
||||||
"apollo-utilities": "^1.3.4",
|
|
||||||
"optimism": "^0.10.0",
|
|
||||||
"ts-invariant": "^0.4.0",
|
|
||||||
"tslib": "^1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-client": {
|
|
||||||
"version": "2.6.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz",
|
|
||||||
"integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==",
|
|
||||||
"requires": {
|
|
||||||
"@types/zen-observable": "^0.8.0",
|
|
||||||
"apollo-cache": "1.3.5",
|
|
||||||
"apollo-link": "^1.0.0",
|
|
||||||
"apollo-utilities": "1.3.4",
|
|
||||||
"symbol-observable": "^1.0.2",
|
|
||||||
"ts-invariant": "^0.4.0",
|
|
||||||
"tslib": "^1.10.0",
|
|
||||||
"zen-observable": "^0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-link": {
|
|
||||||
"version": "1.2.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz",
|
|
||||||
"integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-utilities": "^1.3.0",
|
|
||||||
"ts-invariant": "^0.4.0",
|
|
||||||
"tslib": "^1.9.3",
|
|
||||||
"zen-observable-ts": "^0.8.21"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-link-http": {
|
|
||||||
"version": "1.5.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz",
|
|
||||||
"integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-link": "^1.2.14",
|
|
||||||
"apollo-link-http-common": "^0.2.16",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-link-http-common": {
|
|
||||||
"version": "0.2.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz",
|
|
||||||
"integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-link": "^1.2.14",
|
|
||||||
"ts-invariant": "^0.4.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-link-ws": {
|
|
||||||
"version": "1.0.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz",
|
|
||||||
"integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==",
|
|
||||||
"requires": {
|
|
||||||
"apollo-link": "^1.2.14",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apollo-utilities": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==",
|
|
||||||
"requires": {
|
|
||||||
"@wry/equality": "^0.1.2",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"ts-invariant": "^0.4.0",
|
|
||||||
"tslib": "^1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||||
|
@ -3235,14 +3177,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"bulma": {
|
"bulma": {
|
||||||
"version": "0.7.5",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
|
||||||
"integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw=="
|
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
|
||||||
},
|
|
||||||
"bulma-switch": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
|
|
||||||
},
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
@ -5615,11 +5552,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
|
||||||
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
|
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
|
||||||
},
|
},
|
||||||
"graphql-request": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
|
|
||||||
},
|
|
||||||
"graphql-tag": {
|
"graphql-tag": {
|
||||||
"version": "2.10.4",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
|
||||||
|
@ -6507,11 +6439,6 @@
|
||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jwt-decode": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
|
||||||
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
|
||||||
},
|
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
@ -7466,14 +7393,6 @@
|
||||||
"is-wsl": "^1.1.0"
|
"is-wsl": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"optimism": {
|
|
||||||
"version": "0.10.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz",
|
|
||||||
"integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==",
|
|
||||||
"requires": {
|
|
||||||
"@wry/context": "^0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"original": {
|
"original": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
|
||||||
|
@ -8048,11 +7967,6 @@
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
|
||||||
"version": "6.9.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
|
||||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
|
||||||
},
|
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
|
@ -11006,15 +10920,6 @@
|
||||||
"version": "0.8.15",
|
"version": "0.8.15",
|
||||||
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
||||||
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
|
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
|
||||||
},
|
|
||||||
"zen-observable-ts": {
|
|
||||||
"version": "0.8.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz",
|
|
||||||
"integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==",
|
|
||||||
"requires": {
|
|
||||||
"tslib": "^1.9.3",
|
|
||||||
"zen-observable": "^0.8.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,20 +51,10 @@
|
||||||
"webpack-dev-server": "^3.11.0"
|
"webpack-dev-server": "^3.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.0.2",
|
||||||
"@types/qs": "^6.9.3",
|
"@types/qs": "^6.9.3",
|
||||||
"apollo-cache-inmemory": "^1.6.6",
|
"bulma": "^0.9.0",
|
||||||
"apollo-client": "^2.6.10",
|
|
||||||
"apollo-link": "^1.2.14",
|
|
||||||
"apollo-link-http": "^1.5.17",
|
|
||||||
"apollo-link-ws": "^1.0.20",
|
|
||||||
"apollo-utilities": "^1.3.4",
|
|
||||||
"bulma": "^0.7.2",
|
|
||||||
"bulma-switch": "^2.0.0",
|
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"graphql-request": "^2.0.0",
|
|
||||||
"graphql-tag": "^2.10.4",
|
|
||||||
"jwt-decode": "^2.2.0",
|
|
||||||
"qs": "^6.9.4",
|
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||||
import { HomePage } from './HomePage/HomePage';
|
import { HomePage } from './HomePage/HomePage';
|
||||||
import { store } from '../store/store';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||||
|
import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage';
|
||||||
|
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<Switch>
|
||||||
<Switch>
|
<Route path="/" exact component={HomePage} />
|
||||||
<Route path="/" exact component={HomePage} />
|
<Route path="/profile" exact component={ProfilePage} />
|
||||||
<Route path="/profile" exact component={ProfilePage} />
|
<Route path="/workgroups/:id" exact component={WorkgroupPage} />
|
||||||
<Route component={() => <Redirect to="/" />} />
|
<Route component={() => <Redirect to="/" />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<WorkgroupsPanel />
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<div className="box">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-left">
|
||||||
|
<h3 className="is-size-3 subtitle level-item">D.A.Ds</h3>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>TODO</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<div className="box">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-left">
|
||||||
|
<h3 className="is-size-3 subtitle level-item">Assemblées</h3>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>TODO</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,26 +1,31 @@
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Page } from '../Page';
|
import { Page } from '../Page';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { Dashboard } from './Dashboard';
|
||||||
import { RootState } from '../../store/reducers/root';
|
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||||
|
import { WithLoader } from '../WithLoader';
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
const { data, loading } = useUserProfileQuery();
|
||||||
|
|
||||||
|
const { userProfile } = (data || {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page title="Accueil">
|
<Page title={userProfile ? 'Tableau de bord' : 'Accueil'}>
|
||||||
<div className="container is-fluid">
|
<div className="container is-fluid">
|
||||||
<section className="section">
|
<section className="mt-5">
|
||||||
<div className="columns">
|
<WithLoader loading={loading}>
|
||||||
<div className="column is-4 is-offset-4">
|
{
|
||||||
<div className="box">
|
userProfile ?
|
||||||
{
|
<Dashboard /> :
|
||||||
currentUser && currentUser.email ?
|
<div className="columns">
|
||||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.name ? currentUser.name : currentUser.email}</span> !</p> :
|
<div className="column is-4 is-offset-4">
|
||||||
<p>Veuillez vous authentifier.</p>
|
<div className="box">
|
||||||
}
|
<p>Veuillez vous authentifier.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
</WithLoader>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
||||||
|
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||||
|
import { WithLoader } from '../WithLoader';
|
||||||
|
|
||||||
|
export function WorkgroupsPanel() {
|
||||||
|
const workgroupsQuery = useWorkgroupsQuery();
|
||||||
|
const userProfileQuery = useUserProfileQuery();
|
||||||
|
const [ state, setState ] = useState({ selectedTab: 0 });
|
||||||
|
|
||||||
|
const isLoading = userProfileQuery.loading || workgroupsQuery.loading;
|
||||||
|
const { userProfile } = (userProfileQuery.data || {});
|
||||||
|
const { workgroups } = (workgroupsQuery.data || {});
|
||||||
|
|
||||||
|
const filterTabs = [
|
||||||
|
{
|
||||||
|
label: "Mes groupes en cours",
|
||||||
|
filter: workgroups => workgroups.filter((wg: Workgroup) => {
|
||||||
|
return wg.closedAt === null && wg.members.some((u: User) => u.id === (userProfile ? userProfile.id : ''));
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ouverts",
|
||||||
|
filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Clos",
|
||||||
|
filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectTab = (tabIndex: number) => {
|
||||||
|
setState(state => ({ ...state, selectedTab: tabIndex }));
|
||||||
|
};
|
||||||
|
|
||||||
|
let workgroupsItems = [];
|
||||||
|
|
||||||
|
workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/workgroups/${wg.id}`} key={`wg-${wg.id}`} className="panel-block">
|
||||||
|
<span className="panel-icon">
|
||||||
|
<i className="fas fa-users" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
{wg.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="panel is-info">
|
||||||
|
<div className="level panel-heading mb-0">
|
||||||
|
<div className="level-left">
|
||||||
|
<p className="level-item">
|
||||||
|
Groupes de travail
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<Link to="/workgroups/new" className="button level-item is-outlined is-info is-inverted">
|
||||||
|
<i className="icon fa fa-plus"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="panel-block">
|
||||||
|
<p className="control has-icons-left">
|
||||||
|
<input className="input" type="text" placeholder="Filtrer..." />
|
||||||
|
<span className="icon is-left">
|
||||||
|
<i className="fas fa-search" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
<WithLoader loading={isLoading}>
|
||||||
|
<p className="panel-tabs">
|
||||||
|
{
|
||||||
|
filterTabs.map((tab, i) => {
|
||||||
|
return (
|
||||||
|
<a key={`workgroup-tab-${i}`}
|
||||||
|
onClick={selectTab.bind(null, i)}
|
||||||
|
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||||
|
{tab.label}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
workgroupsItems.length > 0 ?
|
||||||
|
workgroupsItems :
|
||||||
|
<a className="panel-block has-text-centered is-block">
|
||||||
|
<em>Aucun groupe dans cet catégorie pour l'instant.</em>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</WithLoader>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export class Loader extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="loader-container">
|
|
||||||
<div className="lds-ripple">
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { Fragment, useState } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import logo from '../resources/logo.svg';
|
import logo from '../resources/logo.svg';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '../store/reducers/root';
|
|
||||||
import { Config } from '../config';
|
import { Config } from '../config';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useUserProfileQuery } from '../gql/queries/profile';
|
||||||
|
import { WithLoader } from './WithLoader';
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
const userProfileQuery = useUserProfileQuery();
|
||||||
|
|
||||||
const [ isActive, setActive ] = useState(false);
|
const [ isActive, setActive ] = useState(false);
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
|
@ -35,28 +35,30 @@ export function Navbar() {
|
||||||
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||||
<div className="navbar-end">
|
<div className="navbar-end">
|
||||||
<div className="navbar-item">
|
<div className="navbar-item">
|
||||||
<div className="buttons">
|
<WithLoader loading={userProfileQuery.loading}>
|
||||||
{
|
<div className="buttons">
|
||||||
isAuthenticated ?
|
{
|
||||||
<Fragment>
|
userProfileQuery.data && userProfileQuery.data.userProfile ?
|
||||||
<Link to="/profile" className="button">
|
<Fragment>
|
||||||
|
<Link to="/profile" className="button">
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-user"></i>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<a className="button" href={Config.logoutURL}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-sign-out-alt"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Fragment> :
|
||||||
|
<a className="button" href={Config.loginURL}>
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<i className="fas fa-user"></i>
|
<i className="fas fa-sign-in-alt"></i>
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<a className="button" href={Config.logoutURL}>
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fas fa-sign-out-alt"></i>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</Fragment> :
|
}
|
||||||
<a className="button" href={Config.loginURL}>
|
</div>
|
||||||
<span className="icon">
|
</WithLoader>
|
||||||
<i className="fas fa-sign-in-alt"></i>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Page } from '../Page';
|
import { Page } from '../Page';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import { RootState } from '../../store/reducers/root';
|
|
||||||
import { UserForm } from '../UserForm';
|
import { UserForm } from '../UserForm';
|
||||||
import { Loader } from '../Loader';
|
|
||||||
import { User } from '../../types/user';
|
import { User } from '../../types/user';
|
||||||
import { updateProfile } from '../../store/actions/profile';
|
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||||
|
import { useUpdateUserProfileMutation } from '../../gql/mutations/profile';
|
||||||
|
import { WithLoader } from '../WithLoader';
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
const userProfileQuery = useUserProfileQuery();
|
||||||
const dispatch = useDispatch();
|
const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
|
||||||
|
const isLoading = updateUserProfileMutation.loading || userProfileQuery.loading;
|
||||||
|
|
||||||
|
const { userProfile } = (userProfileQuery.data || {});
|
||||||
|
|
||||||
const onUserChange = (user: User) => {
|
const onUserChange = (user: User) => {
|
||||||
if (currentUser.name !== user.name) {
|
if (userProfile.name !== user.name) {
|
||||||
dispatch(updateProfile({ name: user.name }))
|
updateProfile({ variables: {changes: { name: user.name }}});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,11 +26,11 @@ export function ProfilePage() {
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-6 is-offset-3">
|
<div className="column is-6 is-offset-3">
|
||||||
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||||
|
<WithLoader loading={isLoading || !userProfile}>
|
||||||
{
|
{
|
||||||
currentUser ?
|
<UserForm onChange={onUserChange} user={userProfile} />
|
||||||
<UserForm onChange={onUserChange} user={currentUser} /> :
|
|
||||||
<Loader />
|
|
||||||
}
|
}
|
||||||
|
</WithLoader>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -10,11 +10,11 @@ export function UserForm({ user, onChange }: UserFormProps) {
|
||||||
const [ state, setState ] = useState({
|
const [ state, setState ] = useState({
|
||||||
changed: false,
|
changed: false,
|
||||||
user: {
|
user: {
|
||||||
name: '',
|
id: user && user.id ? user.id : '',
|
||||||
email: '',
|
name: user && user.name ? user.name : '',
|
||||||
createdAt: null,
|
email: user && user.email ? user.email : '',
|
||||||
connectedAt: null,
|
createdAt: user && user.createdAt ? user.createdAt : null,
|
||||||
...user,
|
connectedAt: user && user.connectedAt ? user.connectedAt : null,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
export interface WithLoaderProps {
|
||||||
|
loading?: boolean|boolean[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithLoader: FunctionComponent<WithLoaderProps> = ({ loading, children }) => {
|
||||||
|
const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<div>Chargement</div> :
|
||||||
|
children
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
|
||||||
|
export interface InfoFormProps {
|
||||||
|
workgroup: Workgroup
|
||||||
|
onChange?: (workgroup: Workgroup) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
changed: false,
|
||||||
|
workgroup: {
|
||||||
|
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||||
|
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||||
|
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||||
|
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({
|
||||||
|
changed: false,
|
||||||
|
workgroup: {
|
||||||
|
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||||
|
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||||
|
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||||
|
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [workgroup]);
|
||||||
|
|
||||||
|
const onSaveClick = () => {
|
||||||
|
if (!state.changed) return;
|
||||||
|
if (typeof onChange !== 'function') return;
|
||||||
|
onChange(state.workgroup as Workgroup);
|
||||||
|
setState(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
changed: false,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWorkgroupAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = evt.currentTarget.value;
|
||||||
|
setState(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
changed: true,
|
||||||
|
workgroup: {
|
||||||
|
...state.workgroup,
|
||||||
|
[attrName]: value,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form" style={{width: '100%'}}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Nom du groupe</label>
|
||||||
|
<div className="control">
|
||||||
|
<input type="text" className="input" value={state.workgroup.name}
|
||||||
|
onChange={onWorkgroupAttrChange.bind(null, "name")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
state.workgroup.createdAt ?
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Date de création</label>
|
||||||
|
<div className="control">
|
||||||
|
<p className="input is-static">{state.workgroup.createdAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state.workgroup.closedAt ?
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Date de clôture</label>
|
||||||
|
<div className="control">
|
||||||
|
<p className="input is-static">{state.workgroup.closedAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<div className="buttons is-right">
|
||||||
|
<button disabled={!state.changed}
|
||||||
|
className="button is-success" onClick={onSaveClick}>
|
||||||
|
<span>Enregistrer</span>
|
||||||
|
<span className="icon"><i className="fa fa-save"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { InfoForm } from './InfoForm';
|
||||||
|
import { WithLoader } from '../WithLoader';
|
||||||
|
import { useUpdateWorkgroupMutation, useCreateWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
|
export interface InfoPanelProps {
|
||||||
|
workgroup: Workgroup
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfoPanel: FunctionComponent<InfoPanelProps> = ({ workgroup }) => {
|
||||||
|
const [ updateWorkgroup, updateWorkgroupMutation ] = useUpdateWorkgroupMutation();
|
||||||
|
const [ createWorkgroup, createWorkgroupMutation ] = useCreateWorkgroupMutation();
|
||||||
|
const history = useHistory();
|
||||||
|
const isLoading = updateWorkgroupMutation.loading || createWorkgroupMutation.loading;
|
||||||
|
|
||||||
|
const onWorkgroupChange = (formWorkgroup: Workgroup) => {
|
||||||
|
const variables: any = { changes: {} };
|
||||||
|
|
||||||
|
if (workgroup.name !== formWorkgroup.name) {
|
||||||
|
variables.changes.name = formWorkgroup.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(variables.changes).length === 0) return;
|
||||||
|
|
||||||
|
const isCreation = workgroup.id === '';
|
||||||
|
if (isCreation) {
|
||||||
|
createWorkgroup({variables})
|
||||||
|
.then(({ data: { createWorkgroup } }) => {
|
||||||
|
history.push(`/workgroups/${createWorkgroup.id}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
variables.workgroupId = workgroup.id;
|
||||||
|
updateWorkgroup({variables});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="panel">
|
||||||
|
<p className="panel-heading">
|
||||||
|
Informations
|
||||||
|
</p>
|
||||||
|
<div className="panel-block">
|
||||||
|
<WithLoader loading={isLoading}>
|
||||||
|
<InfoForm workgroup={workgroup} onChange={onWorkgroupChange} />
|
||||||
|
</WithLoader>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
|
||||||
|
export interface MembersPanelProps {
|
||||||
|
users: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MembersPanel: FunctionComponent<MembersPanelProps> = ({ users }) => {
|
||||||
|
return (
|
||||||
|
<nav className="panel">
|
||||||
|
<p className="panel-heading">
|
||||||
|
Membres
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
users.map(u => {
|
||||||
|
return (
|
||||||
|
<div key={`user-${u.id}`} className="panel-block">
|
||||||
|
<span className="panel-icon">
|
||||||
|
<i className="fas fa-user" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span>{`${ u.name ? (u.name + ' - ') : '' }`}</span><span className="is-italic">{`${u.email}`}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
users.length === 0 ?
|
||||||
|
<a className="panel-block has-text-centered is-block">
|
||||||
|
<p className="is-italic">Aucun membre pour l'instant.</p>
|
||||||
|
</a> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
import React, { useEffect, useState, Fragment } from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { WithLoader } from '../WithLoader';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
||||||
|
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||||
|
import { MembersPanel } from './MembersPanel';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { InfoPanel } from './InfoPanel';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||||
|
|
||||||
|
export function WorkgroupPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const workgroupsQuery = useWorkgroupsQuery({
|
||||||
|
variables:{
|
||||||
|
filter: {
|
||||||
|
ids: [id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const userProfileQuery = useUserProfileQuery();
|
||||||
|
const [ joinWorkgroup, joinWorkgroupMutation ] = useJoinWorkgroupMutation();
|
||||||
|
const [ leaveWorkgroup, leaveWorkgroupMutation ] = useLeaveWorkgroupMutation();
|
||||||
|
const [ closeWorkgroup, closeWorkgroupMutation ] = useCloseWorkgroupMutation();
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
userProfileId: '',
|
||||||
|
workgroup: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
closedAt: null,
|
||||||
|
createdAt: null,
|
||||||
|
members: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workgroupsQuery.data) return;
|
||||||
|
setState(state => ({...state, workgroup:{ ...state.workgroup, ...workgroupsQuery.data.workgroups[0]}}));
|
||||||
|
}, [workgroupsQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userProfileQuery.data) return;
|
||||||
|
setState(state => ({...state, userProfileId: userProfileQuery.data.userProfile.id }));
|
||||||
|
}, [userProfileQuery.data]);
|
||||||
|
|
||||||
|
const onJoinWorkgroupClick = () => {
|
||||||
|
joinWorkgroup({
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeaveWorkgroupClick = () => {
|
||||||
|
leaveWorkgroup({
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseWorkgroupClick = () => {
|
||||||
|
closeWorkgroup({
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNew = state.workgroup.id === '';
|
||||||
|
const isWorkgroupMember = state.workgroup.members.some(u => u.id === state.userProfileId);
|
||||||
|
const isClosed = state.workgroup.closedAt !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Groupe de travail">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<section className="mt-5">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-left">
|
||||||
|
{
|
||||||
|
isNew ?
|
||||||
|
<div className="level-item">
|
||||||
|
<div>
|
||||||
|
<h2 className="is-size-3 title is-spaced">Nouveau</h2>
|
||||||
|
<h3 className="is-size-5 subtitle">Groupe de travail</h3>
|
||||||
|
</div>
|
||||||
|
</div> :
|
||||||
|
<div className="level-item">
|
||||||
|
<div>
|
||||||
|
<h2 className="is-size-3 title is-spaced">{state.workgroup.name}</h2>
|
||||||
|
<h3 className="is-size-5 subtitle">Groupe de travail <span className="is-italic">{ isClosed ? '(clos)' : null }</span></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<div className="buttons is-right level-item">
|
||||||
|
{
|
||||||
|
isNew || isClosed ? null :
|
||||||
|
<Fragment>
|
||||||
|
{
|
||||||
|
isWorkgroupMember ?
|
||||||
|
<Fragment>
|
||||||
|
<button onClick={onLeaveWorkgroupClick} className="button is-info is-warning is-medium">
|
||||||
|
<span>Quitter</span>
|
||||||
|
<span className="icon"><i className="fas fa-sign-out-alt"></i></span>
|
||||||
|
</button>
|
||||||
|
<button onClick={onCloseWorkgroupClick} className="button is-danger is-medium">
|
||||||
|
<span>Clore</span>
|
||||||
|
<span className="icon"><i className="far fa-times-circle"></i></span>
|
||||||
|
</button>
|
||||||
|
</Fragment> :
|
||||||
|
<button onClick={onJoinWorkgroupClick} className="button is-info is-medium">
|
||||||
|
<span>Rejoindre</span>
|
||||||
|
<span className="icon"><i className="fas fa-user-plus"></i></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WithLoader loading={[workgroupsQuery.loading, userProfileQuery.loading, joinWorkgroupMutation.loading, leaveWorkgroupMutation.loading]}>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<InfoPanel workgroup={state.workgroup as Workgroup} />
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<MembersPanel users={state.workgroup.members as User[]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WithLoader>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
|
||||||
|
import { Config } from '../config';
|
||||||
|
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||||
|
import { RetryLink } from "@apollo/client/link/retry";
|
||||||
|
import { SubscriptionClient } from "subscriptions-transport-ws";
|
||||||
|
|
||||||
|
const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, {
|
||||||
|
reconnect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = new RetryLink({attempts: {max: 2}}).split(
|
||||||
|
(operation) => operation.operationName === 'subscription',
|
||||||
|
new WebSocketLink(subscriptionClient),
|
||||||
|
new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const client = new ApolloClient<any>({
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
link: link,
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||||
|
|
||||||
|
const MUTATION_UPDATE_USER_PROFILE = gql`
|
||||||
|
mutation updateUserProfile($changes: ProfileChanges!) {
|
||||||
|
updateProfile(changes: $changes) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
connectedAt,
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useUpdateUserProfileMutation() {
|
||||||
|
return useMutation(MUTATION_UPDATE_USER_PROFILE);
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||||
|
|
||||||
|
const MUTATION_UPDATE_WORKGROUP = gql`
|
||||||
|
mutation updateWorkgroup($workgroupId: ID!, $changes: WorkgroupChanges!) {
|
||||||
|
updateWorkgroup(workgroupId: $workgroupId, changes: $changes) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useUpdateWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_UPDATE_WORKGROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_CREATE_WORKGROUP = gql`
|
||||||
|
mutation createWorkgroup($changes: WorkgroupChanges!) {
|
||||||
|
createWorkgroup(changes: $changes) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useCreateWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_CREATE_WORKGROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_JOIN_WORKGROUP = gql`
|
||||||
|
mutation joinWorkgroup($workgroupId: ID!) {
|
||||||
|
joinWorkgroup(workgroupId: $workgroupId) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useJoinWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_JOIN_WORKGROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_LEAVE_WORKGROUP = gql`
|
||||||
|
mutation leaveWorkgroup($workgroupId: ID!) {
|
||||||
|
leaveWorkgroup(workgroupId: $workgroupId) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useLeaveWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_LEAVE_WORKGROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_CLOSE_WORKGROUP = gql`
|
||||||
|
mutation closeWorkgroup($workgroupId: ID!) {
|
||||||
|
closeWorkgroup(workgroupId: $workgroupId) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useCloseWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_CLOSE_WORKGROUP);
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
|
||||||
|
const QUERY_USER_PROFILE = gql`
|
||||||
|
query userProfile {
|
||||||
|
userProfile {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
createdAt,
|
||||||
|
connectedAt
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useUserProfileQuery() {
|
||||||
|
return useQuery(QUERY_USER_PROFILE);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
|
||||||
|
const QUERY_WORKGROUP = gql`
|
||||||
|
query workgroups($filter: WorkgroupsFilter) {
|
||||||
|
workgroups(filter: $filter) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useWorkgroupsQuery(options = {}) {
|
||||||
|
return useQuery(QUERY_WORKGROUP, options);
|
||||||
|
}
|
|
@ -2,15 +2,18 @@ import './sass/_all.scss';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { App } from './components/App';
|
import { App } from './components/App';
|
||||||
import { Config } from './config';
|
import { client } from './gql/client';
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||||
import '@fortawesome/fontawesome-free/js/solid'
|
import '@fortawesome/fontawesome-free/js/solid'
|
||||||
import '@fortawesome/fontawesome-free/js/regular'
|
import '@fortawesome/fontawesome-free/js/regular'
|
||||||
import '@fortawesome/fontawesome-free/js/brands'
|
import '@fortawesome/fontawesome-free/js/brands'
|
||||||
import './resources/favicon.png';
|
import './resources/favicon.png';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App />,
|
<ApolloProvider client={client}>
|
||||||
|
<App />
|
||||||
|
</ApolloProvider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
@import 'bulma/bulma.sass';
|
@import 'bulma/bulma.sass';
|
||||||
@import 'bulma-switch/dist/css/bulma-switch.sass';
|
|
||||||
@import '_base.scss';
|
@import '_base.scss';
|
||||||
@import '_loader.scss';
|
@import '_loader.scss';
|
|
@ -1,11 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
|
|
||||||
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
|
|
||||||
|
|
||||||
export interface setCurrentUserAction extends Action {
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCurrentUser(email: string): setCurrentUserAction {
|
|
||||||
return { type: SET_CURRENT_USER, email };
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
import { User } from "../../types/user";
|
|
||||||
|
|
||||||
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
|
|
||||||
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
|
|
||||||
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
|
|
||||||
|
|
||||||
export interface fetchProfileRequestAction extends Action {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface fetchProfileSuccessAction extends Action {
|
|
||||||
profile: User
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function fetchProfile(): fetchProfileRequestAction {
|
|
||||||
return { type: FETCH_PROFILE_REQUEST }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST';
|
|
||||||
export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS';
|
|
||||||
export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE';
|
|
||||||
|
|
||||||
export interface ProfileChanges {
|
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface updateProfileRequestAction extends Action {
|
|
||||||
changes: ProfileChanges
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface updateProfileSuccessAction extends Action {
|
|
||||||
profile: User
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function updateProfile(changes: ProfileChanges): updateProfileRequestAction {
|
|
||||||
return { type: UPDATE_PROFILE_REQUEST, changes }
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
import { User } from "../../types/user";
|
|
||||||
import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth";
|
|
||||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile";
|
|
||||||
|
|
||||||
export interface AuthState {
|
|
||||||
isAuthenticated: boolean
|
|
||||||
currentUser: User
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
isAuthenticated: false,
|
|
||||||
currentUser: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function authReducer(state = defaultState, action: Action): AuthState {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_CURRENT_USER:
|
|
||||||
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
|
||||||
case FETCH_PROFILE_SUCCESS:
|
|
||||||
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
|
|
||||||
case UPDATE_PROFILE_SUCCESS:
|
|
||||||
return handleFetchProfileSuccess(state, action as updateProfileSuccessAction);
|
|
||||||
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
currentUser: {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
currentUser: {
|
|
||||||
...profile,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
currentUser: {
|
|
||||||
...profile,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
|
|
||||||
export interface FlagsState {
|
|
||||||
actions: { [actionName: string]: ActionState }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionState {
|
|
||||||
isLoading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
actions: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function flagsReducer(state = defaultState, action: Action): FlagsState {
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import { flagsReducer, FlagsState } from './flags';
|
|
||||||
import { authReducer, AuthState } from './auth';
|
|
||||||
|
|
||||||
export interface RootState {
|
|
||||||
auth: AuthState,
|
|
||||||
flags: FlagsState,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
|
||||||
flags: flagsReducer,
|
|
||||||
auth: authReducer,
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { UnauthorizedError } from "../../util/daddy";
|
|
||||||
import { all, takeEvery } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
export function* failureRootSaga() {
|
|
||||||
yield all([
|
|
||||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* failuresSaga(action) {
|
|
||||||
if (action.error instanceof UnauthorizedError) {
|
|
||||||
// TODO Implements better authorization error handling
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function patternFromRegExp(re: any) {
|
|
||||||
return (action: any) => {
|
|
||||||
return re.test(action.type);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { all, put } from "redux-saga/effects";
|
|
||||||
import { fetchProfile } from "../actions/profile";
|
|
||||||
|
|
||||||
export function* initRootSaga() {
|
|
||||||
yield all([
|
|
||||||
fetchUserProfileSaga(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* fetchUserProfileSaga() {
|
|
||||||
yield put(fetchProfile());
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { DaddyClient, getClient } from "../../util/daddy";
|
|
||||||
import { Config } from "../../config";
|
|
||||||
import { all, takeLatest, put, select } from "redux-saga/effects";
|
|
||||||
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile";
|
|
||||||
import { SET_CURRENT_USER } from "../actions/auth";
|
|
||||||
import { User } from "../../types/user";
|
|
||||||
|
|
||||||
export function* profileRootSaga() {
|
|
||||||
yield all([
|
|
||||||
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
|
||||||
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
|
||||||
takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* onCurrentUserChangeSaga() {
|
|
||||||
yield put(fetchProfile());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* fetchProfileSaga() {
|
|
||||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
|
||||||
|
|
||||||
let profile: User;
|
|
||||||
try {
|
|
||||||
profile = yield client.fetchProfile().then(result => result.userProfile);
|
|
||||||
} catch(err) {
|
|
||||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* updateProfileSaga({ changes }: updateProfileRequestAction) {
|
|
||||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
|
||||||
|
|
||||||
let profile: User;
|
|
||||||
try {
|
|
||||||
profile = yield client.updateProfile(changes).then(result => result.updateProfile);
|
|
||||||
} catch(err) {
|
|
||||||
yield put({ type: UPDATE_PROFILE_FAILURE, err });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put({type: UPDATE_PROFILE_SUCCESS, profile });
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { all } from 'redux-saga/effects';
|
|
||||||
import { failureRootSaga } from './failure';
|
|
||||||
import { initRootSaga } from './init';
|
|
||||||
import { profileRootSaga } from './profile';
|
|
||||||
|
|
||||||
export function* rootSaga() {
|
|
||||||
yield all([
|
|
||||||
initRootSaga(),
|
|
||||||
failureRootSaga(),
|
|
||||||
profileRootSaga(),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export function selectFlagsIsLoading(state: any, ...actionPrefixes: any[]) {
|
|
||||||
const { actions } = state.flags;
|
|
||||||
return actionPrefixes.reduce((isLoading, prefix) => {
|
|
||||||
if (!(prefix in actions)) return isLoading;
|
|
||||||
return isLoading || actions[prefix].isLoading;
|
|
||||||
}, false);
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { createStore, applyMiddleware, compose } from 'redux'
|
|
||||||
import createSagaMiddleware from 'redux-saga'
|
|
||||||
import { rootReducer } from './reducers/root'
|
|
||||||
import { rootSaga } from './sagas/root'
|
|
||||||
|
|
||||||
let reduxMiddlewares = [];
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
const createLogger = require('redux-logger').createLogger;
|
|
||||||
const loggerMiddleware = createLogger({
|
|
||||||
collapsed: true,
|
|
||||||
diff: true
|
|
||||||
});
|
|
||||||
reduxMiddlewares.push(loggerMiddleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
|
||||||
|
|
||||||
// create the saga middleware
|
|
||||||
const sagaMiddleware = createSagaMiddleware()
|
|
||||||
reduxMiddlewares.push(sagaMiddleware);
|
|
||||||
|
|
||||||
// mount it on the Store
|
|
||||||
export const store = createStore(
|
|
||||||
rootReducer,
|
|
||||||
composeEnhancers(applyMiddleware(...reduxMiddlewares)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// then run the saga
|
|
||||||
sagaMiddleware.run(rootSaga);
|
|
|
@ -1,4 +1,5 @@
|
||||||
export interface User {
|
export interface User {
|
||||||
|
id: string
|
||||||
email: string
|
email: string
|
||||||
name?: string
|
name?: string
|
||||||
connectedAt?: Date
|
connectedAt?: Date
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
|
export interface Workgroup {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
createdAt: Date
|
||||||
|
closedAt: Date
|
||||||
|
members: [User]
|
||||||
|
}
|
|
@ -1,115 +0,0 @@
|
||||||
import ApolloClient, { DefaultOptions } from 'apollo-client';
|
|
||||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
|
||||||
import { split } from 'apollo-link';
|
|
||||||
import { HttpLink } from 'apollo-link-http';
|
|
||||||
import { WebSocketLink } from 'apollo-link-ws';
|
|
||||||
import { getMainDefinition, variablesInOperation } from 'apollo-utilities';
|
|
||||||
import gql from 'graphql-tag';
|
|
||||||
import { ProfileChanges } from '../store/actions/profile';
|
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
|
||||||
constructor(...args: any[]) {
|
|
||||||
super(...args)
|
|
||||||
Object.setPrototypeOf(this, UnauthorizedError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let client: DaddyClient
|
|
||||||
|
|
||||||
export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient {
|
|
||||||
if (!client) {
|
|
||||||
client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DaddyClient {
|
|
||||||
|
|
||||||
gql: ApolloClient<InMemoryCache>
|
|
||||||
|
|
||||||
constructor(graphQLEndpoint: string, subscriptionEndpoint: string) {
|
|
||||||
const wsLink = new WebSocketLink({
|
|
||||||
uri: subscriptionEndpoint,
|
|
||||||
options: {
|
|
||||||
reconnect: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const httpLink = new HttpLink({
|
|
||||||
uri: graphQLEndpoint,
|
|
||||||
fetchOptions: {
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const link = split(
|
|
||||||
({ query }) => {
|
|
||||||
const definition = getMainDefinition(query);
|
|
||||||
return (
|
|
||||||
definition.kind === 'OperationDefinition' &&
|
|
||||||
definition.operation === 'subscription'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
wsLink,
|
|
||||||
httpLink,
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultOptions: DefaultOptions = {
|
|
||||||
watchQuery: {
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
errorPolicy: 'ignore',
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
errorPolicy: 'all',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.gql = new ApolloClient<any>({
|
|
||||||
link: link,
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
defaultOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProfile() {
|
|
||||||
return this.gql.query({
|
|
||||||
query: gql`
|
|
||||||
query {
|
|
||||||
userProfile {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
createdAt,
|
|
||||||
connectedAt
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
})
|
|
||||||
.then(this.assertAuthorization)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProfile(changes: ProfileChanges) {
|
|
||||||
return this.gql.mutate({
|
|
||||||
variables: {
|
|
||||||
changes,
|
|
||||||
},
|
|
||||||
mutation: gql`
|
|
||||||
mutation updateProfile($changes: ProfileChanges!) {
|
|
||||||
updateProfile(changes: $changes) {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
createdAt,
|
|
||||||
connectedAt
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
})
|
|
||||||
.then(this.assertAuthorization)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertAuthorization({ status, data }: any) {
|
|
||||||
if (status === 401) return Promise.reject(new UnauthorizedError());
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -79,6 +79,7 @@ func applyMigration(ctx context.Context, ctn *service.Container) error {
|
||||||
// nolint: gochecknoglobals
|
// nolint: gochecknoglobals
|
||||||
var initialModels = []interface{}{
|
var initialModels = []interface{}{
|
||||||
&model.User{},
|
&model.User{},
|
||||||
|
&model.Workgroup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
func m000initialSchema() orm.Migration {
|
func m000initialSchema() orm.Migration {
|
||||||
|
|
|
@ -3,7 +3,9 @@ package graph
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -23,3 +25,24 @@ func getDB(ctx context.Context) (*gorm.DB, error) {
|
||||||
|
|
||||||
return orm.DB(), nil
|
return orm.DB(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSessionUser(ctx context.Context) (*model.User, *gorm.DB, error) {
|
||||||
|
db, err := getDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmail, err := session.UserEmail(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewUserRepository(db)
|
||||||
|
|
||||||
|
user, err := repo.FindUserByEmail(ctx, userEmail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, db, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,15 @@ input ProfileChanges {
|
||||||
name: String
|
name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input WorkgroupChanges {
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
updateProfile(changes: ProfileChanges!): User!
|
updateProfile(changes: ProfileChanges!): User!
|
||||||
|
joinWorkgroup(workgroupId: ID!): Workgroup!
|
||||||
|
leaveWorkgroup(workgroupId: ID!): Workgroup!
|
||||||
|
createWorkgroup(changes: WorkgroupChanges!): Workgroup!
|
||||||
|
closeWorkgroup(workgroupId: ID!): Workgroup!
|
||||||
|
updateWorkgroup(workgroupId: ID!, changes: WorkgroupChanges!): Workgroup!
|
||||||
}
|
}
|
|
@ -14,6 +14,26 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.Prof
|
||||||
return handleUpdateUserProfile(ctx, changes)
|
return handleUpdateUserProfile(ctx, changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) JoinWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
|
return handleJoinWorkgroup(ctx, workgroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) LeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
|
return handleLeaveWorkgroup(ctx, workgroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
|
return handleCreateWorkgroup(ctx, changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
|
return handleCloseWorkgroup(ctx, workgroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) UpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
|
return handleUpdateWorkgroup(ctx, workgroupID, changes)
|
||||||
|
}
|
||||||
|
|
||||||
// Mutation returns generated.MutationResolver implementation.
|
// Mutation returns generated.MutationResolver implementation.
|
||||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
scalar Time
|
scalar Time
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
email: String!
|
email: String!
|
||||||
connectedAt: Time!
|
connectedAt: Time!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
|
workgroups:[Workgroup]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Workgroup {
|
||||||
|
id: ID!
|
||||||
|
name: String
|
||||||
|
createdAt: Time!
|
||||||
|
closedAt: Time
|
||||||
|
members: [User]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input WorkgroupsFilter {
|
||||||
|
ids: [ID]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
userProfile: User
|
userProfile: User
|
||||||
|
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||||
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
|
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
@ -14,7 +15,27 @@ func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||||
return handleUserProfile(ctx)
|
return handleUserProfile(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Workgroups(ctx context.Context, filter *model1.WorkgroupsFilter) ([]*model1.Workgroup, error) {
|
||||||
|
return handleWorkgroups(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) {
|
||||||
|
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *workgroupResolver) ID(ctx context.Context, obj *model1.Workgroup) (string, error) {
|
||||||
|
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Query returns generated.QueryResolver implementation.
|
// Query returns generated.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
// User returns generated.UserResolver implementation.
|
||||||
|
func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
|
||||||
|
|
||||||
|
// Workgroup returns generated.WorkgroupResolver implementation.
|
||||||
|
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
|
||||||
|
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
type userResolver struct{ *Resolver }
|
||||||
|
type workgroupResolver struct{ *Resolver }
|
||||||
|
|
|
@ -3,26 +3,12 @@ package graph
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||||
db, err := getDB(ctx)
|
user, _, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userEmail, err := session.UserEmail(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewUserRepository(db)
|
|
||||||
|
|
||||||
user, err := repo.FindUserByEmail(ctx, userEmail)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -31,12 +17,7 @@ func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||||
db, err := getDB(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userEmail, err := session.UserEmail(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +30,7 @@ func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges)
|
||||||
userChanges.Name = changes.Name
|
userChanges.Name = changes.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges)
|
user, err = repo.UpdateUserByEmail(ctx, user.Email, userChanges)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*model.Workgroup, error) {
|
||||||
|
db, err := getDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
criteria := make([]interface{}, 0)
|
||||||
|
|
||||||
|
if filter != nil {
|
||||||
|
if len(filter.Ids) > 0 {
|
||||||
|
criteria = append(criteria, "id in (?)", filter.Ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroups, err := repo.FindWorkgroups(ctx, criteria...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||||
|
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, db, err := getSessionUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
workgroup, err := repo.AddUserToWorkgroup(ctx, user.ID, workgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||||
|
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, db, err := getSessionUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
workgroup, err := repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
|
db, err := getDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
workgroup, err := repo.CreateWorkgroup(ctx, changes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||||
|
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := getDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
workgroup, err := repo.CloseWorkgroup(ctx, workgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
|
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := getDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := model.NewWorkgroupRepository(db)
|
||||||
|
|
||||||
|
workgroup, err := repo.UpdateWorkgroup(ctx, workgroupID, changes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWorkgroupID(workgroupID string) (uint, error) {
|
||||||
|
workgroupID64, err := strconv.ParseUint(workgroupID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(workgroupID64), nil
|
||||||
|
}
|
|
@ -1,13 +1,17 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID *uint `gorm:"primary_key"`
|
gorm.Model
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Email string `json:"email" gorm:"unique;not null"`
|
Email string `json:"email" gorm:"unique;not null"`
|
||||||
ConnectedAt time.Time `json:"connectedAt"`
|
ConnectedAt time.Time `json:"connectedAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileChanges struct {
|
type ProfileChanges struct {
|
||||||
|
|
|
@ -15,8 +15,7 @@ type UserRepository struct {
|
||||||
|
|
||||||
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
|
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
|
||||||
user := &User{
|
user := &User{
|
||||||
Email: email,
|
Email: email,
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||||
|
@ -44,7 +43,7 @@ func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*Us
|
||||||
Email: email,
|
Email: email,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.db.First(user, "email = ?", email).Error
|
err := r.db.Model(user).Preload("Workgroups").First(user, "email = ?", email).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "could not find user")
|
return nil, errors.Wrap(err, "could not find user")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Workgroup struct {
|
||||||
|
gorm.Model
|
||||||
|
Name *string `json:"name"`
|
||||||
|
ClosedAt time.Time `json:"closedAt"`
|
||||||
|
Members []*User `gorm:"many2many:users_workgroups;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkgroupChanges struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkgroupRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) FindWorkgroups(ctx context.Context, criteria ...interface{}) ([]*Workgroup, error) {
|
||||||
|
workgroups := make([]*Workgroup, 0)
|
||||||
|
if err := r.db.Model(&Workgroup{}).Preload("Members").Find(&workgroups, criteria...).Error; err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) UpdateWorkgroup(ctx context.Context, workgroupID uint, changes WorkgroupChanges) (*Workgroup, error) {
|
||||||
|
workgroup := &Workgroup{
|
||||||
|
Name: changes.Name,
|
||||||
|
}
|
||||||
|
workgroup.ID = workgroupID
|
||||||
|
|
||||||
|
err := r.db.Model(workgroup).
|
||||||
|
Update(workgroup).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) CreateWorkgroup(ctx context.Context, changes WorkgroupChanges) (*Workgroup, error) {
|
||||||
|
workgroup := &Workgroup{
|
||||||
|
Name: changes.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.db.Model(&Workgroup{}).Create(workgroup).Error; err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) CloseWorkgroup(ctx context.Context, workgroupID uint) (*Workgroup, error) {
|
||||||
|
workgroup := &Workgroup{}
|
||||||
|
|
||||||
|
err := r.db.Model(workgroup).
|
||||||
|
Where("id = ?", workgroupID).
|
||||||
|
UpdateColumn("closedAt", time.Now()).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||||
|
user := &User{}
|
||||||
|
|
||||||
|
err := r.db.Model(user).Preload("Workgroups").First(user, "id = ?", userID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not find user")
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroup := &Workgroup{}
|
||||||
|
workgroup.ID = workgroupID
|
||||||
|
|
||||||
|
err = r.db.Model(user).
|
||||||
|
Association("Workgroups").
|
||||||
|
Append(workgroup).
|
||||||
|
Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not add user to workgroup")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.db.Model(workgroup).
|
||||||
|
Preload("Members").
|
||||||
|
First(workgroup, "id = ?", workgroupID).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||||
|
user := &User{}
|
||||||
|
|
||||||
|
err := r.db.First(user, "id = ?", userID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not find user")
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroup := &Workgroup{}
|
||||||
|
workgroup.ID = workgroupID
|
||||||
|
|
||||||
|
err = r.db.Model(user).
|
||||||
|
Association("Workgroups").
|
||||||
|
Delete(workgroup).
|
||||||
|
Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not add user to workgroup")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.db.Model(workgroup).
|
||||||
|
Preload("Members").
|
||||||
|
First(workgroup, "id = ?", workgroupID).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return workgroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository {
|
||||||
|
return &WorkgroupRepository{db}
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ func Mount(r *chi.Mux, config *config.Config) error {
|
||||||
AllowCredentials: config.HTTP.CORS.AllowCredentials,
|
AllowCredentials: config.HTTP.CORS.AllowCredentials,
|
||||||
Debug: config.Debug,
|
Debug: config.Debug,
|
||||||
}).Handler)
|
}).Handler)
|
||||||
r.Use(oidc.Middleware)
|
|
||||||
r.Use(session.UserEmailMiddleware)
|
r.Use(session.UserEmailMiddleware)
|
||||||
|
|
||||||
gql := handler.New(
|
gql := handler.New(
|
||||||
|
|
|
@ -21,7 +21,9 @@ func UserEmailMiddleware(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
userEmail, err := GetUserEmail(w, r)
|
userEmail, err := GetUserEmail(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrap(err, "could not find user email"))
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := WithUserEmail(r.Context(), userEmail)
|
ctx := WithUserEmail(r.Context(), userEmail)
|
||||||
|
|
Loading…
Reference in New Issue