-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #471 from softwareconstruction240/465-frontend-con…
…fig-refactor Frontend: refactor config page to be more adaptable and modularized
- Loading branch information
Showing
5 changed files
with
425 additions
and
342 deletions.
There are no files selected for viewing
75 changes: 75 additions & 0 deletions
75
src/main/resources/frontend/src/components/config/BannerConfigEditor.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<script setup lang="ts"> | ||
import { ref } from 'vue' | ||
import { useAppConfigStore } from '@/stores/appConfig' | ||
import { setBanner } from '@/services/configService' | ||
const { closeEditor } = defineProps<{ | ||
closeEditor: () => void | ||
}>(); | ||
const appConfigStore = useAppConfigStore(); | ||
const bannerMessageToSubmit = ref<string>(appConfigStore.bannerMessage) | ||
const bannerColorToSubmit = ref<string>(appConfigStore.bannerColor) | ||
const bannerLinkToSubmit = ref<string>(appConfigStore.bannerLink) | ||
const bannerWillExpire = ref<boolean>(false) | ||
const bannerExpirationDate = ref<string>("") | ||
const bannerExpirationTime = ref<string>("") | ||
const clearBannerMessage = () => { | ||
bannerMessageToSubmit.value = "" | ||
bannerLinkToSubmit.value = "" | ||
bannerColorToSubmit.value = "" | ||
bannerColorToSubmit.value = "" | ||
bannerWillExpire.value = false | ||
} | ||
const submitBanner = async () => { | ||
let combinedDateTime; | ||
if (bannerWillExpire.value) { | ||
combinedDateTime = `${bannerExpirationDate.value}T${bannerExpirationTime.value ? bannerExpirationTime.value : "23:59"}:59`; | ||
} else { | ||
combinedDateTime = "" | ||
} | ||
try { | ||
await setBanner(bannerMessageToSubmit.value, bannerLinkToSubmit.value, bannerColorToSubmit.value, combinedDateTime) | ||
} catch (e) { | ||
alert("There was a problem in saving the updated banner message:\n" + e) | ||
} | ||
closeEditor() | ||
} | ||
</script> | ||
|
||
<template> | ||
<p>Set a message for students to see from the Autograder</p> | ||
<input v-model="bannerMessageToSubmit" type="text" placeholder="No Banner Message"/> | ||
<p>Set a url that the user will be taken to if they click on the banner</p> | ||
<input v-model="bannerLinkToSubmit" type="text" placeholder="No Destination URL"/> | ||
<p>Choose a background color</p> | ||
<select id="bannerColorSelect" v-model="bannerColorToSubmit" :style="{ | ||
backgroundColor: bannerColorToSubmit, | ||
color: (bannerColorToSubmit ? '#ffffff' : '#000000') | ||
}"> | ||
<option selected value="">Default</option> | ||
<option value="#d62b18">Red</option> | ||
<option value="#eb700c">Orange</option> | ||
<option value="#ded77a">Yellow</option> | ||
<option value="#0cab11">Green</option> | ||
<option value="#002E5D">BYU Blue</option> | ||
<option value="#5e12b5">Purple</option> | ||
<option value="#424142">Gray</option> | ||
<option value="#000000">Black</option> | ||
</select> | ||
<p>Message Expires: <input type="checkbox" v-model="bannerWillExpire"/></p> | ||
<div v-if="bannerWillExpire"> | ||
<input type="date" v-model="bannerExpirationDate"/><input type="time" v-model="bannerExpirationTime"/> | ||
<p><em>If no time is selected, it will expire at the end of the day (Utah Time)</em></p> | ||
</div> | ||
|
||
<div> | ||
<button class="small" @click="submitBanner" :disabled="bannerWillExpire && (bannerExpirationDate.length == 0)">Save</button> | ||
<button class="small" @click="clearBannerMessage">Clear</button> | ||
</div> | ||
</template> | ||
|
||
<style scoped> | ||
</style> |
72 changes: 72 additions & 0 deletions
72
src/main/resources/frontend/src/components/config/ConfigSection.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<script setup lang="ts"> | ||
/** | ||
* A reusable wrapper component that provides a consistent interface for editable configuration sections. | ||
* Each section includes a title, description, current value display, and an editable popup interface. | ||
* | ||
* All editors should take a function as a prop for closing the editor popup. The function is provided by | ||
* the ConfigSection component by decomposition. | ||
* | ||
* <template #editor="{ closeEditor }"> gives any element inside the template tag access to the `closeEditor()` | ||
* function, which will close the editor popup. | ||
* | ||
* ConfigSection will automatically reload the config from the server each time an editor is opened, to ensure | ||
* the admin has the most upto date config | ||
* | ||
* @example | ||
* <ConfigSection | ||
* title="Banner Message" | ||
* description="A dynamic message displayed across the top of the Autograder" | ||
* > | ||
* <template #current> | ||
* <p>Current message: {{ currentMessage }}</p> | ||
* </template> | ||
* <template #editor="{ closeEditor }"> | ||
* <BannerConfigEditor :closeEditor="closeEditor"/> | ||
* </template> | ||
* </ConfigSection> | ||
*/ | ||
import { ref } from 'vue' | ||
import PopUp from '@/components/PopUp.vue' | ||
import { useAppConfigStore } from '@/stores/appConfig' | ||
defineProps<{ | ||
title: string | ||
description: string | ||
}>() | ||
const editorPopup = ref<boolean>(false); | ||
const openEditor = () => { | ||
useAppConfigStore().updateConfig() | ||
editorPopup.value = true | ||
} | ||
const closeEditor = () => { | ||
editorPopup.value = false; | ||
useAppConfigStore().updateConfig() | ||
} | ||
</script> | ||
|
||
<template> | ||
<section class="config-section"> | ||
<h3 @click="openEditor" style="cursor: pointer">{{ title }} <i class="fa-solid fa-pen-to-square"/></h3> | ||
<p>{{ description }}</p> | ||
<slot name="current"/> | ||
<PopUp | ||
v-if="editorPopup" | ||
@closePopUp="editorPopup = false"> | ||
<h3>Edit {{ title }}</h3> | ||
<slot name="editor" :closeEditor="closeEditor"/> | ||
</PopUp> | ||
</section> | ||
</template> | ||
|
||
<style scoped> | ||
.config-section { | ||
padding: 1rem; | ||
border: 1px solid #e2e8f0; | ||
border-radius: 0.5rem; | ||
background-color: white; | ||
} | ||
</style> |
159 changes: 159 additions & 0 deletions
159
src/main/resources/frontend/src/components/config/CourseIdConfigEditor.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<script setup lang="ts"> | ||
import { listOfPhases, Phase, type RubricInfo, type RubricType } from '@/types/types' | ||
import { | ||
convertPhaseStringToEnum, | ||
convertRubricTypeToHumanReadable, | ||
getRubricTypes, | ||
isPhaseGraded | ||
} from '@/utils/utils' | ||
import { computed, type WritableComputedRef } from 'vue' | ||
import { setCanvasCourseIds, setCourseIds } from '@/services/configService' | ||
import { useAppConfigStore } from '@/stores/appConfig' | ||
const appConfigStore = useAppConfigStore(); | ||
const { closeEditor } = defineProps<{ | ||
closeEditor: () => void | ||
}>(); | ||
const assignmentIdProxy = (phase: Phase): WritableComputedRef<number> => computed({ | ||
get: (): number => appConfigStore.assignmentIds.get(phase) || -1, | ||
set: (value: number) => appConfigStore.assignmentIds.set(phase, value) | ||
}) | ||
const rubricIdInfoProxy = (phase: Phase, rubricType: RubricType): WritableComputedRef<string> => { | ||
return getProxy( | ||
phase, | ||
rubricType, | ||
(rubricInfo) => rubricInfo.id, | ||
(rubricInfo, value) => rubricInfo.id = value, | ||
"No Rubric ID found" | ||
); | ||
} | ||
const rubricPointsInfoProxy = (phase: Phase, rubricType: RubricType): WritableComputedRef<number> => { | ||
return getProxy( | ||
phase, | ||
rubricType, | ||
(rubricInfo) => rubricInfo.points, | ||
(rubricInfo, value) => rubricInfo.points = value, | ||
-1 | ||
); | ||
} | ||
const getProxy = <T>( | ||
phase: Phase, | ||
rubricType: RubricType, | ||
getFunc: (rubricInfo: RubricInfo) => T, | ||
setFunc: (rubricInfo: RubricInfo, value: T) => void, | ||
defaultValue: T, | ||
): WritableComputedRef<T> => computed({ | ||
get: (): T => { | ||
const rubricIdMap = appConfigStore.rubricInfo.get(phase); | ||
if (!rubricIdMap) return defaultValue; | ||
const rubricInfo = rubricIdMap.get(rubricType); | ||
if (!rubricInfo) return defaultValue; | ||
return getFunc(rubricInfo); | ||
}, | ||
set: (value: T) => { | ||
const rubricTypeMap = appConfigStore.rubricInfo.get(phase); | ||
if (!rubricTypeMap) return; | ||
const rubricInfo = rubricTypeMap.get(rubricType); | ||
if (!rubricInfo) return; | ||
setFunc(rubricInfo, value); | ||
} | ||
}); | ||
const submitManuelCourseIds = async () => { | ||
const userConfirmed = window.confirm("Are you sure you want to manually override? \n\nIf you changed the course ID incorrectly, it won't be able to reset properly."); | ||
if (userConfirmed) { | ||
try { | ||
await setCourseIds(appConfigStore.courseNumber, appConfigStore.assignmentIds, appConfigStore.rubricInfo); | ||
closeEditor() | ||
} catch (e) { | ||
alert("There was problem manually setting the course-related IDs: " + (e as Error).message); | ||
} | ||
} | ||
} | ||
const submitCanvasCourseIds = async () => { | ||
const userConfirmed = window.confirm("Are you sure you want to use Canvas to reset ID values? \n\nNote: This will fail if the currently saved Course ID is incorrect.") | ||
if (userConfirmed) { | ||
try { | ||
await setCanvasCourseIds(); | ||
} catch (e) { | ||
alert("There was problem getting and setting the course-related IDs using Canvas: " + (e as Error).message); | ||
} | ||
closeEditor() | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<p> | ||
<i class="fa-solid fa-triangle-exclamation" style="color: orangered"/> | ||
Note: All the default input values are the values that are currently being used. | ||
</p> | ||
|
||
<br> | ||
<h4>Course Number</h4> | ||
<label for="courseIdInput">Course Number: </label> | ||
<input id="courseIdInput" type="number" v-model.number="appConfigStore.courseNumber" placeholder="Course Number"> | ||
<br><br> | ||
<h4>Assignment and Rubric IDs/Points</h4> | ||
<div v-for="(phase, phaseIndex) in listOfPhases()" :key="phaseIndex"> | ||
<div v-if="isPhaseGraded(phase)"> | ||
<h4>{{ phase }}:</h4> | ||
<label :for="'assignmentIdInput' + phaseIndex">Assignment ID: </label> | ||
<input | ||
:id="'assignmentIdInput' + phaseIndex" | ||
type="number" | ||
v-model.number="assignmentIdProxy(phase).value" | ||
placeholder="Assignment ID" | ||
> | ||
<br> | ||
|
||
<ol> | ||
<li v-for="(rubricType, rubricIndex) in getRubricTypes(convertPhaseStringToEnum(phase as unknown as string))" :key="rubricIndex"> | ||
<u>{{ convertRubricTypeToHumanReadable(rubricType) }}</u>: | ||
<div class="inline-container"> | ||
<label :for="'rubricIdInput' + phaseIndex + rubricIndex">Rubric ID: </label> | ||
<input | ||
:id="'rubricIdInput' + phaseIndex + rubricIndex" | ||
type="text" | ||
v-model="rubricIdInfoProxy(phase, rubricType).value" | ||
placeholder="Rubric ID" | ||
> | ||
</div> | ||
<div class="inline-container"> | ||
<label :for="'rubricPointsInput' + phaseIndex + rubricIndex">Rubric Points: </label> | ||
<input | ||
:id="'rubricPointsInput' + phaseIndex + rubricIndex" | ||
type="number" | ||
v-model.number="rubricPointsInfoProxy(phase, rubricType).value" | ||
placeholder="Points" | ||
> | ||
</div> | ||
</li> | ||
</ol> | ||
</div> | ||
</div> | ||
|
||
<br> | ||
<button @click="submitManuelCourseIds">Submit</button> | ||
<button @click="submitCanvasCourseIds">Reset IDs Via Canvas</button> | ||
</template> | ||
|
||
<style scoped> | ||
.inline-container { | ||
display: flex; | ||
align-items: center; | ||
margin-right: 10px; /* Optional: Adjust spacing between elements */ | ||
margin-left: 10px; | ||
} | ||
.inline-container label { | ||
margin-right: 5px; /* Optional: Adjust spacing between label and input */ | ||
} | ||
</style> |
64 changes: 64 additions & 0 deletions
64
src/main/resources/frontend/src/components/config/LivePhaseConfigEditor.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<script setup lang="ts"> | ||
import { listOfPhases, Phase } from '@/types/types' | ||
import { useAppConfigStore } from '@/stores/appConfig' | ||
import { setLivePhases } from '@/services/configService' | ||
const appConfigStore = useAppConfigStore(); | ||
const { closeEditor } = defineProps<{ | ||
closeEditor: () => void | ||
}>(); | ||
const setAllPhases = (setting: boolean) => { | ||
for (const phase of listOfPhases() as Phase[]) { | ||
appConfigStore.phaseActivationList[phase] = setting | ||
} | ||
} | ||
const submitLivePhases = async () => { | ||
let livePhases: Phase[] = [] | ||
for (const phase of listOfPhases() as Phase[]) { | ||
if (useAppConfigStore().phaseActivationList[phase]) { | ||
livePhases.push(phase); | ||
} | ||
} | ||
try { | ||
await setLivePhases(livePhases) | ||
} catch (e) { | ||
alert("There was a problem in saving live phases") | ||
} | ||
closeEditor() | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="checkboxes"> | ||
<label v-for="(phase, index) in listOfPhases()" :key="index"> | ||
<span><input type="checkbox" v-model="appConfigStore.phaseActivationList[phase]"> {{ phase }}</span> | ||
</label> | ||
</div> | ||
|
||
<div class="submitChanges"> | ||
<p><em>This will not effect admin submissions</em></p> | ||
<div> | ||
<button @click="setAllPhases(true)" class="small">Enable all</button> | ||
<button @click="setAllPhases(false)" class="small">Disable all</button> | ||
</div> | ||
<button @click="submitLivePhases">Submit Changes</button> | ||
</div> | ||
</template> | ||
|
||
<style scoped> | ||
.checkboxes { | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
.submitChanges { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
} | ||
.submitChanges >* { | ||
margin: 5px; | ||
} | ||
</style> |
Oops, something went wrong.