diff --git a/frontend/@types/v-clipboard/index.d.ts b/frontend/@types/v-clipboard/index.d.ts index 54bf07180256d6c40fc30d44b346cb01ebdf61eb..8619c86521a4634b98954c93b4cede9db2e182fa 100644 --- a/frontend/@types/v-clipboard/index.d.ts +++ b/frontend/@types/v-clipboard/index.d.ts @@ -1 +1,2 @@ declare module 'v-clipboard' +declare module 'vue-color' diff --git a/frontend/package.json b/frontend/package.json index a2ed358a0dfb4e34bc7fe544379fa11f31de08c5..2e960ea71a34a9b2f8a0ab68ad04ce9eb41e6fce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "v-clipboard": "^2.0.1", "vue": "^2.5.16", "vue-class-component": "^6.0.0", + "vue-color": "^2.7.0", "vue-notification": "^1.3.12", "vue-property-decorator": "^7.3.0", "vue-router": "^3.0.1", diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 22263bfd91698bba027e9e8ece295627f8e1d58d..a96d779f563aa4eec09f9a14733999aaeb3a7985 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -212,6 +212,14 @@ export async function getLabels () { return (await ax.get('/api/label')).data } +export async function createLabel (payload: Partial<FeedbackLabel>) { + return (await ax.post('/api/label/', payload)).data +} + +export async function updateLabel (payload: FeedbackLabel) { + return (await ax.put('/api/label/' + payload.pk + '/', payload)) +} + export interface StudentExportOptions { setPasswords?: boolean } export interface StudentExportItem { Matrikel: string, diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index b68d805f57860f6bd58bfd373c0333fa2251228a..ae32a6af46cbccbbbde8429a8d25b7cd0f03d559 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -140,7 +140,6 @@ export default { <style scoped> .sidebar-footer { - position: absolute; width: 100%; bottom: 0px; } diff --git a/frontend/src/components/feedback_labels/FeedbackLabel.vue b/frontend/src/components/feedback_labels/FeedbackLabel.vue index bf14cbe05e6bc29f1cf5880318d4d11cb1e2299b..1f21ee2cbb671b56fbd459c50c10ae14d78bf759 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabel.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabel.vue @@ -1,8 +1,10 @@ <template> <v-tooltip top> <v-chip - close + :close=removable + :color="colour" slot="activator" + @input="onClose" > {{ name }} </v-chip> <span> {{ description }} </span> </v-tooltip> @@ -15,8 +17,14 @@ import { Prop } from "vue-property-decorator" @Component export default class FeedbackLabel extends Vue { + @Prop({ type: Number, required :true }) readonly pk!: number @Prop({ type: String, required: true }) readonly name!: string @Prop({ type: String, required: true }) readonly description!: string + @Prop({ type: String, required: true }) readonly colour!: string + @Prop({ type: Boolean, default: false }) readonly removable!: boolean + onClose() { + this.$emit("remove-clicked", this.pk) + } } </script> diff --git a/frontend/src/components/feedback_labels/FeedbackLabelForm.vue b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..3cd91c4761f03ed83ab15429fdea53a8f69d251f --- /dev/null +++ b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue @@ -0,0 +1,128 @@ +<template> + <v-layout wrap justify-start> + <v-flex ml-3 xs9> + <v-text-field + label="Name" + v-model="name" + /> + </v-flex> + <v-flex ml-3 xs9> + <v-textarea + label="Description" + v-model="description" + placeholder="The description can be seen when hovering above the label" + auto-grow + /> + </v-flex> + <v-flex ml-2 xs12> + <compact-picker style="width:85%;box-shadow:none;" v-model="colour"/> + </v-flex> + <v-flex ml-1 mb-3 xs4> + <v-btn id="create-label-btn" v-if="!is_update" :loading="loading" color="teal" @click="createLabel">Create</v-btn> + <v-btn id="update-label-btn" v-else color="teal" :loading="loading" @click="updateLabel">Update</v-btn> + </v-flex> + </v-layout> +</template> + +<script lang="ts"> +import Vue from "vue" +import Component from "vue-class-component" +import { Prop } from "vue-property-decorator" +import * as api from "@/api"; +import { Compact } from "vue-color" +import { FeedbackLabels } from "@/store/modules/feedback-labels"; + +@Component({ + components: { + 'compact-picker': Compact, + } +}) +export default class FeedbackLabelForm extends Vue { + @Prop({ type: String, default: "" }) name!: string + @Prop({ type: String, default: "" }) description!: string + @Prop({ type: String, default: "#4d4d4d" }) colour!: string + @Prop({ type: Number, required: false }) readonly pk!: number + @Prop({ type: Boolean, default: false }) readonly is_update!: boolean + + loading = false + + get feedbackLabels () { + return FeedbackLabels.availableLabels + } + + async createLabel () { + this.loading = true + const label = { + name: this.name, + description: this.description, + // @ts-ignore + colour: this.colour.hex, + } + + const duplicate = this.feedbackLabels.find((val) => { + return val.name === label.name + }) + + if (duplicate) { + this.$notify({ + title: "Label creation error", + text: "A label with the same name already exists. " + + "You can update it if you are a reviewer or ask a reviewer to update it if not.", + type: 'error', + duration: -1 + }) + this.resetFields() + this.loading = false + return; + } + + let res + try { + res = await api.createLabel(label) + } catch (ex) { + // user will be notified by the interceptor + this.resetFields() + this.loading = false + return; + } + + FeedbackLabels.ADD_LABEL(res) + this.resetFields() + this.loading = false + } + + async updateLabel () { + this.loading = true + const label = { + pk: this.pk, + name: this.name, + description: this.description, + // @ts-ignore + colour: this.colour.hex, + } + + let res + try { + res = await api.updateLabel(label) + } catch (ex) { + // user will be notified by the interceptor + this.loading = false + return; + } + + FeedbackLabels.UPDATE_LABEL(label) + this.$emit("label-updated", label.pk) + this.loading = false + } + + resetFields () { + this.name = "" + this.description = "" + this.colour = "#4d4d4d" + } +} +</script> + +<style> + +</style> diff --git a/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue new file mode 100644 index 0000000000000000000000000000000000000000..4d6a8ddee67b296849d2b69b7162c59c5997c45d --- /dev/null +++ b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue @@ -0,0 +1,65 @@ +<template> + <v-layout wrap> + <v-flex mx-2 xs12> + <v-autocomplete + :items="feedbackLabels" + item-text="name" + item-value="pk" + append-icon="search" + placeholder="search for keywords" + @input="setLabel" + /> + </v-flex> + <v-flex xs12 v-if="label.pk !== -1"> + <feedback-label-form + is_update + :pk="label.pk" + :name="label.name" + :description="label.description" + :colour="label.colour" + @label-updated="setLabel" + /> + </v-flex> + </v-layout> +</template> + +<script lang="ts"> +import Vue from "vue" +import Component from "vue-class-component" +import { FeedbackLabels } from "@/store/modules/feedback-labels" +import FeedbackLabelForm from "./FeedbackLabelForm.vue" +import { FeedbackLabel } from '../../models'; + +@Component({ + components: { + FeedbackLabelForm, + } +}) +export default class FeedbackLabelUpdater extends Vue { + label: FeedbackLabel = { + pk: -1, + name: "", + description: "", + colour: "#4d4d4d", + } + loading = false + + get feedbackLabels() { + return FeedbackLabels.availableLabels + } + + setLabel (pk: number) { + const label = this.feedbackLabels.find((val: FeedbackLabel) => { + return val.pk === pk + }) + + if (label !== undefined) { + this.label = label + } + } +} +</script> + +<style> + +</style> diff --git a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue index be2413e2731f3f1e910469e9cd5210a7de6ba547..d4ea78176dabf9db91f963a79f2166e77648dedf 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue @@ -1,20 +1,65 @@ <template> - <div> - test test test - </div> + <v-card> + <v-toolbar color="teal" :dense="sidebar"> + <v-toolbar-side-icon> + <v-icon>chat_bubble</v-icon> + </v-toolbar-side-icon> + <v-toolbar-title v-if="showDetail" style="min-width: fit-content;"> + Labels + </v-toolbar-title> + <v-spacer /> + <v-btn icon @click="refreshLabels"> + <v-icon v-if="!updating">refresh</v-icon> + <v-progress-circular v-else indeterminate color="black" size="20" /> + </v-btn> + </v-toolbar> + <v-tabs grow color="teal lighten-1" v-if="showDetail"> + <v-tab>Create</v-tab> + <v-tab>Update</v-tab> + <v-tab-item> + <feedback-label-form/> + </v-tab-item> + <v-tab-item> + <feedback-label-updater/> + </v-tab-item> + </v-tabs> + </v-card> </template> <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' +import { Prop } from 'vue-property-decorator' import { getLabels } from '@/api' import { FeedbackLabels } from '@/store/modules/feedback-labels' +import { UI } from '@/store/modules/ui' +import FeedbackLabelForm from "./FeedbackLabelForm.vue" +import FeedbackLabelUpdater from "./FeedbackLabelUpdater.vue" -@Component +@Component({ + components: { + FeedbackLabelForm, + FeedbackLabelUpdater, + } +}) export default class FeedbackLabelsList extends Vue { + @Prop({type: Boolean, default: false}) sidebar!: boolean + + updating = false + + get showDetail () { + return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed) + } + async created() { + await this.refreshLabels() + } + + async refreshLabels() { + this.updating = true const labels = await getLabels() FeedbackLabels.SET_LABELS(labels) + this.updating = false } } </script> diff --git a/frontend/src/components/feedback_labels/LabelSelector.vue b/frontend/src/components/feedback_labels/LabelSelector.vue index 0bc6e3548fb173944bb59dd81437bb4a1c751bcd..d0725b339e1455e837f855af41c82e743235a886 100644 --- a/frontend/src/components/feedback_labels/LabelSelector.vue +++ b/frontend/src/components/feedback_labels/LabelSelector.vue @@ -10,11 +10,17 @@ item-value="pk" append-icon="search" placeholder="search for keywords" - @input="onChange" + @input="addLabel" /> </v-flex> <v-flex lg8> - + <feedback-label + removable + v-for="label in labelsToShow" + v-bind="label" + :key="label.pk" + @remove-clicked="removeLabel" + /> </v-flex> </v-layout> </v-card> @@ -23,21 +29,121 @@ <script lang="ts"> import Vue from "vue"; import Component from "vue-class-component"; +import { Prop } from "vue-property-decorator"; import { FeedbackLabels } from '@/store/modules/feedback-labels' -import { FeedbackLabel } from '@/models'; +import { SubmissionNotes } from '@/store/modules/submission-notes' +import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" +import { FeedbackComment } from '../../models'; -@Component +@Component({ + components: { + FeedbackLabel, + } +}) export default class LabelSelector extends Vue { + @Prop({ type: String }) readonly lineNo!: string + @Prop({ type: Boolean, required: true }) readonly assignedToFeedback!: boolean + @Prop({ type: Array }) readonly labelsDraft!: number[] get feedbackLabels() { return FeedbackLabels.availableLabels } - onChange(val: string) { - const selectedLabel = this.feedbackLabels.find((label) => { - return label.pk === val - }); - console.log(selectedLabel) + get labelsToShow() { + if (this.assignedToFeedback) { + return this.assignedFeedbackLabels() + } + return this.mapPksToLabelObj(this.labelsDraft) + } + + get labelsGetter () { + if (SubmissionNotes.state.changedLabels) { + return SubmissionNotes.state.updatedFeedback.labels + } + + // merge labels from originalFeedback and updatedFeedback + let merged: number[] = [] + const concated = SubmissionNotes.state.origFeedback.labels.concat( + SubmissionNotes.state.updatedFeedback.labels + ) + + concated.forEach((val) => { + if (!(SubmissionNotes.state.origFeedback.labels.includes(val) && + SubmissionNotes.state.updatedFeedback.labels.includes(val))) { + merged.push(val) + } + }) + + return merged + } + + /** + * Returns an array of labels assigned to the currently loaded feedback + */ + assignedFeedbackLabels() { + const labels = this.labelsGetter + + if (labels.length === 0) return {} + const mapped = this.mapPksToLabelObj(labels) + return mapped ? mapped : {} + } + + /** + * Maps label pk's to the objects stored in vuex store + */ + mapPksToLabelObj(pkArr: number[]): FeedbackLabel[] { + const mappedLabels = pkArr.map((val) => { + const label = FeedbackLabels.availableLabels.find((label) => { + return label.pk === val + }) + + if (!label) return + return { + pk: val, + name: label.name, + description: label.description, + colour: label.colour + } + }) + + return mappedLabels ? mappedLabels : new Array() + } + + /** + * Removes given label from the feedback or fires an event + * to remove the label from a comment + */ + removeLabel(pk: number) { + if (this.assignedToFeedback) { + const labels = this.labelsGetter.filter((val) => { + return val !== pk + }) + SubmissionNotes.SET_FEEDBACK_LABELS(labels) + } + this.$emit("label-removed", pk) + } + + /** + * Adds the given label to the feedback or fires an event + * to add the label to a comment + * Calling this with an already added label will instead remove the label + */ + addLabel(pk: number) { + // there seems to be an issue with the autocomplete + // which fires when a user hits backspace + if (pk == undefined) return + if (this.assignedToFeedback) { + let labels = this.labelsGetter + + if (!labels.includes(pk)) { + labels.push(pk) + SubmissionNotes.SET_FEEDBACK_LABELS(labels) + } else { + this.removeLabel(pk) + } + } else { + this.$emit("label-added", pk) + } } } </script> diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index d84324ebd76a165270a23a2c14f0534910d307db..58b9b8f55bf4f6083afe699eb40819a47db5f80f 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -42,8 +42,9 @@ </submission-line> </tr> </template> - <annotated-submission-labels - class="elevation-1" + <label-selector + :assignedToFeedback="true" + class="mt-1 elevation-1" slot="labels" /> <annotated-submission-bottom-toolbar @@ -65,8 +66,10 @@ import CommentForm from '@/components/submission_notes/base/CommentForm.vue' import FeedbackComment from '@/components/submission_notes/base/FeedbackComment.vue' import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar' import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar' -import AnnotatedSubmissionLabels from '@/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue' import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission' +import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" +import { FeedbackLabels as Labels } from '@/store/modules/feedback-labels' +import LabelSelector from '@/components/feedback_labels/LabelSelector.vue' import SubmissionLine from '@/components/submission_notes/base/SubmissionLine' import { SubmissionNotes } from '@/store/modules/submission-notes' import { Authentication } from '@/store/modules/authentication' @@ -79,8 +82,8 @@ export default { BaseAnnotatedSubmission, AnnotatedSubmissionBottomToolbar, AnnotatedSubmissionTopToolbar, - AnnotatedSubmissionLabels, FeedbackComment, + LabelSelector, CommentForm }, name: 'submission-correction', data () { diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue index 33628127237ca3aadd9e0af88de75dc48e4b61aa..9b27fc398b4d79db8ba3de4bb118fbf56b1774d2 100644 --- a/frontend/src/components/submission_notes/base/CommentForm.vue +++ b/frontend/src/components/submission_notes/base/CommentForm.vue @@ -16,7 +16,13 @@ /> </v-flex> <v-flex lg10 md8 sm8 my-2> - <label-selector/> + <label-selector + :assignedToFeedback="false" + :lineNo="this.lineNo" + :labelsDraft="this.labelsDraft" + @label-added="labelAdded" + @label-removed="labelRemoved" + /> </v-flex> <v-flex lg2 ma-0> <v-btn id="submit-comment" color="success" @click="submitFeedback"><v-icon>check</v-icon>Submit</v-btn> @@ -31,6 +37,7 @@ import Component from 'vue-class-component' import { Prop, Watch } from 'vue-property-decorator' import { SubmissionNotes } from '@/store/modules/submission-notes' import LabelSelector from "@/components/feedback_labels/LabelSelector.vue" +import { FeedbackComment } from '@/models'; @Component({ components: { @@ -42,6 +49,7 @@ export default class CommentForm extends Vue { @Prop({ type: String, required: true }) readonly lineNo!: string currentFeedback = this.feedback + labelsDraft: number[] = this.copyExistingLabels() selectInput (event: Event) { if (event !== null) { @@ -50,15 +58,46 @@ export default class CommentForm extends Vue { } } + copyExistingLabels(): number[] { + const linesOrig = SubmissionNotes.state.origFeedback.feedbackLines + const linesUpdated = SubmissionNotes.state.updatedFeedback.feedbackLines + + // priority for updatedFeedback and always select last created comment + if (linesUpdated && Object.keys(linesUpdated).length > 0) { + let line = <Partial<FeedbackComment>> linesUpdated[Number(this.lineNo)] + if (line.labels) return line.labels + } else if (linesOrig && Object.keys(linesOrig).length > 0) { + let lines = linesOrig[Number(this.lineNo)] + return lines[lines.length - 1].labels + } + + return new Array() + } + collapseTextField () { this.$emit('collapseFeedbackForm') } + + labelAdded (pk: number) { + if (this.labelsDraft.includes(pk)) { + this.labelRemoved(pk) + } else { + this.labelsDraft.push(pk) + } + } + + labelRemoved (pk: number) { + this.labelsDraft = this.labelsDraft.filter((val) => { + return val !== pk + }) + } - submitFeedback () { + submitFeedback (labelPk?: string) { SubmissionNotes.UPDATE_FEEDBACK_LINE({ lineNo: Number(this.lineNo), comment: { - text: this.currentFeedback + text: this.currentFeedback, + labels: this.labelsDraft, } }) this.collapseTextField() diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue index 9ef2ca7cf826be5fb6adb2991d272f0098aca4fb..a8cb74520d67c7dedd1d22a5d9c5d4a2e8d5bec7 100644 --- a/frontend/src/components/submission_notes/base/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue @@ -32,6 +32,15 @@ <v-icon v-else size="20px">restore</v-icon> </v-btn> </div> + <v-layout> + <v-flex> + <feedback-label + v-for="label in labelsToShow" + v-bind="label" + :key="label.pk" + /> + </v-flex> + </v-layout> </div> </template> @@ -39,9 +48,17 @@ import { mapState } from 'vuex' import { UI } from '@/store/modules/ui' import { SubmissionNotes } from '@/store/modules/submission-notes' +import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" +import { FeedbackLabels as Labels } from '@/store/modules/feedback-labels' + +// TODO: allow for displaying of empty comments when they have labels assigned +// TODO: also make labels directly removable export default { name: 'feedback-comment', + components: { + FeedbackLabel, + }, props: { pk: { type: String, @@ -74,7 +91,11 @@ export default { showVisibilityIcon: { type: Boolean, default: true - } + }, + labels: { + type: Array, + required: true, + }, }, computed: { markedForDeletion () { return SubmissionNotes.state.commentsMarkedForDeletion }, @@ -93,7 +114,23 @@ export default { }, backgroundColor () { return UI.state.darkMode ? 'grey' : '#F3F3F3' - } + }, + labelsToShow () { + const mappedLabels = this.labels.map((val) => { + const label = Labels.availableLabels.find((label) => { + return label.pk === val + }) + + if (!label) return new Array() + return { + pk: val, + name: label.name, + description: label.description, + colour: label.colour + } + }) + return mappedLabels ? mappedLabels : new Array() + }, }, methods: { toggleDeleteComment () { diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue deleted file mode 100644 index 57c9647c11bd7c245dd9ca71c4695f23adbc6f10..0000000000000000000000000000000000000000 --- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> - <v-container> - <v-layout wrap> - <v-flex> - <feedback-label - v-for="(label, index) in labels" - v-bind="label" - :key="index" - /> - </v-flex> - </v-layout> - </v-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Component from 'vue-class-component' -import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" -import { SubmissionNotes } from '@/store/modules/submission-notes' -import { FeedbackLabels as Labels } from '@/store/modules/feedback-labels' - -@Component({ - components: { - FeedbackLabel - } -}) -export default class AnnotatedSubmissionLabels extends Vue { - // maps origFeedback's label pk's to the ones stored in vuex - get labels() { - const labels = SubmissionNotes.state.origFeedback.labels - if (labels) { - const mappedLabels = labels.map((val) => { - const label = Labels.availableLabels.find((label) => { - return Number(label.pk) === val - }) - - if (!label) return // TODO: throw error - return { pk: val, name: label.name, description: label.description } - }) - - return mappedLabels - } - - return {} - } -} -</script> - diff --git a/frontend/src/models.ts b/frontend/src/models.ts index ae98cc96b3077d6c105d3f7c0f78151c6d8ddcc4..6186106764718d559b974315ffe6aaf09bcc0b8e 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -139,7 +139,7 @@ export interface Feedback { * @memberof Feedback */ feedbackStageForUser?: string, - labels?: number[], + labels: number[], } /** @@ -184,7 +184,7 @@ export interface FeedbackComment { * @memberof FeedbackComment */ visibleToStudent?: boolean - labels?: FeedbackLabel[] + labels: number[] } /** @@ -193,10 +193,10 @@ export interface FeedbackComment { * @interface FeedbackLabel */ export interface FeedbackLabel { - pk: string + pk: number name: string description: string - color: string + colour: string } /** diff --git a/frontend/src/store/modules/feedback-labels.ts b/frontend/src/store/modules/feedback-labels.ts index 2056979cdba61f38b0115869455f35efdd2f130c..cc4a95257a116b277ab4ec87e8ffd8085cc5996f 100644 --- a/frontend/src/store/modules/feedback-labels.ts +++ b/frontend/src/store/modules/feedback-labels.ts @@ -30,7 +30,14 @@ function ADD_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { } function REMOVE_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { - + state.labels = state.labels.filter((val) => { + return val.pk !== label.pk + }) +} + +function UPDATE_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { + REMOVE_LABEL(state, label) + ADD_LABEL(state, label) } export const FeedbackLabels = { @@ -39,6 +46,7 @@ export const FeedbackLabels = { SET_LABELS: mb.commit(SET_LABELS), ADD_LABEL: mb.commit(ADD_LABEL), - REMOVE_LABEL: mb.commit(REMOVE_LABEL) + REMOVE_LABEL: mb.commit(REMOVE_LABEL), + UPDATE_LABEL: mb.commit(UPDATE_LABEL), } diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts index e7cd1e7089538282a0c68b4e09f37316dd0f5bd2..51c8f6f1c6787aa01106b2bb783267227b0e68fa 100644 --- a/frontend/src/store/modules/submission-notes.ts +++ b/frontend/src/store/modules/submission-notes.ts @@ -18,6 +18,7 @@ export interface SubmissionNotesState { origFeedback: Feedback updatedFeedback: Feedback commentsMarkedForDeletion: { [pk: string]: FeedbackComment } + changedLabels: boolean } function initialState(): SubmissionNotesState { @@ -47,7 +48,8 @@ function initialState(): SubmissionNotesState { feedbackLines: {}, labels: [], }, - commentsMarkedForDeletion: {} + commentsMarkedForDeletion: {}, + changedLabels: false } } @@ -80,7 +82,7 @@ const scoreGetter = mb.read(function score(state) { const workInProgressGetter = mb.read(function workInProgress(state) { const openEditor = Object.values(state.ui.showEditorOnLine).reduce((acc, curr) => acc || curr, false) const feedbackWritten = Object.entries(state.updatedFeedback.feedbackLines || {}).length > 0 - return openEditor || feedbackWritten + return openEditor || feedbackWritten ||Â state.changedLabels }) const isFeedbackCreationGetter = mb.read(function isFeedbackCreation(state) { return !state.origFeedback['feedbackStageForUser'] || @@ -94,6 +96,7 @@ function SET_ORIG_FEEDBACK(state: SubmissionNotesState, feedback: Feedback) { if (feedback) { state.origFeedback = feedback state.hasOrigFeedback = true + //state.updatedFeedback.labels = feedback.labels } } function SET_SHOW_FEEDBACK(state: SubmissionNotesState, val: boolean) { @@ -105,6 +108,10 @@ function UPDATE_FEEDBACK_LINE(state: SubmissionNotesState, feedback: { lineNo: n Vue.set(state.updatedFeedback.feedbackLines, feedback.lineNo.toString(), feedback.comment) } } +function SET_FEEDBACK_LABELS(state: SubmissionNotesState, labels: number[]) { + state.changedLabels = true + state.updatedFeedback.labels = labels +} function UPDATE_FEEDBACK_SCORE(state: SubmissionNotesState, score: number) { state.updatedFeedback.score = score } @@ -143,13 +150,13 @@ async function deleteComments({ state }: BareActionContext<SubmissionNotesState, } async function submitFeedback( { state }: BareActionContext<SubmissionNotesState, RootState>, -{ isFinal = false, labels = [] }): +{ isFinal = false}): Promise<AxiosResponse<void>[]> { let feedback: Partial<Feedback> = { isFinal: isFinal, ofSubmission: state.submission.pk, - labels: labels + labels: state.updatedFeedback.labels } if (state.origFeedback.score === undefined && state.updatedFeedback.score === undefined) { throw new Error('You need to give a score.') @@ -185,6 +192,7 @@ export const SubmissionNotes = { SET_SUBMISSION: mb.commit(SET_SUBMISSION), SET_ORIG_FEEDBACK: mb.commit(SET_ORIG_FEEDBACK), SET_SHOW_FEEDBACK: mb.commit(SET_SHOW_FEEDBACK), + SET_FEEDBACK_LABELS: mb.commit(SET_FEEDBACK_LABELS), UPDATE_FEEDBACK_LINE: mb.commit(UPDATE_FEEDBACK_LINE), UPDATE_FEEDBACK_SCORE: mb.commit(UPDATE_FEEDBACK_SCORE), DELETE_FEEDBACK_LINE: mb.commit(DELETE_FEEDBACK_LINE), diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a8cf2ddead1be9878c724525637bd0c472c39dfe..dc391a2453ff7cb2e473d8c468e24f711190be98 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1412,6 +1412,11 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== +clamp@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634" + integrity sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ= + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -4517,6 +4522,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.throttle@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.transform@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0" @@ -4605,6 +4615,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +material-colors@^1.0.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + math-random@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" @@ -7285,6 +7300,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tinycolor2@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7689,6 +7709,16 @@ vue-class-component@^6.0.0, vue-class-component@^6.2.0: resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-6.3.2.tgz#e6037e84d1df2af3bde4f455e50ca1b9eec02be6" integrity sha512-cH208IoM+jgZyEf/g7mnFyofwPDJTM/QvBNhYMjqGB8fCsRyTf68rH2ISw/G20tJv+5mIThQ3upKwoL4jLTr1A== +vue-color@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/vue-color/-/vue-color-2.7.0.tgz#31e898370a5786fd6c3007388cb7242db6ba6988" + integrity sha512-fak9oPRL3BsYtakTGmWIS2yNRppRYNlMgGGq78CMH34ipU8fLgi/bT9JiSPcscpdTNLGracuOFuZ8OFeml+SQQ== + dependencies: + clamp "^1.0.1" + lodash.throttle "^4.0.0" + material-colors "^1.0.0" + tinycolor2 "^1.1.2" + vue-eslint-parser@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" @@ -7742,6 +7772,11 @@ vue-style-loader@^4.1.0: hash-sum "^1.0.2" loader-utils "^1.0.2" +vue-swatches@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-1.0.3.tgz#84eb23ba99bbbb0d56698f3a8bdb1b340203d33b" + integrity sha512-3J+Nc3bisvhhp0BW0pfTbQvdl3i+dhwoPjoM+2D6R6hW65KNpXOA+sJwcSg2j1Xd6fLcOV7LMb2sz6s4vKSWRg== + vue-template-compiler@^2.5.16: version "2.5.22" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.22.tgz#c3d3c02c65f1908205c4fbd3b0ef579e51239955"