Skip to content

Commit

Permalink
Merge pull request #471 from softwareconstruction240/465-frontend-con…
Browse files Browse the repository at this point in the history
…fig-refactor

Frontend: refactor config page to be more adaptable and modularized
  • Loading branch information
webecke authored Nov 12, 2024
2 parents f3b893b + b650990 commit c22a126
Show file tree
Hide file tree
Showing 5 changed files with 425 additions and 342 deletions.
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>
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>
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&nbsp;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>
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>
Loading

0 comments on commit c22a126

Please sign in to comment.