-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cc758aa
commit 93ef2c1
Showing
5 changed files
with
438 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.