Skip to content

Commit

Permalink
feat(LibreOne): instructor verification requests management (#123)
Browse files Browse the repository at this point in the history
* feat(LibreOne): [wip] verification requests console
* fix(Commons): instructor verification link
* fix(LibreOne): remove dev artifact
* feat(LibreOne): instructor verification request console
* fix(LibreOne): verification request update payload

---------

Co-authored-by: Ethan Turner <[email protected]>
  • Loading branch information
jakeaturner and ethanaturner authored Sep 19, 2023
1 parent 2807f79 commit 8e24ce4
Show file tree
Hide file tree
Showing 17 changed files with 1,161 additions and 75 deletions.
2 changes: 2 additions & 0 deletions client/src/Conductor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import UserDetails from './components/controlpanel/UserDetails';
import UsersManager from './components/controlpanel/UsersManager';
import LoadingSpinner from './components/LoadingSpinner';
const CentralIdentity = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity'));
const CentralIdentityInstructorVerifications = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityInstructorVerifications'));
const CentralIdentityOrgs = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityOrgs'));
const CentralIdentityServices = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityServices'));
const CentralIdentityUsers = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers'));
Expand Down Expand Up @@ -93,6 +94,7 @@ const Conductor = () => {
<PrivateRoute exact path='/controlpanel/harvestingrequests' component={HarvestingRequests} />
<PrivateRoute exact path='/controlpanel/homeworkmanager' component={HomeworkManager} />
<PrivateRoute exact path='/controlpanel/libreone' component={CentralIdentity} />
<PrivateRoute exact path='/controlpanel/libreone/instructor-verifications' component={CentralIdentityInstructorVerifications} />
<PrivateRoute exact path='/controlpanel/libreone/orgs' component={CentralIdentityOrgs} />
<PrivateRoute exact path='/controlpanel/libreone/services' component={CentralIdentityServices} />
<PrivateRoute exact path='/controlpanel/libreone/users' component={CentralIdentityUsers} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import axios from 'axios';
import { Menu, Image, Dropdown, Icon, Button } from 'semantic-ui-react';
import AuthHelper from '../../util/AuthHelper';
import Breakpoint from '../../util/Breakpoints';
import './CommonsNavbar.css';
import { Organization, User } from '../../../types';
import { getCentralAuthInstructorURL } from '../../../utils/centralIdentityHelpers';

const CommonsNavbar = ({
type CommonsNavbarProps = {
org: Organization,
user: User,
commonsTitle: string,
showMobileMenu?: boolean,
showMobileCommonsList?: boolean,
onMobileMenuToggle: () => void,
onMobileCommonsListToggle: () => void,
}

const CommonsNavbar: React.FC<CommonsNavbarProps> = ({
org,
user,
commonsTitle,
showMobileMenu,
showMobileCommonsList,
showMobileMenu = false,
showMobileCommonsList = false,
onMobileMenuToggle,
onMobileCommonsListToggle,
}) => {

// Data
const [campusCommons, setCampusCommons] = useState([]);
const [campusCommons, setCampusCommons] = useState<{key: string, name: string, link: string}[]>([]);

/**
* Retrieves a list of LibreGrid/Campus Commons instances from the server and saves it to state.
Expand Down Expand Up @@ -87,10 +98,9 @@ const CommonsNavbar = ({
const AccountRequestLink = ({ isMobile = false }) => {
return (
<Menu.Item
as={Link}
to="/verification/instructor"
as="a"
href={getCentralAuthInstructorURL()}
target="_blank"
rel="noreferrer"
className="commons-nav-link"
>
Instructor Verification Request
Expand Down Expand Up @@ -289,48 +299,4 @@ const CommonsNavbar = ({
)
};

CommonsNavbar.propTypes = {
/**
* Information about the instance's configured Organization.
*/
org: PropTypes.shape({
orgID: PropTypes.string.isRequired,
shortName: PropTypes.string.isRequired,
mediumLogo: PropTypes.string.isRequired,
aboutLink: PropTypes.string.isRequired,
}).isRequired,
/**
* Information about the currently authenticated Conductor user, if applicable.
*/
user: PropTypes.shape({
isAuthenticated: PropTypes.bool,
avatar: PropTypes.string,
}),
/**
* Display meta-title of the current Commons, such as 'Campus Commons'.
*/
commonsTitle: PropTypes.string.isRequired,
/**
* Whether to show or hide the menu (on mobile displays).
*/
showMobileMenu: PropTypes.bool,
/**
* Whether to show or hide the list of public commons in the (mobile display) menu.
*/
showMobileCommonsList: PropTypes.bool,
/**
* Handler for (mobile display) menu toggle events.
*/
onMobileMenuToggle: PropTypes.func.isRequired,
/**
* Handler for (mobile display) public commons list toggle events.
*/
onMobileCommonsListToggle: PropTypes.func.isRequired,
};

CommonsNavbar.defaultProps = {
showMobileMenu: false,
showMobileCommonsList: false,
};

export default CommonsNavbar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { useState, useEffect } from "react";
import {
Accordion,
Button,
Checkbox,
Form,
Icon,
Input,
Message,
Modal,
ModalProps,
} from "semantic-ui-react";
import LoadingSpinner from "../../LoadingSpinner";
import useGlobalError from "../../error/ErrorHooks";
import axios from "axios";
import {
CentralIdentityApp,
CentralIdentityVerificationRequest,
} from "../../../types";

interface ApproveVerificationRequestModalProps extends ModalProps {
show: boolean;
requestId: string;
onSave: () => void;
onCancel: () => void;
}

const ApproveVerificationRequestModal: React.FC<
ApproveVerificationRequestModalProps
> = ({ show, requestId, onSave, onCancel, ...rest }) => {
// Global state & hooks
const { handleGlobalError } = useGlobalError();

// Data & UI
const [loading, setLoading] = useState(false);
const [request, setRequest] = useState<CentralIdentityVerificationRequest>();
const [allApps, setAllApps] = useState<CentralIdentityApp[]>([]);
const [approvedApps, setApprovedApps] = useState<CentralIdentityApp[]>([]);

// Effects
useEffect(() => {
if (show) {
init();
}
}, [show, requestId]);

useEffect(() => {
if (allApps.length > 0) {
initApprovedApps();
}
}, [request, allApps]);

// Methods
async function init() {
// We don't use Promise.all here because we want to load the request first
await loadRequest();
await loadApps();
initApprovedApps();
}

async function loadRequest() {
try {
if (!requestId) return;

setLoading(true);

const res = await axios.get(
`/central-identity/verification-requests/${requestId}`
);

if (res.data.err) {
handleGlobalError(res.data.err);
return;
}

setRequest(res.data.request);
setApprovedApps(res.data.request.access_request?.applications || []);
} catch (err) {
handleGlobalError(err);
} finally {
setLoading(false);
}
}

async function loadApps() {
try {
setLoading(true);
const res = await axios.get("/central-identity/apps");

if (res.data.err) {
handleGlobalError(res.data.err);
return;
}

setAllApps(res.data.applications);
} catch (err) {
handleGlobalError(err);
} finally {
setLoading(false);
}
}

function initApprovedApps() {
setLoading(true);
if (
!request ||
!request.access_request ||
!request.access_request.applications ||
!Array.isArray(request.access_request.applications)
) {
setLoading(false);
return;
}

const requestApps = request?.access_request.applications;
setApprovedApps([
...allApps.filter((app) => {
return app.is_default_library === true;
}),
...requestApps,
]);

setLoading(false);
}

async function submitUpdateRequest() {
try {
if (!requestId) return;

setLoading(true);

const res = await axios.patch(
`/central-identity/verification-requests/${requestId}`,
{
request: {
effect: "approve",
approved_applications: approvedApps.map((app) => app.id),
},
}
);
if (res.data.err) {
handleGlobalError(res.data.err);
return;
}

onSave();
} catch (err) {
handleGlobalError(err);
} finally {
setLoading(false);
}
}

const isChecked = (appId: number) => {
if (!approvedApps || approvedApps.length === 0) return false;
return approvedApps.map((app) => app.id).includes(appId);
};

function handleCheckApp(appId: number) {
if (isChecked(appId)) {
setApprovedApps(
approvedApps.filter((app) => {
return app.id !== appId;
})
);
} else {
const foundApp = allApps.find((app) => app.id === appId);
if (!foundApp) return;
setApprovedApps([...approvedApps, foundApp]);
}
}

return (
<Modal open={show} onClose={onCancel} size="large" {...rest}>
<Modal.Header>
Select Apps To Approve for Verification Request
</Modal.Header>
<Modal.Content scrolling id="task-view-content">
{loading && (
<div className="my-4r">
<LoadingSpinner />
</div>
)}
{!loading && (
<div className="pa-2r">
<Message info icon size="small">
<Icon name="info circle" />
<Message.Content>
Note: Accounts for applications not yet integrated with LibreOne
will not be automatically appropriated.
</Message.Content>
</Message>
<p>
The applications {request?.user.first_name}{" "}
{request?.user.last_name} requested have been pre-selected.
Select/unselect applications as necessary if you want to provide
access differenty than the user requested. The user will be
notified by email of what applications they were approved for.
</p>
<Form className="flex-col-div" noValidate>
{allApps
.filter(
(app) =>
app.is_default_library === false &&
app.default_access !== "all"
)
.map((app) => (
<Checkbox
key={app.id}
label={app.name}
checked={isChecked(app.id)}
onChange={() => handleCheckApp(app.id)}
className="mb-1r"
/>
))}
<Accordion
className="mt-2p"
panels={[
{
key: "danger",
title: {
content: (
<span>
<strong>Change Default Libraries</strong>
</span>
),
},
content: {
content: (
<div className="flex-col-div">
{allApps
.filter((app) => app.is_default_library)
.map((app) => (
<Checkbox
key={app.id}
label={app.name}
checked={isChecked(app.id)}
onChange={() => handleCheckApp(app.id)}
className="mb-1r"
/>
))}
</div>
),
},
},
]}
/>
</Form>
</div>
)}
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onCancel()}>Cancel</Button>
<Button color="green" onClick={() => submitUpdateRequest()}>
<Icon name="save" /> Confirm
</Button>
</Modal.Actions>
</Modal>
);
};

export default ApproveVerificationRequestModal;
Loading

0 comments on commit 8e24ce4

Please sign in to comment.