Skip to content

Commit

Permalink
Create edit saved annotation component
Browse files Browse the repository at this point in the history
  • Loading branch information
jorg-vr committed Jun 20, 2022
1 parent a115ce9 commit a3a8ead
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 50 deletions.
69 changes: 69 additions & 0 deletions app/assets/javascripts/components/modal_mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { html, TemplateResult, render } from "lit";
import { ref } from "lit/directives/ref.js";
import { Modal as Modal } from "bootstrap";
import { ShadowlessLitElement } from "components/shadowless_lit_element";

export declare abstract class ModalMixinInterface {
modalTemplate(title: TemplateResult, body: TemplateResult, footer: TemplateResult): TemplateResult;
abstract get filledModalTemplate() :TemplateResult;
showModal(): void;
hideModal(): void;
}

type Constructor<T> = abstract new (...args: any[]) => T;

export function modalMixin<T extends Constructor<ShadowlessLitElement>>(superClass: T): Constructor<ModalMixinInterface> & T {
abstract class ModalMixinClass extends superClass implements ModalMixinInterface {
modal: Modal;

private initModal(el: Element): void {
if (!this.modal) {
this.modal = new Modal(el);
} else {
this.modal.handleUpdate();
}
}

modalTemplate(title: TemplateResult, body: TemplateResult, footer: TemplateResult): TemplateResult {
return html`
<div class="modal fade" ${ref(el => this.initModal(el))} tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">${title}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" @click=${() => this.hideModal()}></button>
</div>
<div class="modal-body">
${body}
</div>
<div class="modal-footer">
${footer}
</div>
</div>
</div>
</div>`;
}

abstract get filledModalTemplate() :TemplateResult;

update(changedProperties: Map<string, any>): void {
super.update(changedProperties);
this.renderModal();
}

private renderModal(): void {
render(this.filledModalTemplate, document.getElementById("modal-container"), { host: this });
}

showModal(): void {
this.renderModal();
this.modal?.show();
}

hideModal(): void {
this.modal?.hide();
}
}

return ModalMixinClass as Constructor<ModalMixinInterface> & T;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { customElement, property } from "lit/decorators.js";
import { html, TemplateResult } from "lit";
import { ShadowlessLitElement } from "components/shadowless_lit_element";
import { SavedAnnotation, updateSavedAnnotation } from "state/SavedAnnotations";
import "./saved_annotation_form";
import { modalMixin } from "components/modal_mixin";

@customElement("d-edit-saved-annotation")
export class EditSavedAnnotation extends modalMixin(ShadowlessLitElement) {
@property({ type: Object })
savedAnnotation: SavedAnnotation;

@property({ state: true })
errors: string[];

async updateSavedAnnotation(): Promise<void> {
try {
await updateSavedAnnotation(this.savedAnnotation.id, {
saved_annotation: this.savedAnnotation
});
this.errors = undefined;
this.hideModal();
} catch (errors) {
this.errors = errors;
}
}

get filledModalTemplate(): TemplateResult {
return this.modalTemplate(html`
${I18n.t("js.saved_annotation.edit.title")}</h4>
`, html`
${this.errors !== undefined ? html`
<div class="callout callout-danger">
<h4>${I18n.t("js.saved_annotation.edit.errors", {count: this.errors.length})}</h4>
<ul>
${this.errors.map(error => html`
<li>${error}</li>`)}
</ul>
</div>
` : ""}
<d-saved-annotation-form
.savedAnnotation=${this.savedAnnotation}
@change=${e => this.savedAnnotation = e.detail}
></d-saved-annotation-form>
`, html`
<button class="btn btn-primary btn-text" @click=${() => this.updateSavedAnnotation()}>
${I18n.t("js.saved_annotation.edit.save")}
</button>
`);
}

render(): TemplateResult {
return html`
<a class="btn btn-icon"
title="${I18n.t("js.saved_annotation.edit.button_title")}"
@click=${() => this.showModal()}
>
<i class="mdi mdi-pencil"></i>
</a>`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { customElement, property } from "lit/decorators.js";
import { html, TemplateResult } from "lit";
import { ShadowlessLitElement } from "components/shadowless_lit_element";
import { createSavedAnnotation, SavedAnnotation } from "state/SavedAnnotations";
import { ref } from "lit/directives/ref.js";
import { Modal } from "bootstrap";
import "./saved_annotation_form";
import { modalMixin } from "components/modal_mixin";

@customElement("d-new-saved-annotation")
export class NewSavedAnnotation extends ShadowlessLitElement {
export class NewSavedAnnotation extends modalMixin(ShadowlessLitElement) {
@property({ type: Number, attribute: "from-annotation-id" })
fromAnnotationId: number;
@property({ type: String, attribute: "annotation-text" })
Expand All @@ -17,7 +16,6 @@ export class NewSavedAnnotation extends ShadowlessLitElement {
errors: string[];

savedAnnotation: SavedAnnotation;
modal: Modal;

get newSavedAnnotation(): SavedAnnotation {
return {
Expand All @@ -34,54 +32,43 @@ export class NewSavedAnnotation extends ShadowlessLitElement {
saved_annotation: this.savedAnnotation
});
this.errors = undefined;
this.modal?.hide();
this.hideModal();
} catch (errors) {
this.errors = errors;
}
}

initModal(el: Element): void {
if (!this.modal) {
this.modal = new Modal(el);
}
get filledModalTemplate(): TemplateResult {
return this.modalTemplate(html`
${I18n.t("js.saved_annotation.new.title")}
`, html`
${this.errors !== undefined ? html`
<div class="callout callout-danger">
<h4>${I18n.t("js.saved_annotation.new.errors", {count: this.errors.length})}</h4>
<ul>
${this.errors.map(error => html`
<li>${error}</li>`)}
</ul>
</div>
` : ""}
<d-saved-annotation-form
.savedAnnotation=${this.newSavedAnnotation}
@change=${e => this.savedAnnotation = e.detail}
></d-saved-annotation-form>
`, html`
<button class="btn btn-primary btn-text" @click=${() => this.createSavedAnnotation()}>
${I18n.t("js.saved_annotation.new.save")}
</button>
`);
}

render(): TemplateResult {
return html`
<a class="btn btn-icon annotation-control-button annotation-edit"
title="${I18n.t("js.saved_annotation.new.button_title")}"
@click=${() => this.modal.show()}
@click=${() => this.showModal()}
>
<i class="mdi mdi-content-save"></i>
</a>
<div class="modal fade" ${ref(el => this.initModal(el))} tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">${I18n.t("js.saved_annotation.new.title")}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${this.errors !== undefined ? html`
<div class="callout callout-danger">
<h4>${I18n.t("js.saved_annotation.new.errors", { count: this.errors.length })}</h4>
<ul>
${this.errors.map(error => html`<li>${error}</li>`)}
</ul>
</div>
` : ""}
<d-saved-annotation-form
.savedAnnotation=${this.newSavedAnnotation}
@change=${e => this.savedAnnotation = e.detail}
></d-saved-annotation-form>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-text" @click=${() => this.createSavedAnnotation()}>
${I18n.t("js.saved_annotation.new.save")}
</button>
</div>
</div>
</div>
</div>`;
</a>`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { html, TemplateResult } from "lit";
import { ShadowlessLitElement } from "components/shadowless_lit_element";
import { getSavedAnnotations, SavedAnnotation } from "state/SavedAnnotations";
import { stateMixin } from "state/StateMixin";
import "./edit_saved_annotation";

@customElement("d-saved-annotation-list")
export class SavedAnnotationList extends stateMixin(ShadowlessLitElement) {
Expand All @@ -24,7 +25,7 @@ export class SavedAnnotationList extends stateMixin(ShadowlessLitElement) {
}

render(): TemplateResult {
return html`
return this.savedAnnotations.length > 0 ? html`
<div class="card-supporting-text card-border">
<h4 class="ellipsis-overflow" title=">${I18n.t("js.saved_annotation.list.title")}">
${I18n.t("js.saved_annotation.list.title")}
Expand All @@ -35,9 +36,7 @@ export class SavedAnnotationList extends stateMixin(ShadowlessLitElement) {
<tr>
<td>${sa.title}</td>
<td class="actions">
<a class="btn btn-icon">
<i class="mdi mdi-pencil"></i>
</a>
<d-edit-saved-annotation .savedAnnotation=${sa}></d-edit-saved-annotation>
<a class="btn btn-icon btn-icon-filled bg-danger">
<i class="mdi mdi-delete"></i>
</a>
Expand All @@ -47,6 +46,6 @@ export class SavedAnnotationList extends stateMixin(ShadowlessLitElement) {
</tbody>
</table>
</div>
`;
` : html``;
}
}
4 changes: 2 additions & 2 deletions app/assets/javascripts/i18n/translations.js

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions app/assets/javascripts/state/SavedAnnotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ export async function createSavedAnnotation(data: { from: number, saved_annotati
return savedAnnotation.id;
}

export async function updateSavedAnnotation(number, id: number, data: {saved_annotation: SavedAnnotation}): Promise<void> {
export async function updateSavedAnnotation(id: number, data: {saved_annotation: SavedAnnotation}): Promise<void> {
const url = `${URL}/${id}`;
await fetch(url, {
const response = await fetch(url, {
method: "put",
body: JSON.stringify(data),
headers: {
"X-CSRF-Token": $("meta[name='csrf-token']").attr("content"),
"Content-type": "application/json"
},
});
if (response.status === 422) {
const errors = await response.json();
throw errors;
}
events.publish("fetchSavedAnnotations");
events.publish(`fetchSavedAnnotation${id}`, id);
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/saved_annotations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create

def update
respond_to do |format|
if @saved_annotation.update(args)
if @saved_annotation.update(permitted_attributes(SavedAnnotation))
format.json { render :show, status: :ok, location: @saved_annotation }
else
format.json { render json: @saved_annotation.errors, status: :unprocessable_entity }
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/_main_container.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@
</div>
<% end %>
</div>
<div id="modal-container"></div>
<%= yield %>
</div>
5 changes: 5 additions & 0 deletions config/locales/js/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,10 @@ en:
save: "Save"
title: "Save comment for re-use"
errors: "%{count} errors prevented saving"
edit:
button_title: 'Edit saved comment'
save: "Save"
title: "Edit saved comment"
errors: "%{count} errors prevented saving"
list:
title: Saved comments
5 changes: 5 additions & 0 deletions config/locales/js/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ nl:
save: "Opslaan"
title: "Opmerking opslaan om later te hergebruiken"
errors: "%{count} errors verhinderen het opslaan"
edit:
button_title: 'Bewerk opgeslagen opmerking'
save: "Opslaan"
title: "Bewerk opgeslagen opmerking"
errors: "%{count} errors verhinderen het opslaan"
list:
title: Opgeslagen opmerkingen

0 comments on commit a3a8ead

Please sign in to comment.