Improved alert formatting, and edit student page.

This commit is contained in:
Andrew Lalis 2025-02-20 11:55:25 -05:00
parent 1d4d98665d
commit b69852817e
5 changed files with 84 additions and 18 deletions

View File

@ -2,16 +2,31 @@
* Shows a simple message dialog, with a 'close' button. It uses a pre-rendered * Shows a simple message dialog, with a 'close' button. It uses a pre-rendered
* dialog element that's globally defined for the whole app. * dialog element that's globally defined for the whole app.
* @param msg The message to show. * @param msg The message to show.
* @param alertType The type of alert. "info", "warning", "error"
* @returns A promise that resolves when the alert is closed. * @returns A promise that resolves when the alert is closed.
*/ */
export function showAlert(msg: string): Promise<void> { export function showAlert(msg: string, alertType: string = 'info'): Promise<void> {
const dialog = document.getElementById('alert-dialog') as HTMLDialogElement const dialog = document.getElementById('alert-dialog') as HTMLDialogElement
dialog.classList.remove('alert-dialog-error', 'alert-dialog-warning') // Clear old styling classes if they're present.
if (alertType === 'warning') {
dialog.classList.add('alert-dialog-warning')
} else if (alertType === 'error') {
dialog.classList.add('alert-dialog-error')
}
const messageBox = document.getElementById('alert-dialog-message') as HTMLParagraphElement const messageBox = document.getElementById('alert-dialog-message') as HTMLParagraphElement
const closeButton = document.getElementById('alert-dialog-close-button') as HTMLButtonElement const closeButton = document.getElementById('alert-dialog-close-button') as HTMLButtonElement
closeButton.addEventListener('click', () => dialog.close()) closeButton.addEventListener('click', closeAlertDialog)
messageBox.innerText = msg messageBox.innerText = msg
dialog.showModal() dialog.showModal()
return new Promise((resolve) => { return new Promise((resolve) => {
dialog.addEventListener('close', () => resolve()) dialog.addEventListener('close', () => {
closeButton.removeEventListener('click', closeAlertDialog)
resolve()
})
}) })
} }
function closeAlertDialog() {
const dialog = document.getElementById('alert-dialog') as HTMLDialogElement
dialog.close()
}

View File

@ -30,12 +30,29 @@ export class APIResponse<T> {
async handleErrorsWithAlert(): Promise<T | null> { async handleErrorsWithAlert(): Promise<T | null> {
const value = await this.result const value = await this.result
if (value instanceof APIError) { if (value instanceof APIError) {
await showAlert(value.message) if (value instanceof BadRequestError || value instanceof AuthenticationError) {
await showAlert(value.message, 'warning')
} else {
await showAlert(value.message, 'error')
}
return null return null
} }
return value return value
} }
async handleErrorsWithAlertNoBody(): Promise<boolean> {
const value = await this.result
if (value instanceof APIError) {
if (value instanceof BadRequestError || value instanceof AuthenticationError) {
await showAlert(value.message, 'warning')
} else {
await showAlert(value.message, 'error')
}
return false
}
return true
}
async getOrThrow(): Promise<T> { async getOrThrow(): Promise<T> {
const value = await this.result const value = await this.result
if (value instanceof APIError) throw value if (value instanceof APIError) throw value

View File

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { showAlert } from '@/alerts';
import { APIError } from '@/api/base';
import { import {
ClassroomComplianceAPIClient, ClassroomComplianceAPIClient,
type Class, type Class,
@ -37,24 +35,35 @@ interface StudentFormData {
const formData: Ref<StudentFormData> = ref({ name: '', deskNumber: null, removed: false }) const formData: Ref<StudentFormData> = ref({ name: '', deskNumber: null, removed: false })
onMounted(async () => { onMounted(async () => {
if (!await loadClass()) return
await loadStudent()
})
async function loadClass(): Promise<boolean> {
const classIdNumber = parseInt(props.classId, 10) const classIdNumber = parseInt(props.classId, 10)
cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert() cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert()
if (!cls.value) { if (!cls.value) {
await router.replace('/classroom-compliance') await router.replace('/classroom-compliance')
return return false
} }
return true
}
async function loadStudent() {
const classId = cls.value?.id
if (!classId) return
if ('studentId' in route.query && typeof route.query.studentId === 'string') { if ('studentId' in route.query && typeof route.query.studentId === 'string') {
const studentId = parseInt(route.query.studentId, 10) const studentId = parseInt(route.query.studentId, 10)
student.value = await apiClient.getStudent(classIdNumber, studentId).handleErrorsWithAlert() student.value = await apiClient.getStudent(classId, studentId).handleErrorsWithAlert()
if (!student.value) { if (!student.value) {
await router.replace(`/classroom-compliance/classes/${classIdNumber}`) await router.replace(`/classroom-compliance/classes/${classId}`)
return return
} }
formData.value.name = student.value.name formData.value.name = student.value.name
formData.value.deskNumber = student.value.deskNumber formData.value.deskNumber = student.value.deskNumber
formData.value.removed = student.value.removed formData.value.removed = student.value.removed
} }
}) }
async function submitForm() { async function submitForm() {
const classId = parseInt(props.classId, 10) const classId = parseInt(props.classId, 10)
@ -111,9 +120,8 @@ async function doMoveClass() {
if (!choice) return if (!choice) return
const newClassId = moveClassDialogClassSelection.value.id const newClassId = moveClassDialogClassSelection.value.id
const result = await apiClient.moveStudentToOtherClass(cls.value.id, student.value.id, newClassId) const result = await apiClient.moveStudentToOtherClass(cls.value.id, student.value.id, newClassId)
if (result instanceof APIError) { .handleErrorsWithAlertNoBody()
await showAlert(result.message) if (result) {
} else {
await router.replace(`/classroom-compliance/classes/${newClassId}/students/${student.value.id}`) await router.replace(`/classroom-compliance/classes/${newClassId}/students/${student.value.id}`)
} }
} }
@ -131,17 +139,23 @@ async function doMoveClass() {
<div> <div>
<label for="name-input">Name</label> <label for="name-input">Name</label>
<input id="name-input" type="text" v-model="formData.name" required /> <input id="name-input" type="text" v-model="formData.name" required />
<p class="form-input-hint">
The full name of the student, which should be unique among all students in the class.
</p>
</div> </div>
<div> <div>
<label for="desk-input">Desk Number</label> <label for="desk-input">Desk Number</label>
<input id="desk-input" type="number" v-model="formData.deskNumber" /> <input id="desk-input" type="number" v-model="formData.deskNumber" />
<p style="font-style: italic; font-size: smaller;"> <p class="form-input-hint">
Set desk number to zero to remove a student's desk assignment. Set desk number to zero to remove a student's desk assignment.
</p> </p>
</div> </div>
<div> <div>
<label for="removed-checkbox" style="display: inline">Removed</label> <label for="removed-checkbox" style="display: inline">Removed</label>
<input id="removed-checkbox" type="checkbox" v-model="formData.removed" /> <input id="removed-checkbox" type="checkbox" v-model="formData.removed" />
<p class="form-input-hint">
Check this if the student has been removed from the class.
</p>
</div> </div>
<div class="button-bar"> <div class="button-bar">
<button type="submit">Save</button> <button type="submit">Save</button>
@ -151,19 +165,24 @@ async function doMoveClass() {
</form> </form>
<!-- Dialog for moving the student to another class. --> <!-- Dialog for moving the student to another class. -->
<dialog id="move-class-dialog" ref="move-class-dialog"> <dialog id="move-class-dialog" ref="move-class-dialog" style="max-width: 500px;">
<p> <p>
Select a class to move them to below: Select a class to move {{ student?.name }} to:
</p> </p>
<select v-model="moveClassDialogClassSelection"> <select v-model="moveClassDialogClassSelection">
<option v-for="c in moveClassDialogClassChoices" :key="c.id" v-text="'Class ' + c.number" :value="c"></option> <option v-for="c in moveClassDialogClassChoices" :key="c.id" v-text="'Class ' + c.number" :value="c"></option>
</select> </select>
<p v-if="moveClassDialogClassSelection"> <p v-if="moveClassDialogClassSelection" class="form-input-hint">
Will move {{ student?.name }} from class {{ cls.number }} to class {{ moveClassDialogClassSelection.number }}. Will move {{ student?.name }} from class {{ cls.number }} to class {{ moveClassDialogClassSelection.number }}.
</p> </p>
<div> <p>
Note: All current entries for {{ student?.name }} in class {{ cls.number }} will be retained; no data is lost.
To add new entries for {{ student?.name }}, do so via their new class.
</p>
<div class="button-bar">
<button type="button" id="move-class-dialog-submit-button" @click="doMoveClass()">Submit</button> <button type="button" id="move-class-dialog-submit-button" @click="doMoveClass()">Submit</button>
<button type="button" id="move-class-dialog-cancel-button" @click="moveClassDialog?.close()">Cancel</button> <button type="button" id="move-class-dialog-cancel-button" @click="moveClassDialog?.close()">Cancel</button>
</div> </div>

View File

@ -19,3 +19,9 @@ label {
form > div + div { form > div + div {
margin-top: 0.5em; margin-top: 0.5em;
} }
.form-input-hint {
font-size: smaller;
font-style: italic;
margin-top: 0.25em;
}

View File

@ -7,3 +7,12 @@
</div> </div>
</dialog> </dialog>
</template> </template>
<style>
.alert-dialog-error {
border-color: red;
}
.alert-dialog-warning {
border-color: orange;
}
</style>