Skip to content

Commit

Permalink
fix: [wip] framework exclusions
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Jun 7, 2024
1 parent cc758aa commit 93ef2c1
Show file tree
Hide file tree
Showing 5 changed files with 438 additions and 81 deletions.
2 changes: 2 additions & 0 deletions app/(authorized)/course-settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PageHeader from "@/components/PageHeader";
import GenericPageContainer from "@/components/GenericPageContainer";
import StudentPermissions from "@/components/CourseSettings/StudentPermissions";
import { updateCourseAnalyticsSettings } from "@/lib/analytics-functions";
import FrameworkExclusions from "@/components/CourseSettings/FrameworkExclusions";

export default function CourseSettings() {

Expand All @@ -13,6 +14,7 @@ export default function CourseSettings() {
subtitle="Configure your course analytics and sharing settings."
/>
<StudentPermissions saveData={updateCourseAnalyticsSettings} />
<FrameworkExclusions saveData={updateCourseAnalyticsSettings} className="tw-mt-4"/>
</GenericPageContainer>
);
}
112 changes: 112 additions & 0 deletions components/CourseSettings/FrameworkExclusions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";
import { GlobalState } from "@/lib/types";
import React, { useState } from "react";
import { Button, Form, ListGroup, Toast } from "react-bootstrap";
import ToastContainer from "../ToastContainer";
import { Controller, useForm } from "react-hook-form";
import { useGlobalContext } from "@/state/globalContext";
import TransferList from "../TransferList";
import classNames from "classnames";

interface FrameworkExclusionsProps {
className?: string;
saveData: (
course_id: string,
data: Partial<GlobalState>
) => Promise<void> | void;
}

const FrameworkExclusions: React.FC<FrameworkExclusionsProps> = ({
className,
saveData,
}) => {
const [globalState, setGlobalState] = useGlobalContext();
const [saveError, setSaveError] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const {
control,
setValue,
getValues,
formState: { isDirty },
reset,
} = useForm<Pick<GlobalState, "shareGradeDistribution">>({
defaultValues: {
shareGradeDistribution: globalState.shareGradeDistribution,
},
});

async function handleSave() {
try {
setLoading(true);

await saveData(globalState.courseID, {
shareGradeDistribution: getValues().shareGradeDistribution,
});

const newGlobalState = {
...globalState,
shareGradeDistribution: getValues().shareGradeDistribution,
};

setGlobalState(newGlobalState);
reset(newGlobalState);
setShowSuccess(true);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}

return (
<div className={classNames(className)}>
<ListGroup className="!tw-shadow-sm tw-w-full tw-h-fit">
<ListGroup.Item className="tw-bg-ultra-light-gray">
Framework Exclusions
</ListGroup.Item>
<ListGroup.Item className="tw-bg-white">
<p className="tw-mb-6 tw-text-sm tw-text-slate-500">
Exclude specific framework descriptors from the analytics visualizations.
</p>
<TransferList
availableItems={["Test1", "Test2"]}
selectedItems={[]}
setAvailableItems={(items) => console.log(items)}
setSelectedItems={(items) => console.log(items)}
allowManualEntry={false}
/>
<Form>
<div className="tw-flex tw-justify-end tw-items-center">
{loading && (
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
)}
<Button disabled={!isDirty} color="blue" onClick={handleSave}>
Save
</Button>
</div>
</Form>
</ListGroup.Item>
</ListGroup>
<ToastContainer>
<Toast
onClose={() => setShowSuccess(false)}
show={showSuccess}
className="tw-mt-2"
bg="primary"
delay={3000}
autohide
>
<Toast.Header>
<strong className="me-auto">Success!</strong>
</Toast.Header>
<Toast.Body>Settings saved successfully.</Toast.Body>
</Toast>
</ToastContainer>
</div>
);
};

export default FrameworkExclusions;
194 changes: 194 additions & 0 deletions components/TransferList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import {
ChevronDoubleRight,
ChevronRight,
ChevronDoubleLeft,
ChevronLeft,
Plus,
} from "react-bootstrap-icons";

function not(a: string[], b: string[]) {
return a.filter((value) => b.indexOf(value) === -1);
}

function intersection(a: string[], b: string[]) {
return a.filter((value) => b.indexOf(value) !== -1);
}

function sort(a: string[]) {
return a.sort((a, b) => a.localeCompare(b));
}

function noDuplicates(a: string[]) {
return Array.from(new Set(a));
}

interface TransferListProps {
availableItems: string[];
setAvailableItems: (items: string[]) => void;
selectedItems: string[];
setSelectedItems: (items: string[]) => void;
allowManualEntry?: boolean;
}

const TransferList: React.FC<TransferListProps> = ({
availableItems,
setAvailableItems,
selectedItems,
setSelectedItems,
allowManualEntry,
}) => {
const [checked, setChecked] = useState<string[]>([]);
const availableChecked = intersection(checked, availableItems);
const selectedChecked = intersection(checked, selectedItems);
const [manualEntry, setManualEntry] = useState("");

const handleAllSelected = () => {
setSelectedItems(noDuplicates(sort(selectedItems.concat(availableItems))));
setAvailableItems([]);
};

const handleCheckedRight = () => {
setSelectedItems(
noDuplicates(sort(selectedItems.concat(availableChecked)))
);
setAvailableItems(
noDuplicates(sort(not(availableItems, availableChecked)))
);
setChecked(noDuplicates(not(checked, availableChecked)));
};

const handleCheckedLeft = () => {
setAvailableItems(
noDuplicates(sort(availableItems.concat(selectedChecked)))
);
setSelectedItems(noDuplicates(sort(not(selectedItems, selectedChecked))));
setChecked(noDuplicates(sort(not(checked, selectedChecked))));
};

const handleAllAvailable = () => {
setAvailableItems(noDuplicates(sort(availableItems.concat(selectedItems))));
setSelectedItems([]);
};

const handleCheckItem = (value: string) => {
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];

if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}

setChecked(sort(newChecked));
};

const handleManualEntry = () => {
if (manualEntry) {
setSelectedItems(noDuplicates(sort(selectedItems.concat(manualEntry))));
setManualEntry("");
}
};

return (
<div className="tw-flex tw-flex-col tw-w-full tw-h-96 tw-mb-4">
<div className="tw-flex tw-flex-row">
<div className="tw-flex tw-flex-col tw-basis-2/5">
<p className="tw-font-bold tw-mb-0">Available Items</p>
<div className="tw-flex tw-flex-col tw-border tw-border-solid tw-rounded-md tw-min-h-48 tw-h-72 tw-overflow-auto tw-mt-0.5">
{availableItems.map((item) => (
<div
key={item}
className="tw-flex tw-flex-row tw-items-center tw-p-2"
>
<Form.Check
checked={checked.indexOf(item) !== -1}
onChange={() => handleCheckItem(item)}
/>
<span className="tw-ml-2">{item}</span>
</div>
))}
</div>
</div>
<div className="tw-flex tw-flex-col tw-h-full tw-justify-center tw-basis-1/5 tw-items-center">
<Button className="!tw-mb-4" onClick={handleAllSelected}>
<ChevronDoubleRight />
</Button>
<Button
className="!tw-mb-4"
onClick={handleCheckedRight}
disabled={availableChecked.length === 0}
>
<ChevronRight />
</Button>
<Button
className="!tw-mb-4"
onClick={handleCheckedLeft}
disabled={selectedChecked.length === 0}
>
<ChevronLeft />
</Button>
<Button onClick={handleAllAvailable}>
<ChevronDoubleLeft />
</Button>
</div>
<div className="tw-flex tw-flex-col tw-basis-2/5">
<p className="tw-font-bold tw-mb-0">Selected Items</p>
<div className="tw-flex tw-flex-col tw-border tw-border-solid tw-rounded-md tw-h-72 tw-overflow-auto tw-mt-0.5">
{selectedItems.map((item) => (
<div
key={item}
className="tw-flex tw-flex-row tw-items-center tw-p-2"
>
<Form.Check
checked={checked.indexOf(item) !== -1}
onChange={() => handleCheckItem(item)}
/>
<span className="tw-ml-2">{item}</span>
</div>
))}
</div>
</div>
</div>
{
// Allow manual entry of items
allowManualEntry && (
<div className="tw-flex tw-flex-row tw-mt-2 tw-justify-end">
<div className="tw-flex tw-flex-col tw-basis-2/5">
<Form
className="tw-flex tw-flex-row"
onSubmit={(e) => {
e.preventDefault();
handleManualEntry();
}}
>
<Form.Control
type="text"
className="tw-border tw-rounded-md tw-w-full"
placeholder="Enter a new item"
value={manualEntry}
onChange={(e) => setManualEntry(e.target.value)}
/>
<Button
color="blue"
className="!tw-ml-2"
onClick={handleManualEntry}
>
<Plus />
</Button>
</Form>
<p className="tw-text-sm tw-text-muted tw-text-gray-400 tw-italic tw-ml-1">
Manually added items will need to be entered again if removed
and list is saved.
</p>
</div>
</div>
)
}
</div>
);
};

export default TransferList;
Loading

0 comments on commit 93ef2c1

Please sign in to comment.