Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow easy re-use of annotations #3600

Merged
merged 106 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
92bec99
Create basic UI for saving an annotation
chvp May 3, 2022
6e54792
Save new annotation with remote form
chvp May 11, 2022
1562985
Add unique index on saved annotations title
chvp May 11, 2022
cc27b0b
Add UI for saved annotation re-use
chvp May 12, 2022
f8b2df7
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Jun 13, 2022
f8aad09
Use datalist to save annotations
jorg-vr Jun 16, 2022
361bc86
Create generic staete mixin system for webcomponents
jorg-vr Jun 17, 2022
0118b8f
Move save announecement creation to the frontend
jorg-vr Jun 17, 2022
a115ce9
Crate list with saved annotations
jorg-vr Jun 17, 2022
a3a8ead
Create edit saved annotation component
jorg-vr Jun 20, 2022
9a6f3b3
Allow deleting saved annotations
jorg-vr Jun 20, 2022
dde7f20
Remove unused views
jorg-vr Jun 20, 2022
bc919df
Clean up code and add comments
jorg-vr Jun 20, 2022
80ee924
Add translations for delete button
jorg-vr Jun 20, 2022
1e47307
Init saved annotations by id
jorg-vr Jun 20, 2022
a230c5a
Create basic crud tests
jorg-vr Jun 20, 2022
7341516
Create annotation creation test
jorg-vr Jun 21, 2022
12df858
Fix tests
jorg-vr Jun 21, 2022
1bc952a
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Jun 21, 2022
79b0626
Fix javascript test to compile webcomponents
jorg-vr Jun 21, 2022
5a832c3
Fix non admin users asking questions
jorg-vr Jun 21, 2022
5de3d1f
Fix input service worker
jorg-vr Jun 21, 2022
9e9400d
Creat saved annotation index page
jorg-vr Jun 21, 2022
74aadf1
Create page for saved annotations
jorg-vr Jun 22, 2022
0a47498
Add pagination
jorg-vr Jun 22, 2022
f0350d4
Make saved annotation input work with pagination and add helpfull info
jorg-vr Jun 27, 2022
7514839
Make large version of annotation list
jorg-vr Jun 27, 2022
0facc44
Remove logging
jorg-vr Jun 27, 2022
835f09b
Fix datalist reloading when options change
jorg-vr Jun 27, 2022
2823b24
Refresh data after finding annotation
jorg-vr Jun 27, 2022
9b9eadb
Fix linting
jorg-vr Jun 27, 2022
0e8b17f
Try to identify bug
jorg-vr Jun 27, 2022
7223b53
Break tests
jorg-vr Jun 27, 2022
6db7d90
Update node version of tests
jorg-vr Jun 27, 2022
0c2e9bb
Revert "Update node version of tests"
jorg-vr Jun 27, 2022
897f6fd
Test the tests
jorg-vr Jun 27, 2022
2662ec0
Test the tests
jorg-vr Jun 27, 2022
2731b5e
Undo unneeded changes
jorg-vr Jun 27, 2022
af35cb6
Dont run server timings in tests
jorg-vr Jun 27, 2022
09dc53b
Remove rails server timings
jorg-vr Jun 27, 2022
88ae5f9
Update documentation
jorg-vr Jun 27, 2022
192a812
Remove unused param
jorg-vr Jun 27, 2022
5136b87
Limit number of queries on saved annotation list
jorg-vr Jun 27, 2022
ffb0db3
Move number of ietems per page responsibility to frontend
jorg-vr Jun 28, 2022
50e6891
Test creation failure
jorg-vr Jun 28, 2022
0441fd8
Make function of annotation input field clearer
jorg-vr Jun 28, 2022
aa34df8
Suggest default title for saved annotation
jorg-vr Jun 29, 2022
027502d
Limit title width in small view
jorg-vr Jun 29, 2022
8327b5c
Add icon to make edited saved annotation clear
jorg-vr Jun 29, 2022
4b5f803
Allow search by body
jorg-vr Jun 29, 2022
b214e1c
Fix linting
jorg-vr Jun 29, 2022
fa41e87
Update docs
jorg-vr Jun 29, 2022
aed722b
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Jun 30, 2022
701afcf
Add license
jorg-vr Jun 30, 2022
622600b
Add saved annotations to drawer
jorg-vr Jun 30, 2022
80249d5
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Jul 1, 2022
bbbe0e5
Show link icon when saved
jorg-vr Jul 1, 2022
bd7f3bf
Fix filled out saved annotation input with pagination combo
jorg-vr Jul 1, 2022
ce4bfab
Remove link icon upon saved annotation delete
jorg-vr Jul 1, 2022
4dd5e9b
Fix linting
jorg-vr Jul 1, 2022
c194cca
Limit feature in closed beta
jorg-vr Jul 1, 2022
60e9316
Fix drawwer link
jorg-vr Jul 1, 2022
c42b3a9
Fix linting
jorg-vr Jul 1, 2022
71fea8b
Fix tests with closed beta limitation
jorg-vr Jul 1, 2022
12ce912
Add hardcoded closed beta courses
jorg-vr Jul 1, 2022
09d826b
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Aug 3, 2022
91c2e1d
Add saved annotations to the evaluation
jorg-vr Aug 5, 2022
b278707
Partially rework datalist to a custom dropdown
jorg-vr Aug 5, 2022
0bc8f8e
Fix datalist input to use custom dropdown comoponent
jorg-vr Aug 8, 2022
5ab96e1
Make modal background static
jorg-vr Aug 8, 2022
f8329a6
Make form use full width
jorg-vr Aug 8, 2022
33211e2
explain in info text that existing annotations are not impacted
jorg-vr Aug 8, 2022
1ee13e3
Replace annotationform with webcomponent
jorg-vr Aug 10, 2022
b894649
Move saved annoation input
jorg-vr Aug 31, 2022
711720d
allow saving comments while creating them
jorg-vr Aug 31, 2022
65cbae6
Fix text update when selecting saved input
jorg-vr Aug 31, 2022
af6d48d
Split of save dannotations into their own card
jorg-vr Aug 31, 2022
3100b00
Also show saved annotations sidebar on submission page
jorg-vr Aug 31, 2022
8918440
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Sep 1, 2022
549cd51
Move delete button to edit modal
jorg-vr Sep 5, 2022
9356ddd
Add help texts
jorg-vr Sep 5, 2022
bf6a49c
Show number of usages
jorg-vr Sep 5, 2022
92b367f
Update count when new annotation is created from saved annotation
jorg-vr Sep 9, 2022
837fa35
Use util fetch function
jorg-vr Sep 12, 2022
255a782
Add delayer to limit the amount of requests
jorg-vr Sep 12, 2022
ec06c54
Move link icon
jorg-vr Sep 12, 2022
98a9f35
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Sep 12, 2022
1381af6
Remove uggly border
jorg-vr Sep 12, 2022
b2d9bcb
Fix linting
jorg-vr Sep 12, 2022
7f4c923
Fix tests
jorg-vr Sep 12, 2022
3150311
Use new I18n functionality
jorg-vr Sep 12, 2022
8c7a409
Fix javascript tests
jorg-vr Sep 12, 2022
5348342
Remove unused import
jorg-vr Sep 12, 2022
4b7993f
Fix yarn test
jorg-vr Sep 13, 2022
6b664d6
Fix system tests
jorg-vr Sep 13, 2022
d2d1f44
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Sep 13, 2022
6ce623a
change annotation to comment in english
jorg-vr Sep 13, 2022
3dfdf7c
Try to fix tests again
jorg-vr Sep 13, 2022
0444e02
Apply suggestions from code review
jorg-vr Sep 15, 2022
59b4c21
Update app/assets/javascripts/components/pagination.ts
jorg-vr Sep 15, 2022
a885280
Update app/assets/javascripts/components/pagination.ts
jorg-vr Sep 15, 2022
9c32a90
Merge branch 'develop' into feature/annotation-reuse
jorg-vr Sep 16, 2022
73f71ae
Delete already destroyed file in develop
jorg-vr Sep 16, 2022
233bb85
Add tests
jorg-vr Sep 19, 2022
e228a15
Fix linting
jorg-vr Sep 19, 2022
f30982a
Try to fix system tests
jorg-vr Sep 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ gem 'has_scope', '~> 0.8.0'
# generating zip files
gem 'rubyzip', '~> 2.3.2'

# add request server timings to the devtools
gem 'rails_server_timings', '~> 1.0.8'

# bootstrap tokenizer
gem 'bootstrap_tokenfield_rails', '~> 0.12.1'

Expand Down
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,6 @@ GEM
rails-i18n (7.0.3)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_server_timings (1.0.8)
railties (>= 3.0.0)
railties (7.0.3)
actionpack (= 7.0.3)
activesupport (= 7.0.3)
Expand Down Expand Up @@ -572,7 +570,6 @@ DEPENDENCIES
rails (~> 7.0.3)
rails-controller-testing (~> 1.0.5)
rails-i18n (~> 7.0.3)
rails_server_timings (~> 1.0.8)
rb-readline (~> 0.5.5)
rouge (= 3.29.0)
rubocop-rails (~> 2.15.0)
Expand Down
29 changes: 21 additions & 8 deletions app/assets/javascripts/code_listing/annotation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NewSavedAnnotation } from "components/saved_annotations/new_saved_annotation";

export type AnnotationType = "error" | "info" | "user" | "warning" | "question";
export type QuestionState = "unanswered" | "answered" | "in_progress";

Expand All @@ -10,6 +12,7 @@ export abstract class Annotation {
public readonly line: number | null;
public readonly text: string;
public readonly type: AnnotationType;
public readonly id: number;

protected constructor(line: number | null, text: string, type: AnnotationType) {
this.__html = null;
Expand Down Expand Up @@ -81,16 +84,22 @@ export abstract class Annotation {

// Update button.
if (this.modifiable) {
const link = document.createElement("a");
link.addEventListener("click", () => this.edit());
link.classList.add("btn", "btn-icon", "annotation-control-button", "annotation-edit");
link.title = this.editTitle;
const editLink = document.createElement("a");
editLink.addEventListener("click", () => this.edit());
editLink.classList.add("btn", "btn-icon", "annotation-control-button", "annotation-edit");
editLink.title = this.editTitle;

const icon = document.createElement("i");
icon.classList.add("mdi", "mdi-pencil");
link.appendChild(icon);
const editIcon = document.createElement("i");
editIcon.classList.add("mdi", "mdi-pencil");
editLink.appendChild(editIcon);

header.appendChild(link);
header.appendChild(editLink);

const saveLink = new NewSavedAnnotation();
saveLink.fromAnnotationId = this.id;
saveLink.annotationText = this.rawText;

header.appendChild(saveLink);
}

if (this.transitionable("answered")) {
Expand Down Expand Up @@ -174,6 +183,10 @@ export abstract class Annotation {
return this.text;
}

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

public get removable(): boolean {
return false;
}
Expand Down
65 changes: 49 additions & 16 deletions app/assets/javascripts/code_listing/code_listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
UserAnnotationFormData
} from "code_listing/user_annotation";
import { createUserAnnotation, getAllUserAnnotations } from "code_listing/question_annotation";
import "components/saved_annotations/saved_annotation_input";
import { SavedAnnotationInput } from "components/saved_annotations/saved_annotation_input";

const annotationGlobalAdd = "#add_global_annotation";
const annotationsGlobal = "#feedback-table-global-annotations";
Expand Down Expand Up @@ -40,6 +42,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 +61,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 +384,47 @@ 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 btn-text annotation-control-button annotation-delete-button" type="button">
${I18n.t("js.user_annotation.delete")}
</button>
` : ""}
<button class="btn btn-text annotation-control-button annotation-cancel-button" type="button">
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-filled 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 ? "" : `
<d-saved-annotation-input
name="saved_annotation_id"
course-id="${this.courseId}"
exercise-id="${this.exerciseId}"
user-id="${this.userId}"
class="saved-annotation-input annotation-submission-button-container"
value="${annotation?.savedAnnotationId || ""}"
title="${I18n.t("js.saved_annotation.input.title")}"
annotation-text="${annotation?.rawText}"
></d-saved-annotation-input>
`}
<span class="annotation-submission-button-container">
${annotation && annotation.removable ? `
<button class="btn btn-text annotation-control-button annotation-delete-button" type="button">
${I18n.t("js.user_annotation.delete")}
</button>
` : ""}
<button class="btn btn-text annotation-control-button annotation-cancel-button" type="button">
${I18n.t("js.user_annotation.cancel")}
</button>
<button class="btn btn-filled 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 savedAnnotationInput = form.querySelector<SavedAnnotationInput>(".saved-annotation-input");

savedAnnotationInput?.addEventListener("input", (e: CustomEvent) => {
if (e.detail.text) {
inputField.value = e.detail.text;
savedAnnotationInput.annotationText = inputField.value;
}
});

if (annotation !== null) {
inputField.rows = annotation.rawText.split("\n").length + 1;
Expand All @@ -407,6 +437,9 @@ export class CodeListing {
// Update value while typing.
inputField.addEventListener("input", () => {
usedCharacters.innerHTML = I18n.toNumber(inputField.value.length, { precision: 0 });
if (savedAnnotationInput) {
savedAnnotationInput.annotationText = inputField.value;
}
});

// Cancellation handler.
Expand Down Expand Up @@ -464,7 +497,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": new FormData(form).get("saved_annotation_id") as string || undefined,
};

try {
Expand Down Expand Up @@ -494,8 +528,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": new FormData(form).get("saved_annotation_id") as string || undefined
};

try {
Expand Down
14 changes: 12 additions & 2 deletions app/assets/javascripts/code_listing/user_annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export interface UserAnnotationFormData {
// eslint-disable-next-line camelcase
annotation_text: string;
// eslint-disable-next-line camelcase
line_nr: number | null;
line_nr?: number | null;
// eslint-disable-next-line camelcase
evaluation_id: number | undefined;
evaluation_id?: number | undefined;
// eslint-disable-next-line camelcase
saved_annotation_id?: string;
}

export type UserAnnotationEditor = (ua: UserAnnotation, cb: CallableFunction) => HTMLElement;
Expand Down Expand Up @@ -35,6 +37,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 +55,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 +71,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 @@ -99,6 +105,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
23 changes: 18 additions & 5 deletions app/assets/javascripts/components/datalist_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ import { ref, Ref, createRef } from "lit/directives/ref.js";
* @element d-datalist-input
*
* @prop {String} name - name of the input field (used in form submit)
* @prop {[{label: string, value: string}]} options - The label is used to match the user input, while the value is sent to the server.
* @prop {[{label: string, value: string, extra?: string}]} options - The label is used to match the user input, while the value is sent to the server.
* If the user input does not match any label, the value sent to the server wil be ""
* 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
*
* @fires input - on value change, event details contain {label: string, value: string}
*/
@customElement("d-datalist-input")
export class DatalistInput extends ShadowlessLitElement {
@property({ type: String })
name: string;
@property({ type: Array })
options: [{label: string, value: string}];
options: [{label: string, value: string, extra?: string}];
@property({ type: String })
value: string;
@property({ type: String })
placeholder: string;

inputRef: Ref<HTMLInputElement> = createRef();
hiddenInputRef: Ref<HTMLInputElement> = createRef();
Expand All @@ -30,16 +36,23 @@ export class DatalistInput extends ShadowlessLitElement {
return option?.label;
}

processInput(): void {
processInput(e): void {
const option = this.options.find(option => option.label === this.inputRef.value.value);
this.hiddenInputRef.value.value = option ? option.value : "";
const event = new CustomEvent("input", {
detail: { value: this.hiddenInputRef.value.value, label: this.inputRef.value.value },
bubbles: true,
composed: true }
);
this.dispatchEvent(event);
e.stopPropagation();
}

render(): TemplateResult {
return html`
<input class="form-control" type="text" list="${this.name}-datalist-hidden" ${ref(this.inputRef)} @input=${() => this.processInput()} value="${this.label}">
<input class="form-control" type="text" list="${this.name}-datalist-hidden" ${ref(this.inputRef)} @input=${e => this.processInput(e)} value="${this.label}" placeholder="${this.placeholder}" autocomplete="off">
<datalist id="${this.name}-datalist-hidden">
${this.options.map(option => html`<option value="${option.label}">${option.label}</option>`)}
${this.options.map(option => html`<option value="${option.label}">${option.label}${option.extra ? ": " + option.extra : ""}</option>`)}
</datalist>
<input type="hidden" name="${this.name}" ${ref(this.hiddenInputRef)} value="${this.value}">
`;
Expand Down
83 changes: 83 additions & 0 deletions app/assets/javascripts/components/modal_mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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";

/**
* This mixin adds support for rendering bootstrap modals within a webcomponent.
* This is implemented as a mixin instead of a separate component because bootstrap modals have issues rendering when nested within other html components.
*
* In practice it adds a single modal to `#modal-container`.
* Upon calls of `showModal` the `filledModalTemplate` gets rendered and displayed
*/
export declare abstract class ModalMixinInterface {
modalTemplate(title: TemplateResult, body: TemplateResult, footer: TemplateResult): TemplateResult;
/**
* Should be generated using `modalTemplate`
*/
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;

// On each update of the component, the modal template is also rerendered, making the modal content responsive
// This can cause unexpected behaviour if update is triggered on a modal component which is different from the currently active modalcomponent
// (As there is only one real html modal, the wrong `filledModalTemplate` will be displayed)
// This could be solved by tracking which modalcomponent is currently active
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;
}
Loading