From 4854b8f4c8a359767da1a78b8f9285e482c02397 Mon Sep 17 00:00:00 2001 From: Dominik Seeger <dominik.seeger@gmx.net> Date: Tue, 4 Jun 2019 16:57:44 +0200 Subject: [PATCH] improved labelling system style and feel --- core/migrations/0016_auto_20190521_1803.py | 18 ++ core/models/feedback.py | 2 +- .../feedback_labels/FeedbackLabel.vue | 1 + .../feedback_labels/FeedbackLabelForm.vue | 57 +++--- .../feedback_labels/FeedbackLabelUpdater.vue | 7 +- .../feedback_labels/FeedbackLabelsList.vue | 2 +- .../feedback_labels/LabelSelector.vue | 163 ++++++++++++------ .../components/mixins/commentLabelSelector.ts | 98 +++++++++++ .../submission_notes/base/CommentForm.vue | 69 ++++---- .../submission_notes/base/FeedbackComment.vue | 94 ++++++++-- frontend/src/models.ts | 1 + .../src/store/modules/submission-notes.ts | 14 +- 12 files changed, 400 insertions(+), 126 deletions(-) create mode 100644 core/migrations/0016_auto_20190521_1803.py create mode 100644 frontend/src/components/mixins/commentLabelSelector.ts diff --git a/core/migrations/0016_auto_20190521_1803.py b/core/migrations/0016_auto_20190521_1803.py new file mode 100644 index 00000000..93477a02 --- /dev/null +++ b/core/migrations/0016_auto_20190521_1803.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-05-21 18:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_feedbacklabel_colour'), + ] + + operations = [ + migrations.AlterField( + model_name='feedbackcomment', + name='text', + field=models.TextField(blank=True), + ), + ] diff --git a/core/models/feedback.py b/core/models/feedback.py index fb933be3..15eea22c 100644 --- a/core/models/feedback.py +++ b/core/models/feedback.py @@ -77,7 +77,7 @@ class FeedbackComment(models.Model): comment_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - text = models.TextField() + text = models.TextField(blank=True) created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) diff --git a/frontend/src/components/feedback_labels/FeedbackLabel.vue b/frontend/src/components/feedback_labels/FeedbackLabel.vue index 1f21ee2c..bd97e85f 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabel.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabel.vue @@ -22,6 +22,7 @@ export default class FeedbackLabel extends Vue { @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) } diff --git a/frontend/src/components/feedback_labels/FeedbackLabelForm.vue b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue index 3cd91c47..23796428 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelForm.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue @@ -3,19 +3,19 @@ <v-flex ml-3 xs9> <v-text-field label="Name" - v-model="name" + v-model="mutableName" /> </v-flex> <v-flex ml-3 xs9> <v-textarea label="Description" - v-model="description" + v-model="mutableDescription" 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"/> + <compact-picker style="width:85%;box-shadow:none;" v-model="mutableColour"/> </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> @@ -27,7 +27,7 @@ <script lang="ts"> import Vue from "vue" import Component from "vue-class-component" -import { Prop } from "vue-property-decorator" +import { Prop, Watch } from "vue-property-decorator" import * as api from "@/api"; import { Compact } from "vue-color" import { FeedbackLabels } from "@/store/modules/feedback-labels"; @@ -38,29 +38,45 @@ import { FeedbackLabels } from "@/store/modules/feedback-labels"; } }) 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: String, default: "" }) readonly name!: string + @Prop({ type: String, default: "" }) readonly description!: string + @Prop({ type: String, default: "#4d4d4d" }) readonly colour!: string @Prop({ type: Number, required: false }) readonly pk!: number @Prop({ type: Boolean, default: false }) readonly is_update!: boolean + mutableColour = this.colour + mutableName = this.name + mutableDescription = this.description + loading = false + @Watch('pk') + onPkChange() { this.resetFields() } + + resetFields () { + this.mutableName = this.name + this.mutableDescription = this.description + this.mutableColour = this.colour + } + + get label () { + return { + name: this.mutableName, + description: this.mutableDescription, + // @ts-ignore + colour: this.mutableColour.hex || this.mutableColour // hex may be undefined when colour comes from the updater + } + } + 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 + return val.name === this.label.name }) if (duplicate) { @@ -78,7 +94,7 @@ export default class FeedbackLabelForm extends Vue { let res try { - res = await api.createLabel(label) + res = await api.createLabel(this.label) } catch (ex) { // user will be notified by the interceptor this.resetFields() @@ -94,11 +110,8 @@ export default class FeedbackLabelForm extends Vue { async updateLabel () { this.loading = true const label = { + ...this.label, pk: this.pk, - name: this.name, - description: this.description, - // @ts-ignore - colour: this.colour.hex, } let res @@ -114,12 +127,6 @@ export default class FeedbackLabelForm extends Vue { this.$emit("label-updated", label.pk) this.loading = false } - - resetFields () { - this.name = "" - this.description = "" - this.colour = "#4d4d4d" - } } </script> diff --git a/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue index 4d6a8dde..7b02cc51 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue @@ -13,10 +13,7 @@ <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" + v-bind="currentLabel" @label-updated="setLabel" /> </v-flex> @@ -44,6 +41,8 @@ export default class FeedbackLabelUpdater extends Vue { } loading = false + get currentLabel () { return this.label } + get feedbackLabels() { return FeedbackLabels.availableLabels } diff --git a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue index d4ea7817..db30f7e3 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue @@ -2,7 +2,7 @@ <v-card> <v-toolbar color="teal" :dense="sidebar"> <v-toolbar-side-icon> - <v-icon>chat_bubble</v-icon> + <v-icon>label</v-icon> </v-toolbar-side-icon> <v-toolbar-title v-if="showDetail" style="min-width: fit-content;"> Labels diff --git a/frontend/src/components/feedback_labels/LabelSelector.vue b/frontend/src/components/feedback_labels/LabelSelector.vue index d0725b33..1c4f6bb2 100644 --- a/frontend/src/components/feedback_labels/LabelSelector.vue +++ b/frontend/src/components/feedback_labels/LabelSelector.vue @@ -2,8 +2,8 @@ <v-card> <v-card-title>Assign labels</v-card-title> <v-divider/> - <v-layout> - <v-flex ml-2 lg5> + <v-layout wrap> + <v-flex ml-2 sm10> <v-autocomplete :items="feedbackLabels" item-text="name" @@ -13,15 +13,44 @@ @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 ml-2 mb-3> + <v-flex sm4> + <v-flex sm12> + UNCHANGED + </v-flex> + <feedback-label + removable + v-for="label in unchangedMapped" + v-bind="label" + :key="label.pk" + @remove-clicked="removeLabel" + /> + </v-flex> + <v-flex sm4> + <v-flex sm12> + WILL BE REMOVED + </v-flex> + <feedback-label + removable + v-for="label in removedMapped" + v-bind="label" + :key="label.pk" + @remove-clicked="addLabel" + /> + </v-flex> + <v-flex sm4> + <v-flex sm12> + WILL BE ADDED + </v-flex> + <feedback-label + removable + v-for="label in addedMapped" + v-bind="label" + :key="label.pk" + @remove-clicked="removeLabel" + /> + </v-flex> + </v-layout> </v-layout> </v-card> </template> @@ -33,7 +62,7 @@ import { Prop } from "vue-property-decorator"; import { FeedbackLabels } from '@/store/modules/feedback-labels' import { SubmissionNotes } from '@/store/modules/submission-notes' import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" -import { FeedbackComment } from '../../models'; +import { FeedbackComment, SubmissionType } from '../../models'; @Component({ components: { @@ -43,49 +72,83 @@ import { FeedbackComment } from '../../models'; 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[] + @Prop({ type: Array }) readonly labelsUnchanged!: number[] + @Prop({ type: Array }) readonly labelsAdded!: number[] + @Prop({ type: Array }) readonly labelsRemoved!: number[] - get feedbackLabels() { + get feedbackLabels () { return FeedbackLabels.availableLabels } - get labelsToShow() { + get unchangedMapped() { if (this.assignedToFeedback) { - return this.assignedFeedbackLabels() + return this.mapPksToLabelObj(this.unchangedFeedbackLabels()) + } else { + return this.mapPksToLabelObj(this.labelsUnchanged) } - return this.mapPksToLabelObj(this.labelsDraft) } - get labelsGetter () { - if (SubmissionNotes.state.changedLabels) { - return SubmissionNotes.state.updatedFeedback.labels + get removedMapped() { + if (this.assignedToFeedback) { + return this.mapPksToLabelObj(this.removedFeedbackLabels()) + } else { + return this.mapPksToLabelObj(this.labelsRemoved) } + } - // merge labels from originalFeedback and updatedFeedback - let merged: number[] = [] - const concated = SubmissionNotes.state.origFeedback.labels.concat( - SubmissionNotes.state.updatedFeedback.labels - ) + get addedMapped() { + if (this.assignedToFeedback) { + return this.mapPksToLabelObj(this.addedFeedbackLabels()) + } else { + return this.mapPksToLabelObj(this.labelsAdded) + } + } - concated.forEach((val) => { - if (!(SubmissionNotes.state.origFeedback.labels.includes(val) && - SubmissionNotes.state.updatedFeedback.labels.includes(val))) { - merged.push(val) - } + /** + * Returns an array of label pk's that have not changed from origFeedback to updatedFeedback + */ + unchangedFeedbackLabels() { + const labelsOrig = SubmissionNotes.state.origFeedback.labels + if (labelsOrig === undefined) return new Array() + + const labelsDeleted = this.removedFeedbackLabels() + const labelsAdded = this.addedFeedbackLabels() + + return labelsOrig.filter((label) => { + return !labelsAdded.includes(label) && !labelsDeleted.includes(label) }) + } - return merged + /** + * Returns an array of label pk's that have been removed in updatedFeedback + * but exist in origFeedback + */ + removedFeedbackLabels() { + if (!SubmissionNotes.state.changedLabels) return new Array() + + const labelsOrig = SubmissionNotes.state.origFeedback.labels + const labelsUpdated = SubmissionNotes.state.updatedFeedback.labels + + if (labelsOrig === undefined) return new Array() + + return labelsOrig.filter((label) => { + return !labelsUpdated.includes(label) + }) } /** - * Returns an array of labels assigned to the currently loaded feedback + * Returns an array of label pk's that have been added in updatedFeedback + * but do not exist in origFeedback */ - assignedFeedbackLabels() { - const labels = this.labelsGetter + addedFeedbackLabels()Â { + const labelsOrig = SubmissionNotes.state.origFeedback.labels + const labelsUpdated = SubmissionNotes.state.updatedFeedback.labels - if (labels.length === 0) return {} - const mapped = this.mapPksToLabelObj(labels) - return mapped ? mapped : {} + if (labelsOrig === undefined) return new Array() + + return labelsUpdated.filter((label) => { + return !labelsOrig.includes(label) + }) } /** @@ -115,12 +178,14 @@ export default class LabelSelector extends Vue { */ removeLabel(pk: number) { if (this.assignedToFeedback) { - const labels = this.labelsGetter.filter((val) => { - return val !== pk - }) - SubmissionNotes.SET_FEEDBACK_LABELS(labels) + if (!SubmissionNotes.state.changedLabels) { + SubmissionNotes.SET_FEEDBACK_LABELS(SubmissionNotes.state.origFeedback.labels) + } + + SubmissionNotes.REMOVE_FEEDBACK_LABEL(pk) + } else { + this.$emit("label-removed", pk) } - this.$emit("label-removed", pk) } /** @@ -129,17 +194,11 @@ export default class LabelSelector extends Vue { * 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) + if (!this.unchangedFeedbackLabels().includes(pk) && + !this.addedFeedbackLabels().includes(pk)) + { + SubmissionNotes.ADD_FEEDBACK_LABEL(pk) } } else { this.$emit("label-added", pk) diff --git a/frontend/src/components/mixins/commentLabelSelector.ts b/frontend/src/components/mixins/commentLabelSelector.ts new file mode 100644 index 00000000..4076ac3d --- /dev/null +++ b/frontend/src/components/mixins/commentLabelSelector.ts @@ -0,0 +1,98 @@ +import Vue from 'vue' +import Component from 'vue-class-component' +import { Prop } from "vue-property-decorator" +import { SubmissionNotes } from "@/store/modules/submission-notes" +import { FeedbackComment, FeedbackLabel } from "@/models" +import { FeedbackLabels } from "@/store/modules/feedback-labels" + +@Component +export default class commentLabelSelector extends Vue { + @Prop({ type: String, required: true }) readonly lineNo!: string + + /** + * Returns array of label pk's where feedbackType is + * either "origFeedback" or "updatedFeedback" + */ + copyStateLabels(feedbackType: string): number[] { + if (feedbackType !== "origFeedback" && feedbackType !== "updatedFeedback") return new Array() + + const currentLine = this.getFeedbackLine(feedbackType) + return currentLine ? currentLine.labels : new Array() + } + + getFeedbackLine (feedbackType: string): FeedbackComment |Â undefined { + if (feedbackType !== "origFeedback" && feedbackType !== "updatedFeedback") return undefined + + const stateLines = SubmissionNotes.state[feedbackType].feedbackLines + if (stateLines && Object.keys(stateLines).length > 0) { + let lines = stateLines[Number(this.lineNo)] + if (lines === undefined) return undefined + + // always copy latest comment + if (lines.length > 0) { + return lines[lines.length-1] + } else { + // @ts-ignore + return lines + } + } + } + + getUnchangedLabels() { + const labelsOrig = this.copyStateLabels("origFeedback") + if (labelsOrig === undefined) return new Array() + + const removedLabels = this.getRemovedLabels() + const addedLabels = this.getAddedLabels() + + return labelsOrig.filter((val) => { + return !removedLabels.includes(val) && !addedLabels.includes(val) + }) + } + + getRemovedLabels() { + const currentLine = this.getFeedbackLine("updatedFeedback") + if (currentLine === undefined) return new Array() + + const labelsOrig = this.copyStateLabels("origFeedback") + const labelsUpdated = this.copyStateLabels("updatedFeedback") + + if (labelsOrig == undefined) return new Array() + + return labelsOrig.filter((val) => { + return !labelsUpdated.includes(val) + }) + } + + getAddedLabels() { + const labelsOrig = this.copyStateLabels("origFeedback") + const labelsUpdated = this.copyStateLabels("updatedFeedback") + + if (labelsOrig === undefined) return new Array() + + return labelsUpdated.filter((val) => { + return !labelsOrig.includes(val) + }) + } + + /** + * 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() + } +} \ No newline at end of file diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue index 9b27fc39..f768e78d 100644 --- a/frontend/src/components/submission_notes/base/CommentForm.vue +++ b/frontend/src/components/submission_notes/base/CommentForm.vue @@ -19,7 +19,9 @@ <label-selector :assignedToFeedback="false" :lineNo="this.lineNo" - :labelsDraft="this.labelsDraft" + :labelsUnchanged="labelsUnchanged" + :labelsAdded="labelsAdded" + :labelsRemoved="labelsRemoved" @label-added="labelAdded" @label-removed="labelRemoved" /> @@ -33,23 +35,26 @@ <script lang="ts"> import Vue from 'vue' -import Component from 'vue-class-component' +import Component, { mixins } 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'; +import { FeedbackComment, SubmissionType } from '@/models'; +import commentLabelSelector from "@/components/mixins/commentLabelSelector" @Component({ components: { LabelSelector } }) -export default class CommentForm extends Vue { +export default class CommentForm extends mixins(commentLabelSelector) { @Prop({ type: String, default: '' }) readonly feedback!: string @Prop({ type: String, required: true }) readonly lineNo!: string currentFeedback = this.feedback - labelsDraft: number[] = this.copyExistingLabels() + labelsUnchanged: number[] = this.getUnchangedLabels() + labelsAdded: number[] = this.getAddedLabels() + labelsRemoved: number[] = this.getRemovedLabels() selectInput (event: Event) { if (event !== null) { @@ -58,46 +63,50 @@ 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') } + /** + * Adds label pk to the array of added labels + * or adds already removed labels to unchanged array + */ labelAdded (pk: number) { - if (this.labelsDraft.includes(pk)) { - this.labelRemoved(pk) - } else { - this.labelsDraft.push(pk) + if (this.labelsRemoved.includes(pk)) { + this.labelsUnchanged.push(pk) + this.labelsRemoved = this.labelsRemoved.filter((val) => { + return val !== pk + }) + } else if (!this.labelsAdded.includes(pk) && + !this.labelsUnchanged.includes(pk)) + { + this.labelsAdded.push(pk) } } + /** + * Adds label pk to the array of removed labels + * or removes already added labels from the list of added labels + */ labelRemoved (pk: number) { - this.labelsDraft = this.labelsDraft.filter((val) => { - return val !== pk - }) + if (this.labelsAdded.includes(pk)) { + this.labelsAdded = this.labelsAdded.filter((val) => { + return val !== pk + }) + } else if (!this.labelsRemoved.includes(pk)) { + this.labelsRemoved.push(pk) + this.labelsUnchanged = this.labelsUnchanged.filter((val) => { + return val !== pk + }) + } } - submitFeedback (labelPk?: string) { + submitFeedback () { SubmissionNotes.UPDATE_FEEDBACK_LINE({ lineNo: Number(this.lineNo), comment: { text: this.currentFeedback, - labels: this.labelsDraft, + labels: this.labelsUnchanged.concat(this.labelsAdded), } }) this.collapseTextField() diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue index a8cb7452..8a497083 100644 --- a/frontend/src/components/submission_notes/base/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue @@ -1,6 +1,6 @@ <template> <div class="dialog-box"> - <div class="body elevation-1" :style="{borderColor: borderColor, backgroundColor}"> + <div v-if="commentDisplayable" class="body elevation-1" :style="{borderColor: borderColor, backgroundColor}"> <span class="tip tip-up" :style="{borderBottomColor: borderColor}"></span> <span v-if="ofTutor" class="of-tutor">Of tutor: {{ofTutor}}</span> <span class="comment-created">{{parsedCreated}}</span> @@ -32,12 +32,41 @@ <v-icon v-else size="20px">restore</v-icon> </v-btn> </div> - <v-layout> - <v-flex> + <v-layout v-if="showLabels" ml-2> + <v-flex sm4> + <v-flex sm12> + UNCHANGED + </v-flex> <feedback-label - v-for="label in labelsToShow" + removable + v-for="label in unchangedLabels" v-bind="label" :key="label.pk" + @remove-clicked="deleteAction" + /> + </v-flex> + <v-flex sm4> + <v-flex sm12> + WILL BE REMOVED + </v-flex> + <feedback-label + removable + v-for="label in removedLabels" + v-bind="label" + :key="label.pk" + @remove-clicked="deleteAction" + /> + </v-flex> + <v-flex sm4> + <v-flex sm12> + WILL BE ADDED + </v-flex> + <feedback-label + removable + v-for="label in addedLabels" + v-bind="label" + :key="label.pk" + @remove-clicked="deleteAction" /> </v-flex> </v-layout> @@ -50,15 +79,16 @@ 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 +import commentLabelSelector from "@/components/mixins/commentLabelSelector" export default { name: 'feedback-comment', components: { FeedbackLabel, }, + mixins: [ + commentLabelSelector, + ], props: { pk: { type: String, @@ -91,13 +121,16 @@ export default { showVisibilityIcon: { type: Boolean, default: true - }, - labels: { - type: Array, - required: true, - }, + } }, computed: { + commentDisplayable () { return this.text !== "" }, + showLabels () { + return this.visibleToStudent && + (this.getUnchangedLabels().length > 0 || + this.getAddedLabels().length > 0 || + this.getRemovedLabels().length > 0) + }, markedForDeletion () { return SubmissionNotes.state.commentsMarkedForDeletion }, parsedCreated () { if (this.created) { @@ -131,8 +164,45 @@ export default { }) return mappedLabels ? mappedLabels : new Array() }, + unchangedLabels() { + return this.mapPksToLabelObj(this.getUnchangedLabels()) + }, + addedLabels() { + return this.mapPksToLabelObj(this.getAddedLabels()) + }, + removedLabels() { + return this.mapPksToLabelObj(this.getRemovedLabels()) + } }, methods: { + deleteAction (pk) { + let labels + const concated = this.getUnchangedLabels().concat(this.getAddedLabels()) + if (this.getUnchangedLabels().includes(pk)) { + labels = concated.filter((val) => { + return val !== pk + }) + } else if (this.getAddedLabels().includes(pk)) { + labels = concated.filter((val) => { + return val !== pk + }) + } else if (this.getRemovedLabels().includes(pk)) { + concated.push(pk) + labels = concated + } + + if (labels.length > 0 || SubmissionNotes.state.hasOrigFeedback || this.commentDisplayable) { + SubmissionNotes.UPDATE_FEEDBACK_LINE({ + lineNo: Number(this.lineNo), + comment: { + text: this.text ||Â "", + labels: labels, + } + }) + } else { + SubmissionNotes.DELETE_FEEDBACK_LINE(Number(this.lineNo)) + } + }, toggleDeleteComment () { if (this.pk) { if (!this.markedForDeletion.hasOwnProperty(this.pk)) { diff --git a/frontend/src/models.ts b/frontend/src/models.ts index 302089d3..b24847d8 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -185,6 +185,7 @@ export interface FeedbackComment { */ visibleToStudent?: boolean labels: number[] + updated?: boolean } /** diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts index 51c8f6f1..9055ea81 100644 --- a/frontend/src/store/modules/submission-notes.ts +++ b/frontend/src/store/modules/submission-notes.ts @@ -96,7 +96,6 @@ 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) { @@ -112,6 +111,17 @@ function SET_FEEDBACK_LABELS(state: SubmissionNotesState, labels: number[]) { state.changedLabels = true state.updatedFeedback.labels = labels } +function ADD_FEEDBACK_LABEL(state: SubmissionNotesState, label: number) { + state.changedLabels = true + state.updatedFeedback.labels.push(label) +} +function REMOVE_FEEDBACK_LABEL(state: SubmissionNotesState, label: number) { + state.changedLabels = true + const tmp = state.updatedFeedback.labels.filter((val) => { + return val !== label + }) + state.updatedFeedback.labels = tmp +} function UPDATE_FEEDBACK_SCORE(state: SubmissionNotesState, score: number) { state.updatedFeedback.score = score } @@ -193,6 +203,8 @@ export const SubmissionNotes = { SET_ORIG_FEEDBACK: mb.commit(SET_ORIG_FEEDBACK), SET_SHOW_FEEDBACK: mb.commit(SET_SHOW_FEEDBACK), SET_FEEDBACK_LABELS: mb.commit(SET_FEEDBACK_LABELS), + ADD_FEEDBACK_LABEL: mb.commit(ADD_FEEDBACK_LABEL), + REMOVE_FEEDBACK_LABEL: mb.commit(REMOVE_FEEDBACK_LABEL), UPDATE_FEEDBACK_LINE: mb.commit(UPDATE_FEEDBACK_LINE), UPDATE_FEEDBACK_SCORE: mb.commit(UPDATE_FEEDBACK_SCORE), DELETE_FEEDBACK_LINE: mb.commit(DELETE_FEEDBACK_LINE), -- GitLab