From 49e3b8126f981caf3ab36da2f02bfbc2db1ef147 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 4 Mar 2025 17:40:32 +0100 Subject: [PATCH] Re-orderable assignments --- .../kotlin/com/jaytux/grader/data/DSL.kt | 2 + .../kotlin/com/jaytux/grader/data/Entities.kt | 5 +- .../kotlin/com/jaytux/grader/ui/Editions.kt | 51 ++++++++++++---- .../kotlin/com/jaytux/grader/ui/Widgets.kt | 21 +++++-- .../com/jaytux/grader/viewmodel/DbState.kt | 59 +++++++++++++++++-- 5 files changed, 114 insertions(+), 24 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt index 215596b..4744a26 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt @@ -53,6 +53,7 @@ object EditionStudents : Table("editionStudents") { object GroupAssignments : UUIDTable("grpAssgmts") { val editionId = reference("edition_id", Editions.id) + val number = integer("number").nullable() val name = varchar("name", 50) val assignment = text("assignment") val deadline = datetime("deadline") @@ -60,6 +61,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") { object SoloAssignments : UUIDTable("soloAssgmts") { val editionId = reference("edition_id", Editions.id) + val number = integer("number").nullable() val name = varchar("name", 50) val assignment = text("assignment") val deadline = datetime("deadline") diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt index 01bf9fb..4af829e 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt @@ -10,9 +10,8 @@ import java.util.UUID class Course(id: EntityID) : Entity(id) { companion object : EntityClass(Courses) - fun loadEditions() = transaction { Edition.find { Editions.courseId eq this@Course.id }.toList() } - var name by Courses.name + val editions by Edition referrersOn Editions.courseId } class Edition(id: EntityID) : Entity(id) { @@ -57,6 +56,7 @@ class GroupAssignment(id: EntityID) : Entity(id) { companion object : EntityClass(GroupAssignments) var edition by Edition referencedOn GroupAssignments.editionId + var number by GroupAssignments.number var name by GroupAssignments.name var assignment by GroupAssignments.assignment var deadline by GroupAssignments.deadline @@ -66,6 +66,7 @@ class SoloAssignment(id: EntityID) : Entity(id) { companion object : EntityClass(SoloAssignments) var edition by Edition referencedOn SoloAssignments.editionId + var number by SoloAssignments.number var name by SoloAssignments.name var assignment by SoloAssignments.assignment var deadline by SoloAssignments.deadline diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt index a6d6187..93fb077 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt @@ -4,6 +4,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -11,8 +16,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* -import com.jaytux.grader.data.* +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberDialogState +import com.jaytux.grader.data.Course +import com.jaytux.grader.data.Edition +import com.jaytux.grader.data.Group +import com.jaytux.grader.data.Student import com.jaytux.grader.viewmodel.* data class Navigators( @@ -29,9 +39,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { val groups by state.groups.entities val solo by state.solo.entities val groupAs by state.groupAs.entities - val mergedAssignments by remember(solo, groupAs) { - mutableStateOf(Assignment.merge(groupAs, solo)) - } + val mergedAssignments by remember(solo, groupAs) { mutableStateOf(Assignment.merge(groupAs, solo)) } val hist by state.history val navs = Navigators( @@ -68,7 +76,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { course, edition, mergedAssignments, id, { state.navTo(it) }, { type, name -> state.newAssignment(type, name) }, - { a, name -> state.setAssignmentTitle(a, name) } + { a, name -> state.setAssignmentTitle(a, name) }, + { a1, a2 -> state.swapOrder(a1, a2) } ) { a -> state.delete(a) } } } @@ -90,7 +99,12 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { } OpenPanel.Assignment -> { if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition) - else PaneHeader(mergedAssignments[id].name(), "assignment", course, edition) + else { + when(val a = mergedAssignments[id]) { + is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition) + is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition) + } + } } } } @@ -211,7 +225,7 @@ fun AssignmentPanel( course: Course, edition: Edition, assignments: List, selected: Int, onSelect: (Int) -> Unit, onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit, - onDelete: (Assignment) -> Unit + onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit ) = Column(Modifier.padding(10.dp)) { var showDialog by remember { mutableStateOf(false) } var deleting by remember { mutableStateOf(-1) } @@ -264,12 +278,25 @@ fun AssignmentPanel( { Text("Add an assignment") }, { showDialog = true } ) { idx, it -> - SelectEditDeleteRow( + Selectable( selected == idx, - { onSelect(idx) }, { onSelect(-1) }, - { editing = idx }, { deleting = idx } + { onSelect(idx) }, { onSelect(-1) } ) { - Text(it.name(), Modifier.padding(5.dp)) + Row { + Text(it.name(), Modifier.padding(5.dp).align(Alignment.CenterVertically).weight(1f)) + Column(Modifier.padding(2.dp)) { + Icon(Icons.Default.ArrowUpward, "Move up", Modifier.clickable { + if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1]) + }) + Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable { + if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1]) + }) + } + Column(Modifier.padding(2.dp)) { + Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx }) + Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = idx }) + } + } } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index 10feea6..18940be 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -375,16 +375,27 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{ Text(normal) } +@Composable +fun Selectable( + isSelected: Boolean, + onSelect: () -> Unit, onDeselect: () -> Unit, + content: @Composable () -> Unit +) { + Surface( + Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, + tonalElevation = if (isSelected) 50.dp else 0.dp, + shape = MaterialTheme.shapes.medium + ) { + content() + } +} + @Composable fun SelectEditDeleteRow( isSelected: Boolean, onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit, content: @Composable BoxScope.() -> Unit -) = Surface( - Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, - tonalElevation = if (isSelected) 50.dp else 0.dp, - shape = MaterialTheme.shapes.medium - ) { +) = Selectable(isSelected, onSelect, onDeselect) { Row { Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() } IconButton(onEdit, Modifier.align(Alignment.CenterVertically)) { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index dfcc283..ea04040 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -13,6 +13,7 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import java.util.* +import kotlin.math.max fun MutableState.immutable(): State = this fun SizedIterable.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray())) @@ -22,14 +23,17 @@ sealed class Assignment { class GAssignment(val assignment: GroupAssignment) : Assignment() { override fun name(): String = assignment.name override fun id(): EntityID = assignment.id + override fun index(): Int? = assignment.number } class SAssignment(val assignment: SoloAssignment) : Assignment() { override fun name(): String = assignment.name override fun id(): EntityID = assignment.id + override fun index(): Int? = assignment.number } abstract fun name(): String abstract fun id(): EntityID + abstract fun index(): Int? companion object { fun from(assignment: GroupAssignment) = GAssignment(assignment) @@ -38,9 +42,7 @@ sealed class Assignment { fun merge(groups: List, solos: List): List { val g = groups.map { from(it) } val s = solos.map { from(it) } - return (g + s).sortedBy { - (it as? GAssignment)?.assignment?.name ?: (it as SAssignment).assignment.name - } + return (g + s).sortedWith(compareBy { it.index() }.thenBy { it.name() }) } } } @@ -153,9 +155,17 @@ class EditionState(val edition: Edition) { return instant.toLocalDateTime(TimeZone.currentSystemDefault()) } + private fun nextIdx(): Int = max( + solo.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0, + groupAs.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0 + ) + 1 + fun newSoloAssignment(name: String) { transaction { - SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() } + SoloAssignment.new { + this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() + this.number = nextIdx() + } solo.refresh() } } @@ -167,7 +177,10 @@ class EditionState(val edition: Edition) { } fun newGroupAssignment(name: String) { transaction { - GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() } + GroupAssignment.new { + this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() + this.number = nextIdx() + } groupAs.refresh() } } @@ -187,6 +200,42 @@ class EditionState(val edition: Edition) { is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title) } + fun swapOrder(a1: Assignment, a2: Assignment) { + transaction { + when(a1) { + is Assignment.GAssignment -> { + when(a2) { + is Assignment.GAssignment -> { + val temp = a1.assignment.number + a1.assignment.number = a2.assignment.number + a2.assignment.number = temp + } + is Assignment.SAssignment -> { + val temp = a1.assignment.number + a1.assignment.number = nextIdx() + a2.assignment.number = temp + } + } + } + is Assignment.SAssignment -> { + when(a2) { + is Assignment.GAssignment -> { + val temp = a1.assignment.number + a1.assignment.number = a2.assignment.number + a2.assignment.number = temp + } + is Assignment.SAssignment -> { + val temp = a1.assignment.number + a1.assignment.number = a2.assignment.number + a2.assignment.number = temp + } + } + } + } + } + solo.refresh(); groupAs.refresh() + } + fun delete(s: Student) { transaction { EditionStudents.deleteWhere { studentId eq s.id }