Skip to content

Commit

Permalink
Add UI for saved annotation re-use
Browse files Browse the repository at this point in the history
  • Loading branch information
chvp committed May 12, 2022
1 parent 1562985 commit dc86204
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 23 deletions.
4 changes: 4 additions & 0 deletions app/assets/javascripts/code_listing/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ export abstract class Annotation {
return this.text;
}

public get savedAnnotationId(): number | null {
return null;
}

public get removable(): boolean {
return false;
}
Expand Down
84 changes: 67 additions & 17 deletions app/assets/javascripts/code_listing/code_listing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initTooltips } from "util.js";
import { initTooltips, fetch } from "util.js";
import { Annotation, AnnotationType } from "code_listing/annotation";
import { MachineAnnotation, MachineAnnotationData } from "code_listing/machine_annotation";
import {
Expand Down Expand Up @@ -40,6 +40,9 @@ export class CodeListing {
public readonly code: string;
public readonly codeLines: number;
public readonly submissionId: number;
public readonly courseId: number;
public readonly exerciseId: number;
public readonly userId: number;

private readonly markingClass: string = "marked";
private evaluationId: number;
Expand All @@ -56,11 +59,14 @@ export class CodeListing {

private readonly questionMode: boolean;

constructor(submissionId: number, code: string, codeLines: number, questionMode = false) {
constructor(submissionId: number, courseId: number, exerciseId: number, userId: number, code: string, codeLines: number, questionMode = false) {
this.annotations = new Map<number, Annotation[]>();
this.code = code;
this.codeLines = codeLines;
this.submissionId = submissionId;
this.courseId = courseId;
this.exerciseId = exerciseId;
this.userId = userId;
this.questionMode = questionMode;

this.badge = document.querySelector<HTMLSpanElement>(badge);
Expand Down Expand Up @@ -376,25 +382,41 @@ export class CodeListing {
<span class='help-block'>${I18n.t("js.user_annotation.help")}</span>
<span class="help-block float-end"><span class="used-characters">0</span> / ${I18n.toNumber(maxLength, { precision: 0 })}</span>
</div>
<div class="annotation-submission-button-container">
${annotation && annotation.removable ? `
<button class="btn-text annotation-control-button annotation-delete-button" type="button">
${I18n.t("js.user_annotation.delete")}
</button>
` : ""}
<button class="btn-text annotation-control-button annotation-cancel-button" type="button">
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-text btn-primary annotation-control-button annotation-submission-button" type="button">
${(annotation !== null ? I18n.t(`js.${type}.update`) : I18n.t(`js.${type}.send`))}
</button>
<div class="annotation-controls-block">
${this.questionMode ? "" : `
<input type="hidden" class="saved-annotation-id" value="${annotation?.savedAnnotationId || ""}"/>
<span class="dropdown">
<input class="dropdown-toggle form-control annotation-search-saved" placeholder="${I18n.t("js.user_annotation.use_saved")}">
<ul class="dropdown-menu">
</ul>
</span>
`}
<span class="annotation-submission-button-container">
${annotation && annotation.removable ? `
<button class="btn-text annotation-control-button annotation-delete-button" type="button">
${I18n.t("js.user_annotation.delete")}
</button>
` : ""}
<button class="btn-text annotation-control-button annotation-cancel-button" type="button">
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-text btn-primary annotation-control-button annotation-submission-button" type="button">
${(annotation !== null ? I18n.t(`js.${type}.update`) : I18n.t(`js.${type}.send`))}
</button>
</span>
</div>
`;

const cancelButton = form.querySelector<HTMLButtonElement>(annotationFormCancel);
const deleteButton = form.querySelector<HTMLButtonElement>(annotationFormDelete);
const sendButton = form.querySelector<HTMLButtonElement>(annotationFormSubmit);
const inputField = form.querySelector<HTMLTextAreaElement>("textarea");
const saveSearchField = form.querySelector<HTMLInputElement>(".annotation-search-saved");
const searchDropdown = new bootstrap.Dropdown(form.querySelector<HTMLButtonElement>(".dropdown-toggle"));
const searchResults = form.querySelector<HTMLUListElement>(".dropdown-menu");
const savedAnnotationField = form.querySelector<HTMLInputElement>(".saved-annotation-id");

this.setupSaveAutocomplete(saveSearchField, inputField, savedAnnotationField, searchDropdown, searchResults);

if (annotation !== null) {
inputField.rows = annotation.rawText.split("\n").length + 1;
Expand Down Expand Up @@ -451,9 +473,36 @@ export class CodeListing {
return form;
}

private setupSaveAutocomplete(input: HTMLInputElement, textField: HTMLTextAreaElement, savedAnnotationField: HTMLInputElement, dropdown: bootstrap.Dropdown, resultsContainer: HTMLUListElement) {
input.addEventListener("input", async () => {
fetch(`/saved_annotations.json?course_id=${this.courseId}&exercise_id=${this.exerciseId}&user_id=${this.userId}&filter=${input.value}`)
.then(response => response.json())
.then(results => {
resultsContainer.innerHTML = "";
results.forEach(savedAnnotation => {
const element = document.createElement<HTMLLIElement>("li");
const innerElement = document.createElement<HTMLAnchorElement>("a");
innerElement.classList.add("dropdown-item");
innerElement.innerText = savedAnnotation.title;
innerElement.addEventListener("click", () => {
input.value = savedAnnotation.title;
textField.value = savedAnnotation.annotation_text;
savedAnnotationField.value = savedAnnotation.id;
dropdown.hide();
});
element.appendChild(innerElement);
resultsContainer.appendChild(element);
});
dropdown.show();
});
});

}

private createNewAnnotationForm(line: number | null): HTMLFormElement {
const onSubmit = async (form: HTMLFormElement): Promise<void> => {
const inputField = form.querySelector<HTMLTextAreaElement>("textarea");
const savedAnnotationField = form.querySelector<HTMLInputElement>(".saved-annotation-id");
inputField.classList.remove("validation-error");

// Run client side validations.
Expand All @@ -464,7 +513,8 @@ export class CodeListing {
const annotationData: UserAnnotationFormData = {
"annotation_text": inputField.value,
"line_nr": (line === null ? null : line - 1),
"evaluation_id": this.evaluationId || undefined
"evaluation_id": this.evaluationId || undefined,
"saved_annotation_id": savedAnnotationField.value || undefined,
};

try {
Expand All @@ -486,6 +536,7 @@ export class CodeListing {
callback: CallableFunction): HTMLFormElement {
const onSubmit = async (form: HTMLFormElement): Promise<void> => {
const inputField = form.querySelector<HTMLTextAreaElement>("textarea");
const savedAnnotationField = form.querySelector<HTMLInputElement>(".saved-annotation-id");

// Run client side validations.
if (!inputField.reportValidity()) {
Expand All @@ -494,8 +545,7 @@ export class CodeListing {

const annotationData: UserAnnotationFormData = {
"annotation_text": inputField.value,
"line_nr": (annotation.line === null ? null : annotation.line - 1),
"evaluation_id": annotation.evaluationId || undefined
"saved_annotation_id": savedAnnotationField.value || undefined,
};

try {
Expand Down
8 changes: 8 additions & 0 deletions app/assets/javascripts/code_listing/user_annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface UserAnnotationData {
rendered_markdown: string;
// eslint-disable-next-line camelcase
evaluation_id: number | null;
// eslint-disable-next-line camelcase
saved_annotation_id: number | null;
url: string;
user: UserAnnotationUserData;
type: string;
Expand All @@ -51,6 +53,7 @@ export class UserAnnotation extends Annotation {
private readonly __rawText: string;
public readonly released: boolean;
public readonly evaluationId: number | null;
private readonly __savedAnnotationId: number | null;
public readonly url: string;
public readonly user: UserAnnotationUserData;
public readonly lastUpdatedBy: UserAnnotationUserData;
Expand All @@ -66,6 +69,7 @@ export class UserAnnotation extends Annotation {
this.released = data.released;
this.__rawText = data.annotation_text;
this.evaluationId = data.evaluation_id;
this.__savedAnnotationId = data.saved_annotation_id;
this.url = data.url;
this.user = data.user;
this.lastUpdatedBy = data.last_updated_by;
Expand Down Expand Up @@ -109,6 +113,10 @@ export class UserAnnotation extends Annotation {
return this.__rawText;
}

public get savedAnnotationId(): number | null {
return this.__savedAnnotationId;
}

public get removable(): boolean {
return this.permissions.destroy;
}
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/i18n/translations.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions app/assets/stylesheets/components/code_listing.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ $annotation-info: $info-500;
margin-bottom: 0;
}

.annotation-controls-block {
display: flex;
justify-content: space-between;
align-items: center;
}

.annotation-submission-button-container {
margin-top: 12px;
margin-bottom: 12px;
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/saved_annotations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
class SavedAnnotationsController < ApplicationController
before_action :set_saved_annotation, only: %i[show update destroy]

has_scope :by_user, as: 'user_id'
has_scope :by_course, as: 'course_id'
has_scope :by_exercise, as: 'exercise_id'
has_scope :by_filter, as: 'filter'

def index
authorize SavedAnnotation
@saved_annotations = apply_scopes(policy_scope(SavedAnnotation.all))
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/renderers/feedback_code_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def add_messages(submission, messages, user)
@builder.script(type: 'application/javascript') do
@builder << <<~HEREDOC
window.MathJax.startup.promise.then(() => {
window.dodona.codeListing = new window.dodona.codeListingClass(#{submission.id}, #{@code.to_json}, #{@code.lines.length}, #{user_is_student});
window.dodona.codeListing = new window.dodona.codeListingClass(#{submission.id}, #{submission.course_id}, #{submission.exercise_id}, #{user.id}, #{@code.to_json}, #{@code.lines.length}, #{user_is_student});
window.dodona.codeListing.addMachineAnnotations(#{messages.to_json});
#{'window.dodona.codeListing.initAnnotateButtons();' if user_perm}
window.dodona.codeListing.loadUserAnnotations();
Expand Down
5 changes: 5 additions & 0 deletions app/models/saved_annotation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ class SavedAnnotation < ApplicationRecord
belongs_to :course

has_many :annotations, dependent: :nullify

scope :by_user, ->(user_id) { where user_id: user_id }
scope :by_course, ->(course_id) { where course_id: course_id }
scope :by_exercise, ->(exercise_id) { where exercise_id: exercise_id }
scope :by_filter, ->(filter) { where 'title LIKE ?', "%#{filter}%" }
end
4 changes: 2 additions & 2 deletions app/policies/annotation_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ def transition?(_from, _to = nil)
end

def permitted_attributes_for_create
%i[annotation_text line_nr evaluation_id]
%i[annotation_text saved_annotation_id line_nr evaluation_id]
end

def permitted_attributes_for_update
%i[annotation_text]
%i[annotation_text saved_annotation_id]
end
end
2 changes: 1 addition & 1 deletion app/views/annotations/_annotation.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
json.extract! annotation, :id, :line_nr, :annotation_text, :user_id, :submission_id, :created_at, :updated_at
json.extract! annotation, :id, :line_nr, :annotation_text, :user_id, :submission_id, :saved_annotation_id, :created_at, :updated_at
if annotation.is_a?(Question)
json.extract! annotation, :question_state
json.newer_submission_url(annotation.newer_submission&.yield_self { |s| submission_url(s) })
Expand Down
3 changes: 3 additions & 0 deletions app/views/saved_annotations/index.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
json.array! @saved_annotations do |saved_annotation|
json.extract! saved_annotation, :id, :title, :annotation_text, :user_id, :exercise_id, :course_id, :created_at, :updated_at
end
1 change: 1 addition & 0 deletions config/locales/js/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ en:
delete: 'Delete'
edit: 'Edit comment'
save: 'Save comment'
use_saved: 'Saved comment'
help: 'Press Shift + Enter to send. <a href="https://docs.dodona.be/en/references/exercise-description/#markdown" target="_blank">Markdown</a> is supported.'
fields:
annotation_text: 'Comment'
Expand Down
1 change: 1 addition & 0 deletions config/locales/js/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ nl:
delete: 'Verwijderen'
edit: 'Opmerking bewerken'
save: 'Opmerking opslaan'
use_saved: 'Opgeslagen opmerking'
help: 'Druk Shift + Enter om te verzenden. <a href="https://docs.dodona.be/nl/references/exercise-description/#markdown" target="_blank">Markdown</a> wordt ondersteund.'
fields:
annotation_text: 'Opmerking'
Expand Down

0 comments on commit dc86204

Please sign in to comment.