Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/graphql/types/exam_version_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def instructions
guard ALL_STAFF_OR_PUBLISHED
end

field :all_rubrics, [Types::RubricType], null: true do
field :all_rubrics, [Types::RubricType], null: false do
guard Guards::ALL_STAFF
end
def all_rubrics
Expand All @@ -132,7 +132,7 @@ def all_rubrics
.load(object)
end

field :rubrics, [Types::RubricType], null: true do
field :rubrics, [Types::RubricType], null: false do
guard Guards::ALL_STAFF
end
def rubrics
Expand All @@ -141,11 +141,11 @@ def rubrics
.load(object)
end

field :root_rubric, Types::RubricType, null: true do
field :root_rubric, Types::RubricType, null: false do
guard Guards::ALL_STAFF
end

field :raw_rubrics, GraphQL::Types::JSON, null: true do
field :raw_rubrics, GraphQL::Types::JSON, null: false do
guard Guards::PROFESSORS
end
def raw_rubrics
Expand Down
1 change: 0 additions & 1 deletion app/graphql/types/grading_comment_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,5 @@ def bnum
field :message, String, null: false
field :points, Float, null: false
field :creator, Types::UserType, null: false
field :preset_comment, Types::PresetCommentType, null: true
end
end
14 changes: 9 additions & 5 deletions app/javascript/components/common/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,19 @@ const validateNumericInput : KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === '.') {
const indexOfDot = e.currentTarget.value.indexOf('.');
if (indexOfDot < 0) return;
if (e.currentTarget.selectionStart <= indexOfDot
if (e.currentTarget.selectionStart !== null
&& e.currentTarget.selectionStart <= indexOfDot
&& e.currentTarget.selectionEnd !== null
&& indexOfDot < e.currentTarget.selectionEnd) {
return;
}
}
if (e.key === '-') {
const indexOfMinus = e.currentTarget.value.indexOf('-');
if (indexOfMinus < 0 && e.currentTarget.selectionStart === 0) return;
if (e.currentTarget.selectionStart <= indexOfMinus
if (e.currentTarget.selectionStart !== null
&& e.currentTarget.selectionStart <= indexOfMinus
&& e.currentTarget.selectionEnd !== null
&& indexOfMinus < e.currentTarget.selectionEnd) {
return;
}
Expand Down Expand Up @@ -147,7 +151,7 @@ export const NumericInput: React.FC<{
const selEnd = e.currentTarget.selectionEnd;
const nextVal = `${curVal.slice(0, selStart)}${data}${curVal.slice(selEnd)}`;
const nextAsNum = Number(nextVal);
if (Number.isFinite(nextAsNum)) {
if (Number.isFinite(nextAsNum) && onChange) {
onChange(clamp(nextAsNum, min, max), true);
}
}}
Expand All @@ -167,7 +171,7 @@ export const NumericInput: React.FC<{
variant={variant}
disabled={disabled || (max !== undefined && numValue >= max)}
tabIndex={-1}
onClick={() => onChange(clamp(numValue + step, min, max), false)}
onClick={() => onChange && onChange(clamp(numValue + step, min, max), false)}
>
<Icon className="m-0 p-0" size={size === 'lg' ? '0.75em' : '0.5em'} I={FaChevronUp} />
</Button>
Expand All @@ -177,7 +181,7 @@ export const NumericInput: React.FC<{
variant={variant}
disabled={disabled || (min !== undefined && numValue <= min)}
tabIndex={-1}
onClick={() => onChange(clamp(numValue - step, min, max), false)}
onClick={() => onChange && onChange(clamp(numValue - step, min, max), false)}
>
<Icon size={size === 'lg' ? '0.75em' : '0.5em'} I={FaChevronDown} />
</Button>
Expand Down
9 changes: 4 additions & 5 deletions app/javascript/components/common/ReadableDate.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { DateTime, Duration } from 'luxon';
import { DateTime } from 'luxon';

function makeReadableDate(dd: DateTime, showTime: boolean, capitalize: boolean): string {
const today = DateTime.local().startOf('day');
const yesterday = today.minus({ days: 1 });
const tomorrow = today.plus({ days: 1 });
const twodays = tomorrow.plus({ days: 1 });
let relDay: string;
let relDay: string | undefined;
if (yesterday <= dd && dd < today) {
relDay = (capitalize ? 'Yesterday' : 'yesterday');
} else if (today <= dd && dd < tomorrow) {
Expand All @@ -28,7 +28,6 @@ interface ReadableDateProps {
value: DateTime;
relative?: boolean;
showTime?: boolean;
threshold?: Duration;
capitalize?: boolean;
}

Expand All @@ -40,11 +39,11 @@ const ReadableDate: React.FC<ReadableDateProps> = (props) => {
className,
capitalize,
} = props;
let str: string;
let str: string | null;
if (relative) {
str = value.toRelative();
} else {
str = makeReadableDate(value, showTime, capitalize);
str = makeReadableDate(value, showTime, !!capitalize);
}
return (
<span className={className}>{str}</span>
Expand Down
8 changes: 5 additions & 3 deletions app/javascript/components/common/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ShowAlert: React.FC<{
copyButton = false,
} = alert;
const [show, setShow] = useState(true);
const bodyRef = useRef<HTMLDivElement>();
const bodyRef = useRef<HTMLDivElement>(null);
return (
<Toast
className={`border-${variant}`}
Expand All @@ -55,8 +55,10 @@ const ShowAlert: React.FC<{
className="p-0"
variant={variant}
onClick={async () => {
const text = bodyRef.current.innerText;
await navigator.clipboard.writeText(text);
if (bodyRef.current) {
const text = bodyRef.current.innerText;
await navigator.clipboard.writeText(text);
}
}}
>
<Icon I={FaCopy} />
Expand Down
9 changes: 6 additions & 3 deletions app/javascript/components/common/archive/fileMarks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
interface MarksInfo {
byLine: unknown[];
byLine: {
open: number[],
close: number[],
}[][];
byNum: {
[num: number]: {
startLine: number;
Expand Down Expand Up @@ -37,7 +40,7 @@ export function extractMarks(text: string): FileMarksInfo {
let idx: number;
// eslint-disable-next-line no-cond-assign
while ((idx = lines[lineNo].search(/~ro:\d+:[se]~/)) >= 0) {
const m = lines[lineNo].match(/~ro:(\d+):([se])~/);
const m = lines[lineNo].match(/~ro:(\d+):([se])~/) as RegExpMatchArray;
lines[lineNo] = lines[lineNo].replace(m[0], '');
if (m[2] === 's') {
count += 1;
Expand Down Expand Up @@ -81,7 +84,7 @@ export function extractMarks(text: string): FileMarksInfo {
},
options: {
inclusiveLeft: m.lockBefore,
inclusiveRight: m.lockAfter,
inclusiveRight: !!m.lockAfter,
},
})),
};
Expand Down
14 changes: 8 additions & 6 deletions app/javascript/components/common/archive/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import mime from '@hourglass/common/mime';
import { extractMarks } from '@hourglass/common/archive/fileMarks';

export async function handleDir(root: JSZip): Promise<ExamFile[]> {
const ret = [];
const promises = [];
const ret: ExamFile[] = [];
const promises: Promise<void>[] = [];
root.forEach((path, file) => {
if (file.dir) return;
const exploded = path.split('/');
const fileName = exploded.pop();
let current = ret;
const pathSoFar = [];
// since the split condition is non-empty, the exploded array will be non-empty
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fileName = exploded.pop()!;
let current: ExamFile[] = ret;
const pathSoFar: string[] = [];
exploded.forEach((segment) => {
pathSoFar.push(segment);
const dir = current.find((f) => f.text === `${segment}/`);
const dir = current.find((f): f is ExamDir => f.text === `${segment}/`);
if (dir) {
current = dir.nodes;
} else {
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/components/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ export type SelectOptions<T> = SelectOption<T>[];
export type MutationReturn<T extends MutationParameters> = [
MutateWithVariables<T>, MutationState<T>
];

export function compact<T>(ts : readonly (T | null | undefined)[]): T[] {
return ts.filter((t : T | null | undefined): t is T => t !== null && t !== undefined);
}
4 changes: 2 additions & 2 deletions app/javascript/components/common/student-dnd/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface Section {

interface Student {
id: string;
nuid: number;
nuid: number | null;
username: string;
displayName: string;
}
Expand Down Expand Up @@ -329,7 +329,7 @@ const StudentDNDForm: React.FC<
<Button
disabled={loading}
variant="danger"
className={pristine && 'd-none'}
className={pristine ? 'd-none' : undefined}
onClick={reset}
>
Reset
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/components/common/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export function useApiResponse<Server, Res = Server>(
transformSuccess?: (server: Server) => Res,
deps?: React.DependencyList,
): ApiResponse<Res> {
const [response, setResponse] = useState<Server>(undefined);
const [error, setError] = useState<ApiError>(undefined);
const [response, setResponse] = useState<Server | undefined>(undefined);
const [error, setError] = useState<ApiError | undefined>(undefined);
useEffect(() => {
fetch(url, {
headers: {
Expand Down
42 changes: 24 additions & 18 deletions app/javascript/components/workflows/grading/UseRubrics.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { ReactElement, useContext } from 'react';
import {
Rubric,
RubricOne,
Expand Down Expand Up @@ -39,13 +39,19 @@ import { createCommentMutation } from './__generated__/createCommentMutation.gra
import { grading_one$data } from './__generated__/grading_one.graphql';
import { UseRubricsKey$key } from './__generated__/UseRubricsKey.graphql';

type GradingComment = grading_one$data['gradingComments']['edges'][number]['node'];
type PresetCommentId = GradingComment['presetComment']['id']
type NN<T> = Exclude<T, null>
type O<T> = T | undefined

type GradingComment = NN<NN<NN<NN<grading_one$data['gradingComments']>['edges']>[number]>['node']>;
type PresetCommentId = NN<GradingComment['presetComment']>['id']

interface ShowRubricProps<R> {
rubric: R;
// eslint-disable-next-line react/no-unused-prop-types
showCompletenessAgainst?: PresetCommentId[];
// eslint-disable-next-line react/no-unused-prop-types
parentRubricType?: Rubric['type'];
// eslint-disable-next-line react/no-unused-prop-types
collapseKey?: string;
qnum: number;
pnum: number;
Expand Down Expand Up @@ -127,7 +133,7 @@ const ShowPreset: React.FC<{
};

type CompletionStatus = 'complete' | 'incomplete' | 'invalid'
function combineCompletionAll(c1 : CompletionStatus, c2: CompletionStatus): CompletionStatus {
function combineCompletionAll(c1 : CompletionStatus, c2: O<CompletionStatus>): O<CompletionStatus> {
// complete < incomplete < invalid
switch (c1) {
case 'complete': return c2;
Expand All @@ -137,7 +143,7 @@ function combineCompletionAll(c1 : CompletionStatus, c2: CompletionStatus): Comp
throw new ExhaustiveSwitchError(c1);
}
}
function combineCompletionAny(c1 : CompletionStatus, c2: CompletionStatus): CompletionStatus {
function combineCompletionAny(c1: CompletionStatus, c2: O<CompletionStatus>): O<CompletionStatus> {
// incomplete < complete < invalid
switch (c1) {
case 'incomplete': return c2;
Expand All @@ -147,7 +153,7 @@ function combineCompletionAny(c1 : CompletionStatus, c2: CompletionStatus): Comp
throw new ExhaustiveSwitchError(c1);
}
}
function completionStatus(rubric: Rubric, parentRubricType: Rubric['type'], presetIDs: PresetCommentId[]): CompletionStatus {
function completionStatus(rubric: Rubric, parentRubricType: O<Rubric['type']>, presetIDs: O<PresetCommentId[]>): O<CompletionStatus> {
if (rubric === undefined || rubric === null) return undefined;
if (presetIDs === undefined || presetIDs === null) return undefined;
switch (rubric.type) {
Expand Down Expand Up @@ -198,7 +204,7 @@ function completionStatus(rubric: Rubric, parentRubricType: Rubric['type'], pres
throw new ExhaustiveSwitchError(rubric);
}
}
const statusToClass = (status: CompletionStatus): string => {
const statusToClass = (status: O<CompletionStatus>): string => {
if (status === 'complete') {
return 'status-valid';
}
Expand All @@ -210,8 +216,8 @@ const statusToClass = (status: CompletionStatus): string => {

export const ShowPresetSummary: React.FC<{
direction: RubricPresets['direction'];
label: string;
mercy: number;
label?: string;
mercy?: number;
pointsMsg?: string;
}> = (props) => {
const {
Expand Down Expand Up @@ -357,7 +363,7 @@ const ShowAll: React.FC<ShowRubricProps<RubricAll>> = (props) => {
}
const showAnyway = (collapseKey === undefined ? 'show' : '');
const padDescription = (description ? 'mb-2' : '');
let chevron = null;
let chevron: O<ReactElement>;
if (showChevron) {
if (isOpen) {
chevron = <Icon className="mr-2" I={FaChevronUp} />;
Expand All @@ -379,11 +385,11 @@ const ShowAll: React.FC<ShowRubricProps<RubricAll>> = (props) => {
);
return (
<>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey}>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey as string}>
{heading}
<HTML value={description} className={padDescription} />
</Accordion.Toggle>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey}>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey as string}>
<Card.Body>
{body}
</Card.Body>
Expand Down Expand Up @@ -450,7 +456,7 @@ const ShowOne: React.FC<ShowRubricProps<RubricOne>> = (props) => {
}
const showAnyway = (showChevron ? '' : 'show');
const padDescription = (description ? 'mb-2' : '');
let chevron = null;
let chevron: O<ReactElement>;
if (showChevron) {
if (isOpen) {
chevron = <Icon className="mr-2" I={FaChevronUp} />;
Expand All @@ -472,11 +478,11 @@ const ShowOne: React.FC<ShowRubricProps<RubricOne>> = (props) => {
);
return (
<>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey}>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey as string}>
{heading}
<HTML value={description} className={padDescription} />
</Accordion.Toggle>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey}>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey as string}>
<Card.Body>
{body}
</Card.Body>
Expand Down Expand Up @@ -543,7 +549,7 @@ const ShowAny: React.FC<ShowRubricProps<RubricAny>> = (props) => {
}
const showAnyway = (showChevron ? '' : 'show');
const padDescription = (description ? 'mb-2' : '');
let chevron = null;
let chevron: O<ReactElement>;
if (showChevron) {
if (isOpen) {
chevron = <Icon className="mr-2" I={FaChevronUp} />;
Expand All @@ -565,11 +571,11 @@ const ShowAny: React.FC<ShowRubricProps<RubricAny>> = (props) => {
);
return (
<>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey}>
<Accordion.Toggle as={Card.Header} eventKey={collapseKey as string}>
{heading}
<HTML value={description} className={padDescription} />
</Accordion.Toggle>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey}>
<Accordion.Collapse className={showAnyway} eventKey={collapseKey as string}>
<Card.Body>
{body}
</Card.Body>
Expand Down
Loading