Skip to content

Commit

Permalink
Merge pull request #4701 from dodona-edu/enhance/reorganize-saved-ann…
Browse files Browse the repository at this point in the history
…otation-form

Update annotation form
  • Loading branch information
jorg-vr authored Aug 16, 2023
2 parents 188842b + 5e02361 commit 8f1cbc0
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 134 deletions.
198 changes: 118 additions & 80 deletions app/assets/javascripts/components/annotations/annotation_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import "components/saved_annotations/saved_annotation_input";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { annotationState } from "state/Annotations";
import { userAnnotationState } from "state/UserAnnotations";
import { savedAnnotationState } from "state/SavedAnnotations";
import { courseState } from "state/Courses";
import { exerciseState } from "state/Exercises";
import { userState } from "state/Users";

// Min and max of the annotation text is defined in the annotation model.
const maxLength = 10_000;
Expand Down Expand Up @@ -44,10 +48,18 @@ export class AnnotationForm extends watchMixin(ShadowlessLitElement) {
@property({ state: true })
_savedAnnotationId = "";
@property({ state: true })
savedAnnotationTitle: string;
_savedAnnotationTitle: string;
@property({ state: true })
saveAnnotation = false;

get savedAnnotationTitle(): string {
return this._savedAnnotationTitle || this._annotationText.split(/\s+/).slice(0, 5).join(" ").slice(0, 40);
}

set savedAnnotationTitle(title: string) {
this._savedAnnotationTitle = title;
}

inputRef: Ref<HTMLTextAreaElement> = createRef();
titleRef: Ref<HTMLInputElement> = createRef();

Expand Down Expand Up @@ -176,95 +188,121 @@ export class AnnotationForm extends watchMixin(ShadowlessLitElement) {
this.inputRef.value.focus();
}

updated(changedProperties: Map<string, any>): void {
// Focus the newly shown title input if the user wants to save the annotation.
if (changedProperties.has("saveAnnotation") && this.saveAnnotation) {
this.titleRef.value.focus();
this.titleRef.value.select();
}
}

toggleSaveAnnotation(): void {
this.saveAnnotation = !this.saveAnnotation;
if (this.saveAnnotation && !this.savedAnnotationTitle) {
// Take the first five words, with a max of 40 chars as default title
this.savedAnnotationTitle = this._annotationText.split(/\s+/).slice(0, 5).join(" ").slice(0, 40);
}
}

get canSaveAnnotation(): boolean {
return !annotationState.isQuestionMode && /* REMOVE AFTER CLOSED BETA */ isBetaCourse();
}

get potentialSavedAnnotationsExist(): boolean {
return (savedAnnotationState.getList(new Map([
["course_id", courseState.id.toString()],
["exercise_id", exerciseState.id.toString()],
["user_id", userState.id.toString()]
])) || []).length > 0;
}


render(): TemplateResult {
return html`
<form class="annotation-submission form">
${annotationState.isQuestionMode || /* REMOVE AFTER CLOSED BETA */ !isBetaCourse() ? "" : html`
<d-saved-annotation-input
name="saved_annotation_id"
class="saved-annotation-input"
.value=${this._savedAnnotationId}
annotation-text="${this._annotationText}"
@input="${e => this.handleSavedAnnotationInput(e)}"
></d-saved-annotation-input>
`}
<div class="field form-group">
${annotationState.isQuestionMode || /* REMOVE AFTER CLOSED BETA */ !isBetaCourse() ? "" : html`
<label class="form-label" for="annotation-text">
${I18n.t("js.user_annotation.fields.annotation_text")}
</label>
`}
<textarea id="annotation-text"
autofocus
required
class="form-control annotation-submission-input ${this.hasErrors ? "validation-error" : ""}"
.rows=${this.rows}
minlength="1"
maxlength="${maxLength}"
.value=${this._annotationText}
${ref(this.inputRef)}
@keydown="${e => this.handleKeyDown(e)}"
@input="${() => this.handleTextInput()}"
></textarea>
<div class="clearfix annotation-help-block">
<span class='help-block'>${unsafeHTML(I18n.t("js.user_annotation.help"))}</span>
${annotationState.isQuestionMode ? html`
<span class='help-block'>${unsafeHTML(I18n.t("js.user_annotation.help_student"))}</span>
` : ""}
<span class="help-block float-end">
<span class="used-characters">${I18n.formatNumber(this._annotationText.length)}</span> / ${I18n.formatNumber(maxLength)}
</span>
</div>
</div>
${annotationState.isQuestionMode || /* REMOVE AFTER CLOSED BETA */ !isBetaCourse() ? "" : html`
<div class="field form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" @click="${() => this.toggleSaveAnnotation()}" id="check-save-annotation">
<label class="form-check-label" for="check-save-annotation">
${I18n.t("js.user_annotation.fields.saved_annotation_title")}
</label>
<div class="row">
<div class="col-lg-${this.canSaveAnnotation && this.potentialSavedAnnotationsExist ? 8 : 12}">
<div class="field form-group">
<textarea id="annotation-text"
autofocus
required
class="form-control annotation-submission-input ${this.hasErrors ? "validation-error" : ""}"
.rows=${this.rows}
minlength="1"
maxlength="${maxLength}"
.value=${this._annotationText}
${ref(this.inputRef)}
@keydown="${e => this.handleKeyDown(e)}"
@input="${() => this.handleTextInput()}"
></textarea>
<div class="clearfix annotation-help-block">
<span class='help-block'>${unsafeHTML(I18n.t("js.user_annotation.help"))}</span>
${annotationState.isQuestionMode ? html`
<span class='help-block'>${unsafeHTML(I18n.t("js.user_annotation.help_student"))}</span>
` : ""}
<span class="help-block float-end">
<span class="used-characters">${I18n.formatNumber(this._annotationText.length)}</span> / ${I18n.formatNumber(maxLength)}
</span>
</div>
</div>
</div>
${ this.saveAnnotation ? html`
<div class="field form-group">
<label class="form-label" for="saved-annotation-title">
${I18n.t("js.saved_annotation.title")}
</label>
<input required="required"
class="form-control"
type="text"
${ref(this.titleRef)}
@keydown="${e => this.handleKeyDown(e)}"
@input=${() => this.handleUpdateTitle()}
value=${this.savedAnnotationTitle}
id="saved-annotation-title"
>
${ this.canSaveAnnotation && this.potentialSavedAnnotationsExist ? html`
<div class="col-lg-4">
<d-saved-annotation-input
name="saved_annotation_id"
class="saved-annotation-input"
.value=${this._savedAnnotationId}
annotation-text="${this._annotationText}"
@input="${e => this.handleSavedAnnotationInput(e)}"
.disabled=${this.saveAnnotation}
></d-saved-annotation-input>
</div>
` : html``}
`}
<div class="annotation-submission-button-container">
<button class="btn btn-text annotation-control-button annotation-cancel-button"
type="button"
@click="${() => this.handleCancel()}"
.disabled=${this.disabled}
>
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-filled annotation-control-button annotation-submission-button"
type="button"
@click="${() => this.handleSubmit()}"
.disabled=${this.disabled}
>
${I18n.t(`js.${this.type}.${this.submitButtonText}`)}
</button>
` : ""}
</div>
<div class="row mb-1">
<div class="col-lg-7 col-xxl-8 align-items-center d-inline-flex">
${ this.canSaveAnnotation && this._savedAnnotationId == "" ? html`
<div class="field form-group mb-0">
<div class="form-check save-annotation-check">
<input class="form-check-input mt-2"
type="checkbox"
@click="${() => this.toggleSaveAnnotation()}"
id="check-save-annotation"
.checked=${this.saveAnnotation}
>
<label class="form-check-label mt-2" for="check-save-annotation">
${I18n.t("js.user_annotation.fields.saved_annotation_title")}
</label>
</div>
${this.saveAnnotation ? html`
<div class="saved-annotation-title">
<input required="required"
class="form-control"
type="text"
${ref(this.titleRef)}
@keydown="${e => this.handleKeyDown(e)}"
@input=${() => this.handleUpdateTitle()}
value=${this.savedAnnotationTitle}
id="saved-annotation-title"
>
<label for="saved-annotation-title">${I18n.t("js.saved_annotation.title")}:</label>
</div>
`: ""}
</div>
` : ""}
</div>
<div class="col-lg-5 col-xxl-4 mt-2 mt-lg-0" style="text-align: right">
<button class="btn btn-text"
type="button"
@click="${() => this.handleCancel()}"
.disabled=${this.disabled}
>
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-filled"
type="button"
@click="${() => this.handleSubmit()}"
.disabled=${this.disabled}
>
${I18n.t(`js.${this.type}.${this.submitButtonText}`)}
</button>
</div>
</div>
</form>
`;
Expand Down
6 changes: 5 additions & 1 deletion app/assets/javascripts/components/datalist_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Option = {label: string, value: string, extra?: string};
* The extra string is added in the options and also used to match the input
* @prop {String} value - the initial value for this field
* @prop {String} placeholder - placeholder text shown in input
* @prop {Boolean} disabled - whether the input is disabled
*
* @fires input - on value change, event details contain {label: string, value: string}
*/
Expand All @@ -30,6 +31,8 @@ export class DatalistInput extends watchMixin(ShadowlessLitElement) {
value: string;
@property({ type: String })
placeholder: string;
@property({ type: Boolean })
disabled = false;

inputRef: Ref<HTMLInputElement> = createRef();

Expand Down Expand Up @@ -191,9 +194,10 @@ export class DatalistInput extends watchMixin(ShadowlessLitElement) {
placeholder="${this.placeholder}"
@keydown=${e => this.keydown(e)}
@focus=${() => this.requestUpdate()}
.disabled="${this.disabled}"
>
<ul class="dropdown-menu ${this.filtered_options.length > 0 ? "show-search-dropdown" : ""}"
style="position: fixed; top: ${this.dropdown_top}px; left: ${this.dropdown_left}px; max-width: ${this.dropdown_width}px; overflow-x: hidden;">
style="position: fixed; top: ${this.dropdown_top}px; left: ${this.dropdown_left}px; width: ${this.dropdown_width}px; overflow-x: hidden;">
${this.filtered_options.map(option => html`
<li><a class="dropdown-item ${this.isSoftSelected(option) ? "active" :""} " @click=${ e => this.select(option, e)} style="cursor: pointer;">
${this.mark(option.label)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { exerciseState } from "state/Exercises";
* @prop {String} name - name of the input field (used in form submit)
* @prop {String} value - the initial saved annotation id
* @prop {String} annotationText - the current text of the real annotation, used to detect if there are manual changes from the selected saved annotation
* @prop {Boolean} disabled - whether the input is disabled
*
* @fires input - on value change, event details contain {title: string, id: string, annotation_text: string}
*/
Expand All @@ -28,33 +29,30 @@ export class SavedAnnotationInput extends ShadowlessLitElement {
value: string;
@property( { type: String, attribute: "annotation-text" })
annotationText: string;
@property({ type: Boolean })
disabled = false;

@property({ state: true })
__label: string;

get userId(): number {
return userState.id;
}

get label(): string {
return this.value ? savedAnnotationState.get(parseInt(this.value))?.title : this.__label;
}

lastSavedAnnotations: SavedAnnotation[] = [];
get savedAnnotations(): SavedAnnotation[] {
return savedAnnotationState.getList(new Map([
const savedAnnotations = savedAnnotationState.getList(new Map([
["course_id", courseState.id.toString()],
["exercise_id", exerciseState.id.toString()],
["user_id", this.userId.toString()],
["user_id", userState.id.toString()],
["filter", this.__label]
]));
}

get potentialSavedAnnotationsExist(): boolean {
return savedAnnotationState.getList(new Map([
["course_id", courseState.id.toString()],
["exercise_id", exerciseState.id.toString()],
["user_id", this.userId.toString()]
])).length > 0;
if (savedAnnotations === undefined) {
// return last saved annotations if the updated list is not yet available
return this.lastSavedAnnotations;
}
this.lastSavedAnnotations = savedAnnotations;
return savedAnnotations;
}

get selectedAnnotation(): SavedAnnotation {
Expand Down Expand Up @@ -87,23 +85,21 @@ export class SavedAnnotationInput extends ShadowlessLitElement {
}

render(): TemplateResult {
return this.potentialSavedAnnotationsExist ? html`
return html`
<div class="field form-group">
<label class="form-label">
${I18n.t("js.saved_annotation.input.title")}
</label>
<div class="position-relative">
<d-datalist-input
name="${this.name}"
.options=${this.options}
.value=${this.value}
.disabled=${this.disabled}
@input="${e => this.processInput(e)}"
placeholder="${I18n.t("js.saved_annotation.input.placeholder")}"
></d-datalist-input>
${ this.selectedAnnotation && this.selectedAnnotation.annotation_text !== this.annotationText ? html`
<i
class="mdi mdi-not-equal-variant colored-info position-absolute"
style="left: 165px; top: 3px;"
style="right: 5px; top: 3px;"
title="${I18n.t("js.saved_annotation.input.edited")}"
></i>
` : ""}
Expand All @@ -112,6 +108,6 @@ export class SavedAnnotationInput extends ShadowlessLitElement {
${unsafeHTML(I18n.t("js.saved_annotation.input.help_html"))}
</div>
</div>
` : html``;
`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class SavedAnnotationList extends ShadowlessLitElement {
}

get savedAnnotations(): SavedAnnotation[] {
return savedAnnotationState.getList(this.queryParams, this.arrayQueryParams);
return savedAnnotationState.getList(this.queryParams, this.arrayQueryParams) || [];
}

get pagination(): Pagination {
Expand Down
Loading

0 comments on commit 8f1cbc0

Please sign in to comment.