Skip to content

Commit ba65cbc

Browse files
Global error pins (#590)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent d3e8970 commit ba65cbc

File tree

5 files changed

+894
-41
lines changed

5 files changed

+894
-41
lines changed

components/discussion/ErrorPinManageModal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export function ErrorPinManageModal({
145145
}, [fetchPins]);
146146

147147
const getAssignmentTitle = (assignmentId: number | null) => {
148-
if (!assignmentId) return "Unknown Assignment";
148+
if (assignmentId === null) return "All Assignments (Class-Level)";
149149
const assignment = assignments.find((a) => a.id === assignmentId);
150150
return assignment?.title || `Assignment #${assignmentId}`;
151151
};
@@ -198,8 +198,9 @@ export function ErrorPinManageModal({
198198
{pins.map((pin) => (
199199
<Box key={pin.id} border="1px solid" borderColor="border.emphasized" borderRadius="md" p={4}>
200200
<HStack justify="space-between" mb={2}>
201-
<HStack gap={2}>
201+
<HStack gap={2} flexWrap="wrap">
202202
<Text fontWeight="semibold">{getAssignmentTitle(pin.assignment_id)}</Text>
203+
{pin.assignment_id === null && <Badge colorPalette="purple">Class-Level</Badge>}
203204
{pin.enabled ? (
204205
<Badge colorPalette="green">Enabled</Badge>
205206
) : (
@@ -208,7 +209,7 @@ export function ErrorPinManageModal({
208209
<Badge colorPalette="blue">
209210
{pin.rule_count} Rule{pin.rule_count !== 1 ? "s" : ""}
210211
</Badge>
211-
<Badge colorPalette="purple">
212+
<Badge colorPalette="cyan">
212213
{pin.match_count} Match{pin.match_count !== 1 ? "es" : ""}
213214
</Badge>
214215
<Text fontSize="sm" color="fg.muted">

components/discussion/ErrorPinModal.tsx

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
Text
1919
} from "@chakra-ui/react";
2020
import { useParams } from "next/navigation";
21-
import { useCallback, useEffect, useMemo, useState } from "react";
21+
import { useCallback, useEffect, useState } from "react";
2222
import { Database } from "@/utils/supabase/SupabaseTypes";
2323
import { Controller, useFieldArray, useForm } from "react-hook-form";
2424
import { BsPlus, BsTrash, BsX } from "react-icons/bs";
@@ -48,6 +48,7 @@ interface ErrorPinRule {
4848

4949
interface ErrorPinFormData {
5050
assignment_id: number | null;
51+
is_class_level: boolean;
5152
rule_logic: "and" | "or";
5253
enabled: boolean;
5354
rules: ErrorPinRule[];
@@ -112,14 +113,15 @@ export function ErrorPinModal({
112113
} = useForm<ErrorPinFormData>({
113114
defaultValues: {
114115
assignment_id: defaultAssignmentId ?? null,
116+
is_class_level: false,
115117
rule_logic: "and",
116118
enabled: true,
117119
rules: []
118120
}
119121
});
120122

121123
// Set default assignment when modal opens with defaultAssignmentId
122-
useMemo(() => {
124+
useEffect(() => {
123125
if (defaultAssignmentId && !existingPinId) {
124126
setValue("assignment_id", defaultAssignmentId);
125127
}
@@ -131,6 +133,7 @@ export function ErrorPinModal({
131133
});
132134

133135
const assignmentId = watch("assignment_id");
136+
const isClassLevel = watch("is_class_level");
134137
const rules = watch("rules");
135138

136139
// Load existing pin data if editing
@@ -186,6 +189,7 @@ export function ErrorPinModal({
186189
if (existingPin && existingRules) {
187190
reset({
188191
assignment_id: existingPin.assignment_id,
192+
is_class_level: existingPin.assignment_id === null,
189193
rule_logic: existingPin.rule_logic as "and" | "or",
190194
enabled: existingPin.enabled,
191195
rules: existingRules.map((r) => ({
@@ -221,10 +225,20 @@ export function ErrorPinModal({
221225
};
222226

223227
const handlePreview = async () => {
224-
if (!assignmentId || rules.length === 0) {
228+
// For class-level pins, we don't need an assignment_id
229+
// For assignment-level pins, we need an assignment_id
230+
if (!isClassLevel && !assignmentId) {
231+
toaster.error({
232+
title: "Error",
233+
description: "Please select an assignment or choose 'All Assignments (Class-Level)'"
234+
});
235+
return;
236+
}
237+
238+
if (rules.length === 0) {
225239
toaster.error({
226240
title: "Error",
227-
description: "Please select an assignment and add at least one rule"
241+
description: "Please add at least one rule"
228242
});
229243
return;
230244
}
@@ -239,9 +253,10 @@ export function ErrorPinModal({
239253
match_value_max: r.match_value_max?.trim() || null
240254
}));
241255
const { data, error } = await supabase.rpc("preview_error_pin_matches", {
242-
p_assignment_id: assignmentId,
256+
p_assignment_id: isClassLevel ? null : assignmentId,
243257
p_rules: sanitizedRules as unknown as Json,
244-
p_rule_logic: watch("rule_logic")
258+
p_rule_logic: watch("rule_logic"),
259+
p_class_id: isClassLevel ? Number(course_id) : null
245260
});
246261

247262
if (error) throw error;
@@ -264,10 +279,20 @@ export function ErrorPinModal({
264279
};
265280

266281
const onSubmit = async (data: ErrorPinFormData) => {
267-
if (!data.assignment_id || data.rules.length === 0) {
282+
// For class-level pins, assignment_id should be null
283+
// For assignment-level pins, we need a valid assignment_id
284+
if (!data.is_class_level && !data.assignment_id) {
285+
toaster.error({
286+
title: "Error",
287+
description: "Please select an assignment or choose 'All Assignments (Class-Level)'"
288+
});
289+
return;
290+
}
291+
292+
if (data.rules.length === 0) {
268293
toaster.error({
269294
title: "Error",
270-
description: "Please select an assignment and add at least one rule"
295+
description: "Please add at least one rule"
271296
});
272297
return;
273298
}
@@ -285,7 +310,7 @@ export function ErrorPinModal({
285310
const pinData = {
286311
id: existingPinId || undefined,
287312
discussion_thread_id,
288-
assignment_id: data.assignment_id,
313+
assignment_id: data.is_class_level ? null : data.assignment_id,
289314
class_id: Number(course_id),
290315
created_by: private_profile_id,
291316
rule_logic: data.rule_logic,
@@ -337,25 +362,55 @@ export function ErrorPinModal({
337362
<Dialog.Body>
338363
<form onSubmit={handleSubmit(onSubmit)}>
339364
<Stack spaceY={4}>
340-
<Field.Root invalid={!!errors.assignment_id}>
341-
<Field.Label>Assignment</Field.Label>
342-
<NativeSelect.Root>
343-
<NativeSelect.Field
344-
{...register("assignment_id", {
345-
required: "Assignment is required",
346-
valueAsNumber: true
347-
})}
348-
>
349-
<option value="">Select an assignment</option>
350-
{assignments.map((assignment) => (
351-
<option key={assignment.id} value={assignment.id}>
352-
{assignment.title}
353-
</option>
354-
))}
355-
</NativeSelect.Field>
356-
</NativeSelect.Root>
357-
<Field.ErrorText>{errors.assignment_id?.message}</Field.ErrorText>
358-
</Field.Root>
365+
<Controller
366+
control={control}
367+
name="is_class_level"
368+
render={({ field }) => (
369+
<Field.Root>
370+
<ChakraButton
371+
variant={field.value ? "solid" : "outline"}
372+
colorPalette={field.value ? "purple" : "gray"}
373+
onClick={() => {
374+
field.onChange(!field.value);
375+
// Clear assignment selection when switching to class-level
376+
if (!field.value) {
377+
setValue("assignment_id", null);
378+
}
379+
}}
380+
size="sm"
381+
>
382+
{field.value ? "Class-Level Pin (All Assignments)" : "Assignment-Specific Pin"}
383+
</ChakraButton>
384+
<Field.HelperText>
385+
{field.value
386+
? "This pin will match against all submissions in the class, regardless of assignment"
387+
: "This pin will only match against submissions for a specific assignment"}
388+
</Field.HelperText>
389+
</Field.Root>
390+
)}
391+
/>
392+
393+
{!isClassLevel && (
394+
<Field.Root invalid={!!errors.assignment_id}>
395+
<Field.Label>Assignment</Field.Label>
396+
<NativeSelect.Root>
397+
<NativeSelect.Field
398+
{...register("assignment_id", {
399+
required: !isClassLevel ? "Assignment is required" : false,
400+
valueAsNumber: true
401+
})}
402+
>
403+
<option value="">Select an assignment</option>
404+
{assignments.map((assignment) => (
405+
<option key={assignment.id} value={assignment.id}>
406+
{assignment.title}
407+
</option>
408+
))}
409+
</NativeSelect.Field>
410+
</NativeSelect.Root>
411+
<Field.ErrorText>{errors.assignment_id?.message}</Field.ErrorText>
412+
</Field.Root>
413+
)}
359414

360415
<Controller
361416
control={control}
@@ -602,7 +657,7 @@ export function ErrorPinModal({
602657
variant="outline"
603658
onClick={handlePreview}
604659
loading={isPreviewing}
605-
disabled={!assignmentId || rules.length === 0}
660+
disabled={(!isClassLevel && !assignmentId) || rules.length === 0}
606661
>
607662
Preview Matches
608663
</ChakraButton>
@@ -614,7 +669,7 @@ export function ErrorPinModal({
614669
colorPalette="green"
615670
onClick={handleSubmit(onSubmit)}
616671
loading={isSubmitting}
617-
disabled={!assignmentId || rules.length === 0}
672+
disabled={(!isClassLevel && !assignmentId) || rules.length === 0}
618673
>
619674
{existingPinId ? "Update Pin" : "Create Pin"}
620675
</ChakraButton>

supabase/functions/_shared/SupabaseTypes.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2963,7 +2963,7 @@ export type Database = {
29632963
};
29642964
error_pins: {
29652965
Row: {
2966-
assignment_id: number;
2966+
assignment_id: number | null;
29672967
class_id: number;
29682968
created_at: string;
29692969
created_by: string;
@@ -2973,7 +2973,7 @@ export type Database = {
29732973
rule_logic: string;
29742974
};
29752975
Insert: {
2976-
assignment_id: number;
2976+
assignment_id?: number | null;
29772977
class_id: number;
29782978
created_at?: string;
29792979
created_by: string;
@@ -2983,7 +2983,7 @@ export type Database = {
29832983
rule_logic?: string;
29842984
};
29852985
Update: {
2986-
assignment_id?: number;
2986+
assignment_id?: number | null;
29872987
class_id?: number;
29882988
created_at?: string;
29892989
created_by?: string;
@@ -11559,7 +11559,7 @@ export type Database = {
1155911559
Returns: number;
1156011560
};
1156111561
preview_error_pin_matches: {
11562-
Args: { p_assignment_id: number; p_rule_logic?: string; p_rules: Json };
11562+
Args: { p_assignment_id: number | null; p_class_id?: number | null; p_rule_logic?: string; p_rules: Json };
1156311563
Returns: Json;
1156411564
};
1156511565
process_calendar_announcements: { Args: never; Returns: Json };

0 commit comments

Comments
 (0)