Skip to content
Merged
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
19 changes: 19 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"admin_tasks_card_updated": "Updated:",
"admin_tasks_card_user_prefix": "User #",
"admin_tasks_card_view_details": "View Task",
"admin_tasks_card_visible_to_users": "Visible to Users",
"admin_tasks_dialog_description": "Upload a task archive to the FileStorage service. The archive should contain all necessary files for the task.",
"admin_tasks_dialog_title": "Upload Task",
"admin_tasks_form_archive_label": "Task Archive",
Expand All @@ -106,6 +107,24 @@
"admin_tasks_form_title_label": "Task Title",
"admin_tasks_form_title_placeholder": "Enter task title",
"admin_tasks_form_upload": "Upload",
"admin_tasks_form_update": "Update",
"admin_tasks_form_updating": "Updating...",
"admin_tasks_form_visible_label": "Make task visible to all users",
"admin_tasks_form_visible_description": "When checked, this task will be visible to all users immediately",
"admin_tasks_visibility_confirm_title": "Confirm Visibility Change",
"admin_tasks_visibility_confirm_enable": "Are you sure you want to make \"{taskTitle}\" visible to all users?",
"admin_tasks_visibility_confirm_disable": "Are you sure you want to hide \"{taskTitle}\" from users?",
"admin_tasks_visibility_confirm": "Confirm",
"admin_tasks_visibility_enabled": "Task is now visible to users",
"admin_tasks_visibility_disabled": "Task is now hidden from users",
"admin_tasks_visibility_error": "Failed to update task visibility",
"admin_tasks_card_edit": "Edit Task",
"admin_tasks_edit_dialog_title": "Edit Task",
"admin_tasks_edit_dialog_description": "Update the task details. Leave fields empty to keep current values.",
"admin_tasks_edit_form_title_note": "Leave empty to keep the current title",
"admin_tasks_edit_form_archive_note": "Optional - only upload if you want to replace the task archive",
"admin_tasks_edit_success": "Task updated successfully!",
"admin_tasks_edit_error": "Failed to update task. Please try again.",
"admin_tasks_load_error_title": "Failed to load tasks",
"admin_tasks_loading": "Loading tasks...",
"admin_tasks_no_tasks_description": "Upload your first task to get started",
Expand Down
19 changes: 19 additions & 0 deletions messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"admin_tasks_card_updated": "Zaktualizowano:",
"admin_tasks_card_user_prefix": "Użytkownik #",
"admin_tasks_card_view_details": "Zobacz Zadanie",
"admin_tasks_card_visible_to_users": "Widoczne dla użytkowników",
"admin_tasks_dialog_description": "Prześlij archiwum zadania do serwisu FileStorage. Archiwum powinno zawierać wszystkie niezbędne pliki zadania.",
"admin_tasks_dialog_title": "Prześlij zadanie",
"admin_tasks_form_archive_label": "Archiwum Zadania",
Expand All @@ -106,6 +107,24 @@
"admin_tasks_form_title_label": "Tytuł Zadania",
"admin_tasks_form_title_placeholder": "Wprowadź tytuł zadania",
"admin_tasks_form_upload": "Prześlij",
"admin_tasks_form_update": "Aktualizuj",
"admin_tasks_form_updating": "Aktualizowanie...",
"admin_tasks_form_visible_label": "Udostępnij zadanie wszystkim użytkownikom",
"admin_tasks_form_visible_description": "Kiedy zaznaczone, to zadanie będzie natychmiast widoczne dla wszystkich użytkowników",
"admin_tasks_visibility_confirm_title": "Potwierdź zmianę widoczności",
"admin_tasks_visibility_confirm_enable": "Czy na pewno chcesz udostępnić zadanie \"{taskTitle}\" wszystkim użytkownikom?",
"admin_tasks_visibility_confirm_disable": "Czy na pewno chcesz ukryć zadanie \"{taskTitle}\" przed użytkownikami?",
"admin_tasks_visibility_confirm": "Potwierdź",
"admin_tasks_visibility_enabled": "Zadanie jest teraz widoczne dla użytkowników",
"admin_tasks_visibility_disabled": "Zadanie jest teraz ukryte przed użytkownikami",
"admin_tasks_visibility_error": "Nie udało się zaktualizować widoczności zadania",
"admin_tasks_card_edit": "Edytuj Zadanie",
"admin_tasks_edit_dialog_title": "Edytuj Zadanie",
"admin_tasks_edit_dialog_description": "Zaktualizuj szczegóły zadania. Pozostaw pola puste, aby zachować obecne wartości.",
"admin_tasks_edit_form_title_note": "Pozostaw puste, aby zachować obecny tytuł",
"admin_tasks_edit_form_archive_note": "Opcjonalne - prześlij tylko jeśli chcesz zastąpić archiwum zadania",
"admin_tasks_edit_success": "Zadanie zostało pomyślnie zaktualizowane!",
"admin_tasks_edit_error": "Nie udało się zaktualizować zadania. Spróbuj ponownie.",
"admin_tasks_load_error_title": "Nie udało się załadować zadań",
"admin_tasks_loading": "Ładowanie zadań...",
"admin_tasks_no_tasks_description": "Prześlij swoje pierwsze zadanie, aby zacząć",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
import * as m from '$lib/paraglide/messages';
import { Permission } from '$lib/dto/accessControl';
import type { User } from '$lib/dto/user';
import type { Collaborator } from '$lib/dto/accessControl';
import type { AddCollaboratorForm } from '$routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote';
import { LoadingSpinner } from '$lib/components/common';
import { UserRole } from '$lib/dto/jwt';
import type { PaginatedData } from '$lib/dto/response';

interface Props {
Expand All @@ -23,31 +21,17 @@
users: PaginatedData<User> | undefined;
usersLoading: boolean;
usersError: Error | null;
existingCollaborators: Collaborator[] | undefined;
}

let {
contestId,
addCollaborator,
users,
usersLoading,
usersError,
existingCollaborators
}: Props = $props();
let { contestId, addCollaborator, users, usersLoading, usersError }: Props = $props();

let dialogOpen = $state(false);
let searchQuery = $state('');
let selectedUserId = $state<number | null>(null);
let selectedPermission = $state<Permission | null>(null);

// Filter out users who are already collaborators and users with student role
let availableUsers = $derived.by(() => {
if (!users) return [];
const collaboratorIds = new Set(existingCollaborators?.map((c) => c.userId) ?? []);
return users.items.filter(
(user) => !collaboratorIds.has(user.id) && user.role !== UserRole.Student
);
});
// Backend returns only assignable users (teachers who aren't already collaborators)
let availableUsers = $derived(users?.items ?? []);

// Filter users by search query
let filteredUsers = $derived.by(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@

<div class="flex items-center gap-3">
<Checkbox
id="isRegistrationOpen"
{...updateContest.fields.isRegistrationOpen.as('checkbox')}
checked={contest.isRegistrationOpen}
/>
Expand All @@ -263,7 +262,6 @@

<div class="flex items-center gap-3">
<Checkbox
id="isSubmissionOpen"
{...updateContest.fields.isSubmissionOpen.as('checkbox')}
checked={contest.isSubmissionOpen}
/>
Expand All @@ -274,7 +272,6 @@

<div class="flex items-center gap-3">
<Checkbox
id="isVisible"
{...updateContest.fields.isVisible.as('checkbox')}
checked={contest.isVisible}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
import * as m from '$lib/paraglide/messages';
import { Permission } from '$lib/dto/accessControl';
import type { User } from '$lib/dto/user';
import type { Collaborator } from '$lib/dto/accessControl';
import type { AddCollaboratorForm } from '$routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote';
import { LoadingSpinner } from '$lib/components/common';
import { UserRole } from '$lib/dto/jwt';
import type { PaginatedData } from '$lib/dto/response';

interface Props {
Expand All @@ -23,25 +21,17 @@
users: PaginatedData<User> | undefined;
usersLoading: boolean;
usersError: Error | null;
existingCollaborators: Collaborator[] | undefined;
}

let { taskId, addCollaborator, users, usersLoading, usersError, existingCollaborators }: Props =
$props();
let { taskId, addCollaborator, users, usersLoading, usersError }: Props = $props();

let dialogOpen = $state(false);
let searchQuery = $state('');
let selectedUserId = $state<number | null>(null);
let selectedPermission = $state<Permission | null>(null);

// Filter out users who are already collaborators and users with student role
let availableUsers = $derived.by(() => {
if (!users) return [];
const collaboratorIds = new Set(existingCollaborators?.map((c) => c.userId) ?? []);
return users.items.filter(
(user) => !collaboratorIds.has(user.id) && user.role !== UserRole.Student
);
});
// Backend returns only assignable users (teachers who aren't already collaborators)
let availableUsers = $derived(users?.items ?? []);

// Filter users by search query
let filteredUsers = $derived.by(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script lang="ts">
import { toggleTaskVisibility } from '$routes/dashboard/teacher/tasks/tasks.remote';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { toast } from 'svelte-sonner';
import * as m from '$lib/paraglide/messages';

interface TaskVisibilityToggleProps {
taskId: number;
taskTitle: string;
initialVisibility: boolean;
onToggled: () => void;
}

let { taskId, taskTitle, initialVisibility, onToggled }: TaskVisibilityToggleProps = $props();

let isVisible = $state(initialVisibility);
let showConfirmDialog = $state(false);
let pendingVisibility = $state(false);
let checkboxKey = $state(0);

function handleCheckboxChange(checked: boolean) {
pendingVisibility = checked;
showConfirmDialog = true;
}

async function confirmToggle() {
try {
await toggleTaskVisibility({ taskId, isVisible: pendingVisibility });
isVisible = pendingVisibility;
showConfirmDialog = false;
toast.success(
pendingVisibility ? m.admin_tasks_visibility_enabled() : m.admin_tasks_visibility_disabled()
);
onToggled();
} catch (error) {
toast.error(m.admin_tasks_visibility_error());
console.error('Failed to toggle visibility:', error);
}
}

function cancelToggle() {
showConfirmDialog = false;
checkboxKey++; // Force checkbox to re-render with current isVisible value
}
</script>

<div class="flex items-center space-x-2">
{#key checkboxKey}
<Checkbox
id="visibility-{taskId}"
checked={isVisible}
onCheckedChange={handleCheckboxChange}
class={isVisible ? '' : 'bg-card'}
/>
{/key}
<Label
for="visibility-{taskId}"
class="cursor-pointer text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{m.admin_tasks_card_visible_to_users()}
</Label>
</div>

<Dialog.Root bind:open={showConfirmDialog}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>{m.admin_tasks_visibility_confirm_title()}</Dialog.Title>
<Dialog.Description>
{pendingVisibility
? m.admin_tasks_visibility_confirm_enable({ taskTitle })
: m.admin_tasks_visibility_confirm_disable({ taskTitle })}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={cancelToggle}>
{m.admin_tasks_form_cancel()}
</Button>
<Button type="button" onclick={confirmToggle}>
{m.admin_tasks_visibility_confirm()}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
16 changes: 16 additions & 0 deletions src/lib/components/dashboard/admin/tasks/TasksUploadDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Dialog from '$lib/components/ui/dialog';
import { toast } from 'svelte-sonner';
import { isHttpError } from '@sveltejs/kit';
Expand Down Expand Up @@ -123,6 +124,21 @@
{/if}
</div>

<div class="flex items-start space-x-2">
<Checkbox {...uploadTask.fields.isVisible.as('checkbox')} />
<div class="grid gap-1.5 leading-none">
<Label
for="isVisible"
class="cursor-pointer text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{m.admin_tasks_form_visible_label()}
</Label>
<p class="text-sm text-muted-foreground">
{m.admin_tasks_form_visible_description()}
</p>
</div>
</div>

<Dialog.Footer>
<Button type="button" variant="outline" onclick={handleCancel}>
{m.admin_tasks_form_cancel()}
Expand Down
11 changes: 11 additions & 0 deletions src/lib/components/dashboard/tasks/AdminTaskCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { localizeHref } from '$lib/paraglide/runtime';
import { AppRoutes } from '$lib/routes';
import ManageTestCasesLimitsDialog from '$lib/components/dashboard/admin/tasks/ManageTestCasesLimitsDialog.svelte';
import TaskVisibilityToggle from '$lib/components/dashboard/admin/tasks/TaskVisibilityToggle.svelte';
import RemoveTaskButton from '$lib/components/dashboard/admin/tasks/RemoveTaskButton.svelte';
import type { DeleteTaskForm } from '$routes/dashboard/teacher/tasks/tasks.remote';

Expand Down Expand Up @@ -74,6 +75,16 @@
</div>
</div>

<!-- Visibility Toggle -->
<div class="rounded-lg border border-border bg-muted/30 p-3">
<TaskVisibilityToggle
taskId={task.id}
taskTitle={task.title}
initialVisibility={task.isVisible}
onToggled={() => {}}
/>
</div>

<!-- Action Buttons -->
<div class="flex flex-col gap-2">
<Button
Expand Down
23 changes: 23 additions & 0 deletions src/lib/dto/accessControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export enum Permission {
Owner = 'owner'
}

/**
* Resource types for access control.
*/
export enum ResourceType {
Tasks = 'tasks',
Contests = 'contests'
}

/**
* Represents a collaborator on a task or contest.
*/
Expand All @@ -19,3 +27,18 @@ export interface Collaborator {
permission: Permission;
addedAt: string;
}

/**
* Request body for adding a collaborator.
*/
export interface AddCollaboratorRequest {
user_id: number;
permission: Permission.Edit | Permission.Manage;
}

/**
* Request body for updating a collaborator.
*/
export interface UpdateCollaboratorRequest {
permission: Permission.Edit | Permission.Manage;
}
2 changes: 2 additions & 0 deletions src/lib/dto/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Task {
createdAt: string;
updatedAt: string;
createdBy: number;
isVisible: boolean;
}

export interface TaskDetail {
Expand Down Expand Up @@ -37,6 +38,7 @@ export interface UploadTaskResponse {
export interface UploadTaskDto {
title: string;
archive: File;
isVisible: boolean;
}

export interface UserContestTask {
Expand Down
Loading