️ This Gitlab will be shut down at 2021-12-31 23:59:59.
Students and staff can migrate to gitlab.utwente.nl.
SNT members can migrate to gitlab.snt.utwente.nl.
Contact bestuur@snt.utwente.nl for more information.

Migrate your projects today!
Export your project using the webinterface or use a script.

Commit 81b91b6b authored by TJHeeringa's avatar TJHeeringa

Init for RtkQuery

parent 599c4137
......@@ -17,8 +17,8 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import CssBaseline from "@material-ui/core/CssBaseline";
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import AlertHandlerProvider from "./Contexts/AlertHandler";
import API from "./Contexts/API";
......@@ -36,22 +36,20 @@ library.add(faSignOutAlt, faUser, faIgloo, faBars, faCheck, faTimes, faHome,
const App = () => {
return (
<div className={"App"}>
<AlertHandlerProvider>
<Provider store={store}>
<Router>
<AlertHandlerProvider>
<API>
<ThemeProvider>
<CssBaseline/>
<LocaleProvider>
<Routing/>
</LocaleProvider>
</ThemeProvider>
</API>
</AlertHandlerProvider>
<API>
<ThemeProvider>
<CssBaseline/>
<LocaleProvider>
<Routing/>
</LocaleProvider>
</ThemeProvider>
</API>
</Router>
</Provider>
</div>
</AlertHandlerProvider>
);
};
......
import PropTypes from "prop-types";
import React from "react";
import {TextValidator} from "react-material-ui-form-validator";
import useFieldStyles from "./fieldStyles";
const TextField = ({name, value, ...remaining_props}) => {
......
......@@ -8,7 +8,7 @@ const useCurrentAssociation = () => {
const associations = useSelector((state) => selectAssociations(state));
const pathParts = pathname.split("/");
console.log(associations, pathParts);
if (!pathParts.includes("associations") || pathParts.length < 3) {
return {};
}
......
......@@ -22,8 +22,8 @@ const AssociationInfo = ({ association }) => {
"Snapchat": association.snapchat,
};
data["Visiting Location"] = {
"Zip Code": association.zip_code + (" ") + association.city,
"Address": association.address,
"Zip Code": association.zip_code + (" ") + association.city,
"Room": association.room,
};
data["Postal"] = {
......@@ -36,7 +36,7 @@ const AssociationInfo = ({ association }) => {
"Founding date": association.founding_date,
};
data["Legal"] = {
"Privacy Statement": <a href={association.privacy_statements}>Privacy Statement</a>,
"Privacy Statement": <a href={association.privacy_statement}>Privacy Statement</a>,
"Terms of Service": <a href={association.terms_of_service}>Terms of Service</a>
};
......
import PropTypes from "prop-types";
import React, {useMemo} from "react";
import {useStudies} from "../../Contexts/Studies";
import {useGetStudiesQuery} from "../../Store/services/studies";
import Info from "./Info";
const ProfileInfo = ({ profile }) => {
const StudiesApi = useStudies();
const {data: studies} = useGetStudiesQuery();
const data = useMemo(()=>{
let study = profile.study ? profile.study.name : "Not Listed";
if (StudiesApi.collection && StudiesApi.collection.length > 0) {
study = study || StudiesApi.collection.find(existing_study => existing_study.url === profile.study).name;
}
const study = (studies && studies.find(existing_study => existing_study.url === profile.study).name) || "Unknown";
let data = [];
data["General"] = {
"Given name:": profile.given_name,
......@@ -37,7 +35,7 @@ const ProfileInfo = ({ profile }) => {
"Study": study
};
return data;
}, [profile, StudiesApi.collection]);
}, [profile, studies]);
return (
<Info
......
import React from "react";
import InfoForm from "./InfoForm";
import AssociationInfo from "../Info/AssociationInfo";
import AssociationForm from "../Forms/AssociationForm";
import {useAPI} from "../../Contexts/API";
import {useAlertHandler} from "../../Contexts/AlertHandler";
import {useAPI} from "../../Contexts/API";
import AssociationForm from "../Forms/AssociationForm";
import AssociationInfo from "../Info/AssociationInfo";
import AssociationType from "../Types/Association";
import InfoForm from "./InfoForm";
const AssociationInfoWrapper = ({ info }) => <AssociationInfo association={info}/>;
......
......@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import React, {useMemo} from "react";
import {useDataFields, useLoadedMembers, useMemberTypes} from "../../Contexts/Members";
import {useStudies} from "../../Contexts/Studies";
import {useGetStudiesQuery} from "../../Store/services/studies";
import {
deriveColumnBoundsFromDataFields,
deriveMemberTableHeadersFromDataFields,
......@@ -18,7 +18,7 @@ const MemberTable = ({ initial_headers, row_filter, ...rest_props }) => {
const { collection: members, refresh: onRefresh } = useLoadedMembers();
const { collection: data_fields } = useDataFields();
const { collection: member_types } = useMemberTypes();
const { collection: studies } = useStudies();
const { data: studies } = useGetStudiesQuery();
// first flatten the member, since otherwise they don't fit nicely inside a single table
const rows = useMemo(()=> flattenMembers(members, studies).filter(row_filter), [members, row_filter, studies]);
......
import Cookies from "js-cookie";
import PropTypes from "prop-types";
import React, {createContext, useContext, useEffect, useState} from "react";
import {useDispatch} from "react-redux";
import { useHistory } from "react-router-dom";
import {RestfulProvider} from "restful-react";
import {Helper} from "../Helper";
import {useAlertHandler} from "./AlertHandler";
import {useDispatch} from "react-redux";
import {addMany} from "../Store/slices/association";
import {useAlertHandler} from "./AlertHandler";
export const APIContext = createContext({
authenticated: false,
......
import PropTypes from "prop-types";
import React, {createContext} from "react";
import useCollectionAPI from "../Components/Hooks/useCollectionAPI";
import useContextHook from "../Components/Hooks/useContextHook";
const StudiesContext = createContext({});
export const useStudies = () => useContextHook(StudiesContext, "useStudies() hook must be used within a Members block");
const Studies = ({ children }) => {
const StudiesContextValue = useCollectionAPI("/studies", { limit: 10000 });
return (
<StudiesContext.Provider value={StudiesContextValue}>
{ children }
</StudiesContext.Provider>
);
};
Studies.propTypes = {
// eslint-disable-next-line react/require-default-props
children: PropTypes.node
};
Studies.defaultProps = {
};
export default Studies;
\ No newline at end of file
import LinearProgress from "@material-ui/core/LinearProgress";
import makeStyles from "@material-ui/core/styles/makeStyles";
import Typography from "@material-ui/core/Typography";
import PropTypes from "prop-types";
import React, { useEffect, useState } from "react";
import React from "react";
import Container from "react-bootstrap/Container";
import {ValidatorForm} from "react-material-ui-form-validator";
import Block from "../../Components/PageLayout/Content/Block";
import {useAlertHandler} from "../../Contexts/AlertHandler";
import {useAPI} from "../../Contexts/API";
import Typography from "@material-ui/core/Typography";
import {useAddNewsMutation, useDeleteNewsMutation, useGetNewsByAssociationQuery} from "../../Store/services/news";
import TextField from "../../Components/Fields/TextField";
import Button from "@material-ui/core/Button";
import Wrapper from "../../Components/Fields/Wrapper";
const useStyles = makeStyles((theme) => ({
progress: {
width: "100%",
"& > * + *": {
marginTop: theme.spacing(2),
},
},
}));
const News = ({ association }) => {
const API = useAPI();
const alerthandler = useAlertHandler();
const [news, setNews] = useState([]);
const getNews = () => {
API.callv4({
url: "/news",
queryParams: {association__slug: association.slug},
method: "GET",
on_succes: (data) => setNews(data.results),
on_failure: () => alerthandler.handleAlertHandler("Red", "Loading news failed")
const classes = useStyles();
const { data: news, error, isLoading } = useGetNewsByAssociationQuery(association);
const [ addNews ] = useAddNewsMutation();
const [ deleteNews ] = useDeleteNewsMutation();
const handleSubmit = (event) => {
const data = new FormData(event.target);
addNews({
title: data.get("title"),
text: data.get("text"),
association: association.url
});
};
console.log(news, error, isLoading);
useEffect(()=>getNews(), [association]);
if (isLoading) {
return (
<LinearProgress className={classes.progress}/>
);
}
if (news.length === 0){
return (
......@@ -41,11 +59,28 @@ const News = ({ association }) => {
<Container>
{ news.map((news_item, id) => (
<Block key={id}>
<Typography variant={"h5"}>{ news_item.title }</Typography>
<Wrapper>
<Typography variant={"h5"}>{ news_item.title }</Typography>
<Button onClick={()=>deleteNews(news_item.slug)} color={"secondary"}>Delete</Button>
</Wrapper>
<hr className={"box-title-separator"}/>
<Typography variant={"h5"}>{ news_item.description }</Typography>
<Typography variant={"h5"}>{ news_item.text }</Typography>
</Block>
)) }
<Block>
<ValidatorForm
onSubmit={handleSubmit}
onError={errors => console.log(errors)}
>
<TextField
name={"title"}
/>
<TextField
name={"text"}
/>
<Button type={"submit"} variant={"contained"} color={"primary"}>Save</Button>
</ValidatorForm>
</Block>
</Container>
);
};
......
......@@ -22,8 +22,8 @@ import {ToolbarButton} from "../../Components/Tables/Plugins/ToolbarButton";
import { useAlertHandler } from "../../Contexts/AlertHandler";
import {useAPI} from "../../Contexts/API";
import {useDataFields, useMembers, useMemberTypes} from "../../Contexts/Members";
import {useStudies} from "../../Contexts/Studies";
import {deriveMemberTableHeadersFromDataFields, flattenMembers} from "../../Transformers/Members";
import {useGetStudiesQuery} from "../../Store/services/studies";
const useStyles = makeStyles(theme => ({
wrapper: {
......@@ -71,7 +71,7 @@ const Matchings = ({ association }) => {
const MembersApi = useMembers();
const MemberTypesApi = useMemberTypes();
const MatchesAPI = useCollectionAPI("/matchings", {limit: 10000, current: true, association__slug: association.slug});
const StudyApi = useStudies();
const { data: studies } = useGetStudiesQuery();
useEffect(()=>{
if (!MembersApi.loaded) {
......@@ -83,7 +83,7 @@ const Matchings = ({ association }) => {
const setMatches = MatchesAPI.set;
const member_types = MemberTypesApi.collection;
const members = MembersApi.collection;
const studies = StudyApi.collection;
const data_fields = DataFieldsApi.collection;
const [confirmationModal, setConfirmationModal] = useState(false);
const [show, setShowMatches] = useState(true);
......@@ -93,7 +93,6 @@ const Matchings = ({ association }) => {
const toggleShowMatches = () => setShowMatches(prevState => !prevState);
const data_fields = DataFieldsApi.list() || [];
const initial_headers = ["given_name", "type"].concat(data_fields ? data_fields.map(data_field=>data_field.name) : []);
const createMatch = () => {
......@@ -271,8 +270,9 @@ const Matchings = ({ association }) => {
if (members.length === 0) {
return [];
}
console.log(members, studies);
const flatttened_members = flattenMembers(members, studies);
console.log(members, flatttened_members, matches);
console.log(flatttened_members);
return matches.map(match=>({
...prependKeys("left_", flatttened_members.find(member=>member.urls.profile === match.left_profile)),
...prependKeys("right_", flatttened_members.find(member=>member.urls.profile === match.right_profile)),
......
......@@ -4,7 +4,7 @@ import React from "react";
import Block from "../../Components/PageLayout/Content/Block";
const Home = (props) => {
const Home = () => {
return (
<Container>
<Block>
......
......@@ -17,6 +17,7 @@ import Block from "../../Components/PageLayout/Content/Block";
import isTruthy from "../../Components/ValidatorRules/isTruthy";
import {useAlertHandler} from "../../Contexts/AlertHandler";
import {useAPI} from "../../Contexts/API";
import { useGetDataFieldsByAssociationQuery } from "../../Store/services/dataFields";
const breakedTextNode = (text) => <>{ text }<br/></>;
......@@ -32,11 +33,14 @@ const MembershipRequest = ({ onCancel, association }) => {
membertype: ""
});
let { data: dataFields } = useGet({
path: "/association_data_fields",
queryParams: {limit: 10000, association__slug: association.slug, board_only: false},
resolve: data => data && data.results
});
const { data: dataFields, error, isLoading } = useGetDataFieldsByAssociationQuery(association.slug);
// let { data: dataFields } = useGet({
// path: "/association_data_fields",
// queryParams: {limit: 10000, association__slug: association.slug, board_only: false},
// resolve: data => data && data.results
// });
let { data: membertypes } = useGet({
path: "/membertypes",
queryParams: {limit: 10000, association__slug: association.slug},
......@@ -145,8 +149,7 @@ const MembershipRequest = ({ onCancel, association }) => {
name={"membertype"}
onChange={(event)=>handleChange("membertype", event.target.value)}
label={"Member type"}
validators={["isTruthy"]}
errorMessages={["This field is required"]}
required
helperText={membertypeHelperText}
>
{ chooseable_membertypes.map((membertype, m) => (
......
import Container from "@material-ui/core/Container";
import React, {useState} from "react";
import Typography from "@material-ui/core/Typography";
import React from "react";
import {Cell, Pie, PieChart, ResponsiveContainer} from "recharts";
import Block from "../../Components/PageLayout/Content/Block";
import {useLoadedMembers, useMemberTypes} from "../../Contexts/Members";
import {useStudies} from "../../Contexts/Studies";
import Typography from "@material-ui/core/Typography";
import {useGetStudiesQuery} from "../../Store/services/studies";
const random_hex_color_code = () => {
......@@ -76,7 +76,7 @@ const MemoChart = React.memo(({ data, label })=> {
const StudyPieChart = () => {
const { collection: members } = useLoadedMembers();
const { collection: studies } = useStudies();
const { data: studies } = useGetStudiesQuery();
const studyUrlToStudyNameDict = Object.fromEntries(studies.map(study=>[study.url, study.name]));
const numberOfMembersPerStudy = members.reduce((perStudyObject, member) => {
......
......@@ -2,7 +2,6 @@ import React from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { useAPI } from "../../Contexts/API";
import Studies from "../../Contexts/Studies";
import Home from "../../Pages/Home/Home";
import Store from "../../Pages/Lab/Store";
import MergeProfiles from "../../Pages/Profile/MergeProfiles";
......@@ -40,7 +39,7 @@ export const AuthorizedRoutes = () => {
const needToCreateProfile = number_of_profiles === 0;
return (
<Studies>
<React.Fragment>
<Switch>
{ gotClaimedMemberships
? <Redirect to={"/protected/memberships/resolve"}/>
......@@ -107,6 +106,6 @@ export const AuthorizedRoutes = () => {
/>
</Route>
</Switch>
</Studies>
</React.Fragment>
);
};
......@@ -37,7 +37,10 @@ const BoardMemberRoutes = (props) => {
const {path, association_membership, association} = props;
return (
<Members association={association}>
<Members
association={association}
queryParam={{current: "True"}}
>
<Switch>
<Route exact path={path+"/membership/end"}>
<MembershipEnd
......
import { emptySplitApi } from "./index";
const authApi = emptySplitApi
.injectEndpoints({
endpoints: (build) => ({
loginJWT: build.mutation({
query: (credentials) => ({ url: "/auth/token/obtain", method: "POST", body: credentials }),
})
}),
overrideExisting: false
});
export const { useLoginMutation } = authApi;
export default authApi;
\ No newline at end of file
import {emptySplitApi, providesListTags} from "./index";
const dataFieldsApi = emptySplitApi
.enhanceEndpoints({addTagTypes: ["DataField"]})
.injectEndpoints({
endpoints: (build) => ({
getDataFieldsByAssociation: build.query({
query: (association_slug) => ({ url: "/association_data_fields", params: {"association__slug": association_slug, limit: 100} }),
providesTags: (result, error, association_slug) => [{ type: "DataField", id: association_slug }],
transformResponse: (response) => response.results
}),
addDataField: build.mutation({
query: (body) => ({ url: "/association_data_fields", method: "POST", body: body }),
invalidatesTags: (result) => [{ type: "DataField", id: result.association }],
transformResponse: (response) => response.data,
}),
patchDataField: build.mutation({
query: ({ slug, ...patch}) => ({ url: `/association_data_fields/${slug}`, method: "PATCH", body: patch }),
invalidatesTags: (result, error, arg) => [{ type: "DataField", id: arg.association }],
transformResponse: (response) => response.data,
}),
deleteDataField: build.mutation({
query: (slug) => ({ url: `/association_data_fields/${slug}`, method: "DELETE" }),
invalidatesTags: (result, error, arg) => [{ type: "DataField", id: arg.association }],
})
}),
overrideExisting: false
});
export const {
useGetDataFieldsByAssociationQuery,
useAddDataFieldMutation,
usePatchDataFieldMutation,
useDeleteDataFieldMutation
} = dataFieldsApi;
export default dataFieldsApi;
\ No newline at end of file
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import Cookies from "js-cookie";
const baseQuery = fetchBaseQuery({
baseUrl: process.env.REACT_APP_API_URL,
credentials: "include",
prepareHeaders: (headers, { getState }) => {
const backend = getState().auth.authentication_backend;
const token = getState().auth.authentication_token;
if (backend === "OIDC") {
headers.set("X-CSRFToken", Cookies.get("csrftoken"));
}
if (backend === "jwt") {
headers.set("Authorization", token);
}
return headers;
},
});
const providesListTags = (resultsWithIds, tagType, listTag) => {
return resultsWithIds
? [
{ type: tagType, id: listTag },
...resultsWithIds.map(({ slug }) => ({ type: tagType, id: slug })),
]
: [{ type: tagType, id: listTag }];
};
// initialize an empty api service that we'll inject endpoints into later as needed
const emptySplitApi = createApi({
reducerPath: "api",
baseQuery: baseQuery,
endpoints: () => ({}),
});
export { emptySplitApi, providesListTags};
\ No newline at end of file
import {emptySplitApi, providesListTags} from "./index";
const newsApi = emptySplitApi
.enhanceEndpoints({addTagTypes: ["News"]})
.injectEndpoints({
endpoints: (build) => ({
getNewsByAssociation: build.query({
query: (association) => ({ url: "/news", params: {"association__slug": association.slug} }),
providesTags: (result, error, association) =>
providesListTags(result, "News", association.url),
transformResponse: (response) => response.results
}),
addNews: build.mutation({
query: (body) => ({ url: "/news/", method: "POST", body: body }),
invalidatesTags: (result) => [{ type: "News", id: result.association }],
transformResponse: (response) => response.data,
}),
patchNews: build.mutation({
query: ({ slug, ...patch}) => ({ url: `/news/${slug}`, method: "PATCH", body: patch }),
invalidatesTags: (result, error, arg) => [{ type: "News", id: arg.association }],
transformResponse: (response) => response.data,
}),
deleteNews: build.mutation({
query: (slug) => ({ url: `/news/${slug}`, method: "DELETE" }),
invalidatesTags: (result, error, arg) => [{ type: "News", id: arg.association }],
})
}),
overrideExisting: false
});
export const { useGetNewsByAssociationQuery, useAddNewsMutation, usePatchNewsMutation, useDeleteNewsMutation } = newsApi;
export default newsApi;
\ No newline at end of file
import {emptySplitApi} from "./index";
const studiesApi = emptySplitApi