diff --git a/client/package-lock.json b/client/package-lock.json
index 635b1c5..8552a0f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -4,6 +4,43 @@
"lockfileVersion": 1,
"requires": true,
"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": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@@ -1230,7 +1267,8 @@
"@types/node": {
"version": "13.13.12",
"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": {
"version": "15.7.3",
@@ -1551,15 +1589,6 @@
"@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": {
"version": "0.1.11",
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -3235,14 +3177,9 @@
"dev": true
},
"bulma": {
- "version": "0.7.5",
- "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz",
- "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw=="
- },
- "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=="
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
+ "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
},
"bytes": {
"version": "3.0.0",
@@ -5615,11 +5552,6 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
"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": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
@@ -6507,11 +6439,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -7466,14 +7393,6 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
@@ -8048,11 +7967,6 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -11006,15 +10920,6 @@
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
"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"
- }
}
}
}
diff --git a/client/package.json b/client/package.json
index d5cf484..ad76110 100644
--- a/client/package.json
+++ b/client/package.json
@@ -51,20 +51,10 @@
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
+ "@apollo/client": "^3.0.2",
"@types/qs": "^6.9.3",
- "apollo-cache-inmemory": "^1.6.6",
- "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",
+ "bulma": "^0.9.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-dom": "^16.12.0",
"react-redux": "^7.1.3",
diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx
index ec0367f..89e3dca 100644
--- a/client/src/components/App.tsx
+++ b/client/src/components/App.tsx
@@ -1,22 +1,20 @@
import React from 'react';
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import { HomePage } from './HomePage/HomePage';
-import { store } from '../store/store';
-import { Provider } from 'react-redux';
import { ProfilePage } from './ProfilePage/ProfilePage';
+import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage';
export class App extends React.Component {
render() {
return (
-
-
-
-
-
- } />
-
-
-
+
+
+
+
+
+ } />
+
+
);
}
}
\ No newline at end of file
diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/HomePage/Dashboard.tsx
new file mode 100644
index 0000000..916fba1
--- /dev/null
+++ b/client/src/components/HomePage/Dashboard.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { WorkgroupsPanel } from './WorkgroupsPanel';
+
+export function Dashboard() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx
index 7607cee..2e8e26a 100644
--- a/client/src/components/HomePage/HomePage.tsx
+++ b/client/src/components/HomePage/HomePage.tsx
@@ -1,26 +1,31 @@
-import React, { useEffect } from 'react';
+import React from 'react';
import { Page } from '../Page';
-import { useSelector, useDispatch } from 'react-redux';
-import { RootState } from '../../store/reducers/root';
+import { Dashboard } from './Dashboard';
+import { useUserProfileQuery } from '../../gql/queries/profile';
+import { WithLoader } from '../WithLoader';
export function HomePage() {
- const currentUser = useSelector((state: RootState) => state.auth.currentUser);
+ const { data, loading } = useUserProfileQuery();
+
+ const { userProfile } = (data || {});
return (
-
+
-
-
-
-
- {
- currentUser && currentUser.email ?
-
Bonjour {currentUser.name ? currentUser.name : currentUser.email} !
:
-
Veuillez vous authentifier.
- }
+
+
+ {
+ userProfile ?
+ :
+
+
+
+
Veuillez vous authentifier.
+
-
-
+
+ }
+
diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx
new file mode 100644
index 0000000..439b931
--- /dev/null
+++ b/client/src/components/HomePage/WorkgroupsPanel.tsx
@@ -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 (
+
+
+
+
+ {wg.name}
+
+ );
+ });
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/client/src/components/Loader.tsx b/client/src/components/Loader.tsx
deleted file mode 100644
index 5632248..0000000
--- a/client/src/components/Loader.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-export class Loader extends React.Component {
- render() {
- return (
-
- )
- }
-}
\ No newline at end of file
diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx
index aebbf99..0bd718f 100644
--- a/client/src/components/Navbar.tsx
+++ b/client/src/components/Navbar.tsx
@@ -1,13 +1,13 @@
import React, { Fragment, useState } from 'react';
import logo from '../resources/logo.svg';
import { useSelector } from 'react-redux';
-import { RootState } from '../store/reducers/root';
import { Config } from '../config';
import { Link } from 'react-router-dom';
+import { useUserProfileQuery } from '../gql/queries/profile';
+import { WithLoader } from './WithLoader';
export function Navbar() {
- const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
-
+ const userProfileQuery = useUserProfileQuery();
const [ isActive, setActive ] = useState(false);
const toggleMenu = () => {
@@ -35,28 +35,30 @@ export function Navbar() {
-
- {
- isAuthenticated ?
-
-
+
+
+ }
+
+
diff --git a/client/src/components/ProfilePage/ProfilePage.tsx b/client/src/components/ProfilePage/ProfilePage.tsx
index 54c72bf..9194d24 100644
--- a/client/src/components/ProfilePage/ProfilePage.tsx
+++ b/client/src/components/ProfilePage/ProfilePage.tsx
@@ -1,19 +1,21 @@
-import React, { useEffect } from 'react';
+import React from 'react';
import { Page } from '../Page';
-import { useSelector, useDispatch } from 'react-redux';
-import { RootState } from '../../store/reducers/root';
import { UserForm } from '../UserForm';
-import { Loader } from '../Loader';
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() {
- const currentUser = useSelector((state: RootState) => state.auth.currentUser);
- const dispatch = useDispatch();
+ const userProfileQuery = useUserProfileQuery();
+ const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
+ const isLoading = updateUserProfileMutation.loading || userProfileQuery.loading;
+
+ const { userProfile } = (userProfileQuery.data || {});
const onUserChange = (user: User) => {
- if (currentUser.name !== user.name) {
- dispatch(updateProfile({ name: user.name }))
+ if (userProfile.name !== user.name) {
+ updateProfile({ variables: {changes: { name: user.name }}});
}
};
@@ -24,11 +26,11 @@ export function ProfilePage() {
Mon profil
+
{
- currentUser ?
- :
-
+
}
+
diff --git a/client/src/components/UserForm.tsx b/client/src/components/UserForm.tsx
index 1ba6c7a..8e94f4e 100644
--- a/client/src/components/UserForm.tsx
+++ b/client/src/components/UserForm.tsx
@@ -10,11 +10,11 @@ export function UserForm({ user, onChange }: UserFormProps) {
const [ state, setState ] = useState({
changed: false,
user: {
- name: '',
- email: '',
- createdAt: null,
- connectedAt: null,
- ...user,
+ id: user && user.id ? user.id : '',
+ name: user && user.name ? user.name : '',
+ email: user && user.email ? user.email : '',
+ createdAt: user && user.createdAt ? user.createdAt : null,
+ connectedAt: user && user.connectedAt ? user.connectedAt : null,
}
});
diff --git a/client/src/components/WithLoader.tsx b/client/src/components/WithLoader.tsx
new file mode 100644
index 0000000..cbc55fb
--- /dev/null
+++ b/client/src/components/WithLoader.tsx
@@ -0,0 +1,18 @@
+import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
+
+export interface WithLoaderProps {
+ loading?: boolean|boolean[]
+}
+
+export const WithLoader: FunctionComponent
= ({ loading, children }) => {
+ const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
+ return (
+
+ {
+ isLoading ?
+ Chargement
:
+ children
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/client/src/components/WorkgroupPage/InfoForm.tsx b/client/src/components/WorkgroupPage/InfoForm.tsx
new file mode 100644
index 0000000..cb3a3f8
--- /dev/null
+++ b/client/src/components/WorkgroupPage/InfoForm.tsx
@@ -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) {
+ const value = evt.currentTarget.value;
+ setState(state => {
+ return {
+ ...state,
+ changed: true,
+ workgroup: {
+ ...state.workgroup,
+ [attrName]: value,
+ }
+ };
+ });
+ };
+
+ return (
+
+
+ {
+ state.workgroup.createdAt ?
+
+
+
+
{state.workgroup.createdAt}
+
+
:
+ null
+ }
+ {
+ state.workgroup.closedAt ?
+
+
+
+
{state.workgroup.closedAt}
+
+
:
+ null
+ }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/WorkgroupPage/InfoPanel.tsx b/client/src/components/WorkgroupPage/InfoPanel.tsx
new file mode 100644
index 0000000..ac69d55
--- /dev/null
+++ b/client/src/components/WorkgroupPage/InfoPanel.tsx
@@ -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 = ({ 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 (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/WorkgroupPage/MembersPanel.tsx b/client/src/components/WorkgroupPage/MembersPanel.tsx
new file mode 100644
index 0000000..7092ace
--- /dev/null
+++ b/client/src/components/WorkgroupPage/MembersPanel.tsx
@@ -0,0 +1,35 @@
+import React, { FunctionComponent } from 'react';
+import { User } from '../../types/user';
+
+export interface MembersPanelProps {
+ users: User[]
+}
+
+export const MembersPanel: FunctionComponent = ({ users }) => {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/WorkgroupPage/WorkgroupPage.tsx b/client/src/components/WorkgroupPage/WorkgroupPage.tsx
new file mode 100644
index 0000000..a0a114c
--- /dev/null
+++ b/client/src/components/WorkgroupPage/WorkgroupPage.tsx
@@ -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 (
+
+
+
+
+
+ {
+ isNew ?
+
+
+
Nouveau
+ Groupe de travail
+
+
:
+
+
+
{state.workgroup.name}
+ Groupe de travail { isClosed ? '(clos)' : null }
+
+
+ }
+
+
+
+ {
+ isNew || isClosed ? null :
+
+ {
+ isWorkgroupMember ?
+
+
+
+ :
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/gql/client.tsx b/client/src/gql/client.tsx
new file mode 100644
index 0000000..3aff245
--- /dev/null
+++ b/client/src/gql/client.tsx
@@ -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({
+ cache: new InMemoryCache(),
+ link: link,
+});
\ No newline at end of file
diff --git a/client/src/gql/mutations/profile.tsx b/client/src/gql/mutations/profile.tsx
new file mode 100644
index 0000000..9a681a4
--- /dev/null
+++ b/client/src/gql/mutations/profile.tsx
@@ -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);
+}
\ No newline at end of file
diff --git a/client/src/gql/mutations/workgroups.tsx b/client/src/gql/mutations/workgroups.tsx
new file mode 100644
index 0000000..b9f6770
--- /dev/null
+++ b/client/src/gql/mutations/workgroups.tsx
@@ -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);
+}
\ No newline at end of file
diff --git a/client/src/gql/queries/profile.tsx b/client/src/gql/queries/profile.tsx
new file mode 100644
index 0000000..573fc04
--- /dev/null
+++ b/client/src/gql/queries/profile.tsx
@@ -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);
+}
\ No newline at end of file
diff --git a/client/src/gql/queries/workgroups.tsx b/client/src/gql/queries/workgroups.tsx
new file mode 100644
index 0000000..4693edd
--- /dev/null
+++ b/client/src/gql/queries/workgroups.tsx
@@ -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);
+}
\ No newline at end of file
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 9dde128..cee36c1 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -2,15 +2,18 @@ import './sass/_all.scss';
import React from 'react';
import ReactDOM from 'react-dom';
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/solid'
import '@fortawesome/fontawesome-free/js/regular'
import '@fortawesome/fontawesome-free/js/brands'
import './resources/favicon.png';
+import { ApolloProvider } from '@apollo/client';
ReactDOM.render(
- ,
+
+
+ ,
document.getElementById('app')
);
diff --git a/client/src/sass/_all.scss b/client/src/sass/_all.scss
index 8f13fd0..c08fa8c 100644
--- a/client/src/sass/_all.scss
+++ b/client/src/sass/_all.scss
@@ -1,4 +1,3 @@
@import 'bulma/bulma.sass';
-@import 'bulma-switch/dist/css/bulma-switch.sass';
@import '_base.scss';
@import '_loader.scss';
\ No newline at end of file
diff --git a/client/src/store/actions/auth.ts b/client/src/store/actions/auth.ts
deleted file mode 100644
index 8a8c608..0000000
--- a/client/src/store/actions/auth.ts
+++ /dev/null
@@ -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 };
-}
\ No newline at end of file
diff --git a/client/src/store/actions/profile.ts b/client/src/store/actions/profile.ts
deleted file mode 100644
index 8e51900..0000000
--- a/client/src/store/actions/profile.ts
+++ /dev/null
@@ -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 }
-}
\ No newline at end of file
diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts
deleted file mode 100644
index 2192326..0000000
--- a/client/src/store/reducers/auth.ts
+++ /dev/null
@@ -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,
- }
- };
-};
\ No newline at end of file
diff --git a/client/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts
deleted file mode 100644
index f42701b..0000000
--- a/client/src/store/reducers/flags.ts
+++ /dev/null
@@ -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'
- }
- }
- };
-
-}
\ No newline at end of file
diff --git a/client/src/store/reducers/root.ts b/client/src/store/reducers/root.ts
deleted file mode 100644
index 5835800..0000000
--- a/client/src/store/reducers/root.ts
+++ /dev/null
@@ -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,
-});
\ No newline at end of file
diff --git a/client/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts
deleted file mode 100644
index de90822..0000000
--- a/client/src/store/sagas/failure.ts
+++ /dev/null
@@ -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);
- };
-}
\ No newline at end of file
diff --git a/client/src/store/sagas/init.ts b/client/src/store/sagas/init.ts
deleted file mode 100644
index b809870..0000000
--- a/client/src/store/sagas/init.ts
+++ /dev/null
@@ -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());
-}
\ No newline at end of file
diff --git a/client/src/store/sagas/profile.ts b/client/src/store/sagas/profile.ts
deleted file mode 100644
index 9e0864a..0000000
--- a/client/src/store/sagas/profile.ts
+++ /dev/null
@@ -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 });
-}
\ No newline at end of file
diff --git a/client/src/store/sagas/root.ts b/client/src/store/sagas/root.ts
deleted file mode 100644
index 6a1c770..0000000
--- a/client/src/store/sagas/root.ts
+++ /dev/null
@@ -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(),
- ]);
-}
diff --git a/client/src/store/selectors/flags.ts b/client/src/store/selectors/flags.ts
deleted file mode 100644
index 1f51cb4..0000000
--- a/client/src/store/selectors/flags.ts
+++ /dev/null
@@ -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);
-};
\ No newline at end of file
diff --git a/client/src/store/store.ts b/client/src/store/store.ts
deleted file mode 100644
index 911fd81..0000000
--- a/client/src/store/store.ts
+++ /dev/null
@@ -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);
\ No newline at end of file
diff --git a/client/src/types/user.ts b/client/src/types/user.ts
index 4fc773d..2fae681 100644
--- a/client/src/types/user.ts
+++ b/client/src/types/user.ts
@@ -1,4 +1,5 @@
export interface User {
+ id: string
email: string
name?: string
connectedAt?: Date
diff --git a/client/src/types/workgroup.ts b/client/src/types/workgroup.ts
new file mode 100644
index 0000000..459f29a
--- /dev/null
+++ b/client/src/types/workgroup.ts
@@ -0,0 +1,9 @@
+import { User } from "./user";
+
+export interface Workgroup {
+ id: string
+ name: string
+ createdAt: Date
+ closedAt: Date
+ members: [User]
+}
\ No newline at end of file
diff --git a/client/src/util/daddy.ts b/client/src/util/daddy.ts
deleted file mode 100644
index 8c439fd..0000000
--- a/client/src/util/daddy.ts
+++ /dev/null
@@ -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
-
- 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({
- 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;
- }
-
-}
\ No newline at end of file
diff --git a/cmd/server/migration.go b/cmd/server/migration.go
index 083ee3e..f4fc39a 100644
--- a/cmd/server/migration.go
+++ b/cmd/server/migration.go
@@ -79,6 +79,7 @@ func applyMigration(ctx context.Context, ctn *service.Container) error {
// nolint: gochecknoglobals
var initialModels = []interface{}{
&model.User{},
+ &model.Workgroup{},
}
func m000initialSchema() orm.Migration {
diff --git a/internal/graph/helper.go b/internal/graph/helper.go
index 73ec676..3351cc7 100644
--- a/internal/graph/helper.go
+++ b/internal/graph/helper.go
@@ -3,7 +3,9 @@ package graph
import (
"context"
+ "forge.cadoles.com/Cadoles/daddy/internal/model"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
+ "forge.cadoles.com/Cadoles/daddy/internal/session"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
@@ -23,3 +25,24 @@ func getDB(ctx context.Context) (*gorm.DB, error) {
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
+}
diff --git a/internal/graph/mutation.graphql b/internal/graph/mutation.graphql
index 3f0f71a..7dcc5da 100644
--- a/internal/graph/mutation.graphql
+++ b/internal/graph/mutation.graphql
@@ -2,6 +2,15 @@ input ProfileChanges {
name: String
}
+input WorkgroupChanges {
+ name: String
+}
+
type Mutation {
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!
}
\ No newline at end of file
diff --git a/internal/graph/mutation.resolvers.go b/internal/graph/mutation.resolvers.go
index c979e19..7f8872c 100644
--- a/internal/graph/mutation.resolvers.go
+++ b/internal/graph/mutation.resolvers.go
@@ -14,6 +14,26 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.Prof
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.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
diff --git a/internal/graph/query.graphql b/internal/graph/query.graphql
index b11434c..bca4f56 100644
--- a/internal/graph/query.graphql
+++ b/internal/graph/query.graphql
@@ -1,12 +1,27 @@
scalar Time
type User {
+ id: ID!
name: String
email: String!
connectedAt: Time!
createdAt: Time!
+ workgroups:[Workgroup]!
+}
+
+type Workgroup {
+ id: ID!
+ name: String
+ createdAt: Time!
+ closedAt: Time
+ members: [User]!
+}
+
+input WorkgroupsFilter {
+ ids: [ID]
}
type Query {
userProfile: User
+ workgroups(filter: WorkgroupsFilter): [Workgroup]!
}
diff --git a/internal/graph/query.resolvers.go b/internal/graph/query.resolvers.go
index 33af4b3..f73897e 100644
--- a/internal/graph/query.resolvers.go
+++ b/internal/graph/query.resolvers.go
@@ -5,6 +5,7 @@ package graph
import (
"context"
+ "strconv"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
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)
}
+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.
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 userResolver struct{ *Resolver }
+type workgroupResolver struct{ *Resolver }
diff --git a/internal/graph/user_profile.go b/internal/graph/user_profile_handler.go
similarity index 55%
rename from internal/graph/user_profile.go
rename to internal/graph/user_profile_handler.go
index 8298352..62564ea 100644
--- a/internal/graph/user_profile.go
+++ b/internal/graph/user_profile_handler.go
@@ -3,26 +3,12 @@ package graph
import (
"context"
- "forge.cadoles.com/Cadoles/daddy/internal/session"
-
"forge.cadoles.com/Cadoles/daddy/internal/model"
"github.com/pkg/errors"
)
func handleUserProfile(ctx context.Context) (*model.User, error) {
- db, err := getDB(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)
+ user, _, err := getSessionUser(ctx)
if err != nil {
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) {
- db, err := getDB(ctx)
- if err != nil {
- return nil, errors.WithStack(err)
- }
-
- userEmail, err := session.UserEmail(ctx)
+ user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
@@ -49,7 +30,7 @@ func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges)
userChanges.Name = changes.Name
}
- user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges)
+ user, err = repo.UpdateUserByEmail(ctx, user.Email, userChanges)
if err != nil {
return nil, errors.WithStack(err)
}
diff --git a/internal/graph/workgroup_handler.go b/internal/graph/workgroup_handler.go
new file mode 100644
index 0000000..07f89dd
--- /dev/null
+++ b/internal/graph/workgroup_handler.go
@@ -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
+}
diff --git a/internal/model/user.go b/internal/model/user.go
index f2877b5..79a88e0 100644
--- a/internal/model/user.go
+++ b/internal/model/user.go
@@ -1,13 +1,17 @@
package model
-import "time"
+import (
+ "time"
+
+ "github.com/jinzhu/gorm"
+)
type User struct {
- ID *uint `gorm:"primary_key"`
- Name *string `json:"name"`
- Email string `json:"email" gorm:"unique;not null"`
- ConnectedAt time.Time `json:"connectedAt"`
- CreatedAt time.Time `json:"createdAt"`
+ gorm.Model
+ Name *string `json:"name"`
+ Email string `json:"email" gorm:"unique;not null"`
+ ConnectedAt time.Time `json:"connectedAt"`
+ Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
}
type ProfileChanges struct {
diff --git a/internal/model/user_repository.go b/internal/model/user_repository.go
index ec24928..f84a70c 100644
--- a/internal/model/user_repository.go
+++ b/internal/model/user_repository.go
@@ -15,8 +15,7 @@ type UserRepository struct {
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
user := &User{
- Email: email,
- CreatedAt: time.Now(),
+ Email: email,
}
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,
}
- err := r.db.First(user, "email = ?", email).Error
+ err := r.db.Model(user).Preload("Workgroups").First(user, "email = ?", email).Error
if err != nil {
return nil, errors.Wrap(err, "could not find user")
}
diff --git a/internal/model/workgroup.go b/internal/model/workgroup.go
new file mode 100644
index 0000000..64b2c0a
--- /dev/null
+++ b/internal/model/workgroup.go
@@ -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"`
+}
diff --git a/internal/model/workgroup_repository.go b/internal/model/workgroup_repository.go
new file mode 100644
index 0000000..283c1fe
--- /dev/null
+++ b/internal/model/workgroup_repository.go
@@ -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}
+}
diff --git a/internal/route/mount.go b/internal/route/mount.go
index 973424d..af4d08b 100644
--- a/internal/route/mount.go
+++ b/internal/route/mount.go
@@ -33,7 +33,6 @@ func Mount(r *chi.Mux, config *config.Config) error {
AllowCredentials: config.HTTP.CORS.AllowCredentials,
Debug: config.Debug,
}).Handler)
- r.Use(oidc.Middleware)
r.Use(session.UserEmailMiddleware)
gql := handler.New(
diff --git a/internal/session/user_email.go b/internal/session/user_email.go
index b1dc0c3..171f23e 100644
--- a/internal/session/user_email.go
+++ b/internal/session/user_email.go
@@ -21,7 +21,9 @@ func UserEmailMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
userEmail, err := GetUserEmail(w, r)
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)