From 0cf955bd2f45ad0eef446cf6b3a9d3b8e73e8109 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick <inga.kirschnick@uni-bielefeld.de> Date: Fri, 12 May 2023 13:43:38 +0200 Subject: [PATCH] CorpusFile selection+restore public_corpus page --- app/corpora/routes.py | 32 +- app/static/js/ResourceLists/CorpusFileList.js | 168 +++++++- app/templates/_scripts.html.j2 | 1 - app/templates/corpora/public_corpus.html.j2 | 379 ++++++++++++++++++ 4 files changed, 569 insertions(+), 11 deletions(-) create mode 100644 app/templates/corpora/public_corpus.html.j2 diff --git a/app/corpora/routes.py b/app/corpora/routes.py index a81734f8..25c5228f 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -54,6 +54,7 @@ def corpus(corpus_id): # TODO: Better solution for filtering admin users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all() cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first() + cfas = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id).all() if cfa is None: if corpus.user == current_user or current_user.is_administrator(): cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first() @@ -61,14 +62,29 @@ def corpus(corpus_id): cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first() else: cfr = cfa.role - return render_template( - 'corpora/corpus.html.j2', - title=corpus.title, - corpus=corpus, - cfrs=cfrs, - cfr=cfr, - users = users - ) + if corpus.user == current_user or current_user.is_administrator(): + return render_template( + 'corpora/corpus.html.j2', + title=corpus.title, + corpus=corpus, + cfr=cfr, + cfrs=cfrs, + users = users + ) + if (current_user.is_following_corpus(corpus) or corpus.is_public): + cfas = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id).all() + return render_template( + 'corpora/public_corpus.html.j2', + title=corpus.title, + corpus=corpus, + cfrs=cfrs, + cfr=cfr, + cfas=cfas, + cfa=cfa, + users = users + ) + abort(403) + @bp.route('/<hashid:corpus_id>/analysis') diff --git a/app/static/js/ResourceLists/CorpusFileList.js b/app/static/js/ResourceLists/CorpusFileList.js index 813676fb..ac87e8e5 100644 --- a/app/static/js/ResourceLists/CorpusFileList.js +++ b/app/static/js/ResourceLists/CorpusFileList.js @@ -8,7 +8,11 @@ class CorpusFileList extends ResourceList { constructor(listContainerElement, options = {}) { super(listContainerElement, options); this.listjs.list.addEventListener('click', (event) => {this.onClick(event)}); + document.querySelectorAll('.selection-action-trigger[data-selection-action]').forEach((element) => { + element.addEventListener('click', (event) => {this.onSelectionAction(event)}); + }); this.isInitialized = false; + this.selectedItemIds = []; this.userId = listContainerElement.dataset.userId; this.corpusId = listContainerElement.dataset.corpusId; this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false; @@ -29,6 +33,12 @@ class CorpusFileList extends ResourceList { return (values) => { return ` <tr class="list-item"> + <td> + <label class="list-action-trigger ${this.hasPermissionView ? '' : 'hide'}" data-list-action="select"> + <input type="checkbox"> + <span class="disable-on-click"></span> + </label> + </td> <td><span class="filename"></span></td> <td><span class="author"></span></td> <td><span class="title"></span></td> @@ -68,11 +78,20 @@ class CorpusFileList extends ResourceList { <table> <thead> <tr> + <th> + <label class="selection-action-trigger ${this.listContainerElement.dataset?.hasPermissionView == 'true' ? '' : 'hide'}" data-selection-action="select-all"> + <input type="checkbox"> + <span></span> + </label> + </th> <th>Filename</th> <th>Author</th> <th>Title</th> <th>Publishing year</th> - <th></th> + <th class="right-align"> + <a class="selection-action-trigger btn-floating red waves-effect waves-light hide" data-selection-action="delete"><i class="material-icons">delete</i></a> + <a class="selection-action-trigger btn-floating service-color darken waves-effect waves-light hide" data-selection-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a> + </th> </tr> </thead> <tbody class="list"></tbody> @@ -97,11 +116,12 @@ class CorpusFileList extends ResourceList { } onClick(event) { + if (event.target.closest('.disable-on-click') !== null) {return;} let listItemElement = event.target.closest('.list-item[data-id]'); if (listItemElement === null) {return;} let itemId = listItemElement.dataset.id; let listActionElement = event.target.closest('.list-action-trigger[data-list-action]'); - let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; + let listAction = listActionElement === null ? '' : listActionElement.dataset.listAction; switch (listAction) { case 'delete': { let values = this.listjs.get('id', itemId)[0].values(); @@ -145,12 +165,156 @@ class CorpusFileList extends ResourceList { window.location.href = `/corpora/${this.corpusId}/files/${itemId}`; break; } + case 'select': { + if (event.target.checked) { + this.selectedItemIds.push(itemId); + } else { + let index = this.selectedItemIds.indexOf(itemId); + if (index > -1) { + this.selectedItemIds.splice(index, 1); + } + } + this.renderingItemSelection(); + } + default: { + break; + } + } + } + + onSelectionAction(event) { + let selectionActionElement = event.target.closest('.selection-action-trigger[data-selection-action]'); + let selectionAction = selectionActionElement.dataset.selectionAction; + let items = this.listjs.items; + let selectableItems = Array.from(items) + .filter(item => item.elm) + .map(item => item.elm.querySelector('input[type="checkbox"]')); + + switch (selectionAction) { + case 'select-all': { + let selectedIds = Array.from(items) + .map(item => item.values().id); + if (event.target.checked) { + selectableItems.forEach(selectableItem => selectableItem.checked = true); + this.selectedItemIds = selectedIds; + } else { + selectableItems.forEach(checkbox => checkbox.checked = false); + this.selectedItemIds = this.selectedItemIds.filter(id => !selectedIds.includes(id)); + } + this.renderingItemSelection(); + break; + } + case 'delete': { + let modalElement = Utils.HTMLToElement( + ` + <div class="modal"> + <div class="modal-content"> + <h4>Confirm Corpus File deletion</h4> + <p>Do you really want to delete the Corpus Files?</p> + <ul id="selected-items-list"></ul> + <p>All files will be permanently deleted!</p> + </div> + <div class="modal-footer"> + <a class="btn modal-close waves-effect waves-light">Cancel</a> + <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> + </div> + </div> + ` + ); + document.querySelector('#modals').appendChild(modalElement); + let itemList = document.querySelector('#selected-items-list'); + this.selectedItemIds.forEach(selectedItemId => { + let listItem = this.listjs.get('id', selectedItemId)[0].elm; + let values = this.listjs.get('id', listItem.dataset.id)[0].values(); + let itemElement = Utils.HTMLToElement(`<li> - ${values.title}</li>`); + itemList.appendChild(itemElement); + }); + let modal = M.Modal.init( + modalElement, + { + dismissible: false, + onCloseEnd: () => { + modal.destroy(); + modalElement.remove(); + } + } + ); + let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); + confirmElement.addEventListener('click', (event) => { + this.selectedItemIds.forEach(selectedItemId => { + Requests.corpora.entity.files.ent.delete(this.corpusId, selectedItemId); + }); + this.selectedItemIds = []; + this.renderingItemSelection(); + }); + modal.open(); + break; + } + case 'download': { + this.selectedItemIds.forEach(selectedItemId => { + let downloadLink = document.createElement('a'); + downloadLink.href = `/corpora/${this.corpusId}/files/${selectedItemId}/download`; + downloadLink.download = ''; + downloadLink.click(); + }); + selectableItems.forEach(checkbox => checkbox.checked = false); + this.selectedItemIds = []; + this.renderingItemSelection(); + break; + } default: { break; } } } + renderingItemSelection() { + let selectionActionButtons; + if (this.hasPermissionManageFiles) { + selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"])'); + } else if (this.hasPermissionView) { + selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"]):not([data-selection-action="delete"])'); + } + let selectableItems = this.listjs.items; + let actionButtons = []; + + Object.values(selectableItems).forEach(selectableItem => { + if (selectableItem.elm) { + let checkbox = selectableItem.elm.querySelector('input[type="checkbox"]'); + if (checkbox.checked) { + selectableItem.elm.classList.add('grey', 'lighten-3'); + } else { + selectableItem.elm.classList.remove('grey', 'lighten-3'); + } + let itemActionButtons = []; + if (this.hasPermissionManageFiles) { + itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"])'); + } else if (this.hasPermissionView) { + itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"]):not([data-list-action="delete"]):not([data-list-action="view"])'); + } + itemActionButtons.forEach(itemActionButton => { + actionButtons.push(itemActionButton); + }); + } + }); + // Hide item action buttons if > 0 item is selected and show selection action buttons + if (this.selectedItemIds.length > 0) { + selectionActionButtons.forEach(selectionActionButton => { + selectionActionButton.classList.remove('hide'); + }); + actionButtons.forEach(actionButton => { + actionButton.classList.add('hide'); + }); + } else { + selectionActionButtons.forEach(selectionActionButton => { + selectionActionButton.classList.add('hide'); + }); + actionButtons.forEach(actionButton => { + actionButton.classList.remove('hide'); + }); + } + } + onPatch(patch) { let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`); let filteredPatch = patch.filter(operation => re.test(operation.path)); diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 07d2d59a..bee5b7b2 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -40,7 +40,6 @@ 'js/ResourceLists/ResourceList.js', 'js/ResourceLists/CorpusFileList.js', 'js/ResourceLists/CorpusList.js', - 'js/ResourceLists/FollowedCorpusList.js', 'js/ResourceLists/PublicCorpusList.js', 'js/ResourceLists/JobList.js', 'js/ResourceLists/JobInputList.js', diff --git a/app/templates/corpora/public_corpus.html.j2 b/app/templates/corpora/public_corpus.html.j2 new file mode 100644 index 00000000..fb13c252 --- /dev/null +++ b/app/templates/corpora/public_corpus.html.j2 @@ -0,0 +1,379 @@ +{% extends "base.html.j2" %} +{% import "materialize/wtf.html.j2" as wtf %} + +{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %} + +{% block page_content %} +<div class="container"> + <div class="row"> + <div class="col s12"> + <h1>{{ corpus.title }}</h1> + </div> + <div class="col s12 l7"> + <div class="card service-color-border border-darken" data-service="corpus-analysis" style="border-top: 10px solid"> + <div class="card-content"> + <span class="chip corpus-status-text corpus-status-color white-text" data-status="{{ corpus.status.name }}"></span></p> + <div class="row"> + + <div class="col s12"> + <div class="input-field"> + <label>Description</label> + <input disabled type="text" value="{{ corpus.description }}"> + </div> + </div> + + <div class="col s12 m6"> + <div class="input-field"> + <label for="corpus-creation-date">Creation date</label> + <input disabled type="text" value="{{ corpus.creation_date }}"> + </div> + </div> + + <div class="col s12 m6"> + <div class="input-field"> + <label for="corpus-token-ratio">Nr. of tokens used <sup><i class="material-icons tooltipped tiny" data-position="bottom" data-tooltip="Current number of tokens in this corpus. Updates after every analyze session.">help</i></sup></label> + <input disabled type="text" value="{{ corpus.num_tokens }}"> + </div> + </div> + </div> + </div> + </div> + </div> + + {% if cfr.has_permission('VIEW') %} + <div class="col s12 l5"> + <div class="card"> + <div class="card-content"> + <span class="card-title">Actions</span> + <div class="row"> + {% if cfr.has_permission('MANAGE_FILES') %} + <div class="col s12 l6" style="padding: 0 2.5px;"> + <a class="action-button btn disabled waves-effect waves-light" data-action="build-request" style="width: 100%;"><i class="nopaque-icons left">K</i>Build</a> + </div> + <div class="col s12 l6" style="padding: 0 2.5px;"> + {% if corpus.status.name in ['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'] and current_user.is_following_corpus(corpus) %} + <a class="action-button btn waves-effect waves-light" data-action="analyze" href="{{ url_for('corpora.analysis', corpus_id=corpus.id) }}" style="width: 100%;"><i class="material-icons left">search</i>Analyze</a> + {% else %} + <a class="action-button btn disabled waves-effect waves-light" data-action="analyze" style="width: 100%;"><i class="material-icons left">search</i>Analyze</a> + {% endif %} + </div> + {% endif %} + {% if current_user.is_following_corpus(corpus) %} + <div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;"> + <a class="action-button btn red waves-effect waves-light" data-action="unfollow-request" style="width: 100%;"><i class="material-icons left outlined">close</i>Unfollow Corpus</a> + </div> + {% endif %} + </div> + {% if cfr.has_permission('MANAGE_FOLLOWERS') %} + <span class="card-title">Social</span> + <div class="row"> + <div class="col s12 l6" style="padding: 0 2.5px;"> + <a class="btn waves-effect waves-light modal-trigger" href="#invite-user-modal" style="width: 100%;"><i class="material-icons left">person_add</i>invite user</a> + </div> + <div class="col s12 l6" style="padding: 0 2.5px;"> + <a class="btn waves-effect waves-light modal-trigger" href="#share-link-modal" style="width: 100%;"><i class="material-icons left">link</i>Share link</a> + </div> + </div> + {% endif %} + </div> + </div> + </div> + {% endif %} + + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <span class="card-title" id="files">Corpus Owner</span> + <div class="row"> + <div class="col s12"> + <table> + <tr> + <td style="width:10%; margin-top:25px;"> + <img src="{{ url_for('users.user_avatar', user_id=corpus.user.id) }}" alt="user-image" class="circle responsive-img"> + </td> + <td></td> + <td> + <ul> + <li><b>{{ corpus.user.username }}</b></li> + {% if corpus.user.full_name %} + <li>{{ corpus.user.full_name }}</li> + {% endif %} + {% if corpus.user.show_email %} + <li></li><a href="mailto:{{ corpus.user.email }}">{{ corpus.user.email }}</a></li> + {% endif %} + </ul> + </td> + </tr> + </table> + <br> + <p></p> + {% if not current_user.is_following_corpus(corpus) and corpus.user.has_profile_privacy_setting('SHOW_EMAIL') %} + <a class="waves-effect waves-light btn-small" href="mailto:{{ corpus.user.email }}">Request Corpus</a> + {% endif %} + <a class="waves-effect waves-light btn-small" href="{{ url_for('users.user', user_id=corpus.user.id) }}">View profile</a> + </div> + </div> + </div> + </div> + </div> + + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <span class="card-title" id="corpus-files">Corpus files</span> + <div class="corpus-file-list no-autoinit" id="corpus-file-list" data-has-permission-view="{{ cfr.has_permission('VIEW')|tojson }}" data-has-permission-manage-files="{{ cfr.has_permission('MANAGE_FILES')|tojson }}" data-corpus-id="{{ corpus.hashid }}"></div> + </div> + {% if cfr.has_permission('MANAGE_FILES') %} + <div class="card-action right-align"> + <a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> + </div> + {% endif %} + </div> + </div> + + {% if cfr.has_permission('MANAGE_FOLLOWERS') %} + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <span class="card-title" id="corpus-followers">Corpus followers</span> + <div class="corpus-follower-list no-autoinit"></div> + </div> + </div> + </div> + {% endif %} + + </div> +</div> +{% endblock page_content %} + +{% block modals %} +{{ super() }} + +{% if cfr.has_permission('MANAGE_FOLLOWERS') %} +<div class="modal no-autoinit" id="invite-user-modal"> + <div class="modal-content"> + <h4>Invite a nopaque user by username</h4> + <p> + Add other nopaque users as followers to your corpus. You can also add multiple + users at the same time. Added users get the role of "viewer" + by default, so they are only allowed to analyze files within nopaque, but not + to download or edit them. You can customize the roles later below. + </p> + <p><b>Please make sure that the invited users are legally allowed to view the included corpus files.</b></p> + <div class="row"> + <div class="col s10"> + <div class="chips no-autoinit" id="invite-user-modal-search"></div> + </div> + <div class="col s2"> + <br class="hide-on-med-and-down"> + <a class="btn modal-close waves-effect waves-light" id="invite-user-modal-invite-button">Invite<i class="material-icons right">send</i></a> + </div> + </div> + </div> + <div class="modal-footer"> + <a class="modal-close waves-effect waves-green btn-flat">Close</a> + </div> +</div> + +<div class="modal no-autoinit" id="share-link-modal"> + <div class="modal-content"> + <h4>Create a link to share your corpus</h4> + <p> + With the link other users follow your corpus directly, if it has not expired. + You can set different roles via the link, you can also edit them later in the menu below. + It is recommended not to set the expiration date of the link too far. + </p> + <p><b>Please make sure that the invited users are legally allowed to view the included corpus files.</b></p> + <div class="row"> + <div class="col s12 l2"> + <div class="input-field"> + <i class="material-icons prefix">badge</i> + <select id="share-link-modal-corpus-follower-role-select"> + {% for cfr in cfrs %} + <option value="{{ cfr.name }}">{{ cfr.name }}</option> + {% endfor %} + </select> + <label>Role</label> + </div> + </div> + <div class="col s12 l3"> + <div class="input-field"> + <i class="material-icons prefix">calendar_month</i> + <input type="text" class="datepicker no-autoinit" id="share-link-modal-expiration-date-datepicker"> + <label for="expiration-date">Expiration date</label> + </div> + </div> + <div class="col s12 l2"> + <br class="hide-on-med-and-down"> + <a class="btn waves-effect waves-light" id="share-link-modal-create-button">Create<i class="material-icons right">send</i></a> + </div> + + <div class="col s12 l5"> + <div class="row hide" id="share-link-modal-output-container"> + <div class="col s9"> + <div class="input-field"> + <input disabled id="share-link-modal-output-field" readonly type="text"> + </div> + </div> + <div class="col s3"> + <br class="hide-on-med-and-down"> + <a class="btn-small waves-effect waves-light" id="share-link-modal-output-copy-button"><i class="material-icons left">content_copy</i>Copy</a> + </div> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <a class="modal-close waves-effect waves-green btn-flat">Close</a> + </div> +</div> +{% endif %} + +{% endblock modals %} + +{% block scripts %} +{{ super() }} +<script> + +let publicCorpusFileList = new CorpusFileList(document.querySelector('#corpus-file-list')); +publicCorpusFileList.add( + [ + {% for corpus_file in corpus.files %} + {{ corpus_file.to_json_serializeable()|tojson }}, + {% endfor %} + ] +); + +{% if cfr.has_permission('MANAGE_FOLLOWERS') %} +let publicCorpusFollowerList = new CorpusFollowerList(document.querySelector('.corpus-follower-list')); +publicCorpusFollowerList.add( + [ + {% for cfa in cfas %} + {{ cfa.to_json_serializeable()|tojson }}, + {% endfor %} + ] +); +{% endif %} + +// #region Corpus Unfollow Request +{% if current_user.is_following_corpus(corpus) %} + let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]'); + unfollowRequestElement.addEventListener('click', () => { + Requests.corpora.entity.followers.entity.delete({{ corpus.hashid|tojson }}, {{ current_user.hashid|tojson }}) + .then((response) => { + window.location.reload(); + }); + }); +{% endif %} +// #endregion Corpus Unfollow Request + +{% if cfr.has_permission('MANAGE_FOLLOWERS') %} +// #region Invite user +let inviteUserModalElement = document.querySelector('#invite-user-modal'); +let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); +let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button'); +let users = { + {% for user in users %} + {{ user.username|tojson }}: {{ url_for('users.user_avatar', user_id=user.id)|tojson }} + {% if not loop.last %},{% endif %} + {% endfor %} +}; + +let inviteUserModalSearch = M.Chips.init( + inviteUserModalSearchElement, + { + autocompleteOptions: { + data: users + }, + limit: 3, + onChipAdd: (a, chipElement) => { + if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) { + chipElement.firstElementChild.click(); + } + }, + placeholder: 'Enter username', + secondaryPlaceholder: 'Add more users' + } +); + +M.Modal.init( + inviteUserModalElement, + { + onOpenStart: (modalElement, modalTriggerElement) => { + while (inviteUserModalSearch.chipsData.length > 0) { + inviteUserModalSearch.deleteChip(0); + } + } + } +) + +inviteUserModalInviteButtonElement.addEventListener('click', (event) => { + let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag); + Requests.corpora.entity.followers.add({{ corpus.hashid|tojson }}, usernames); +}); +// #endregion Invite user + +// #region Share link +let shareLinkModalElement = document.querySelector('#share-link-modal'); +let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select'); +let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker'); +let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button'); +let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container'); +let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field'); +let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button'); + +let today = new Date(); +let tomorrow = new Date(); +tomorrow.setDate(today.getDate() + 1); +let oneWeekLater = new Date(); +oneWeekLater.setDate(today.getDate() + 7); +let fourWeeksLater = new Date(); +fourWeeksLater.setDate(today.getDate() + 28); + +M.Datepicker.init( + shareLinkModalExpirationDateDatepickerElement, + { + container: document.querySelector('main'), + defaultDate: oneWeekLater, + setDefaultDate: true, + minDate: tomorrow, + maxDate: fourWeeksLater + } +); + +M.Modal.init( + shareLinkModalElement, + { + onOpenStart: (modalElement, modalTriggerElement) => { + shareLinkModalOutputFieldElement.value = ''; + shareLinkModalOutputContainerElement.classList.add('hide'); + } + } +) + +shareLinkModalCreateButtonElement.addEventListener('click', (event) => { + let role = shareLinkModalCorpusFollowerRoleSelectElement.value; + let expiration = shareLinkModalExpirationDateDatepickerElement.value + Requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration) + .then((response) => { + response.json() + .then((json) => { + shareLinkModalOutputContainerElement.classList.remove('hide'); + shareLinkModalOutputFieldElement.value = json.corpusShareLink; + }); + }); +}); + +shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { + navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) + .then( + () => {app.flash('Copied!');}, + () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} + ); + +}); +// #endregion Share link +{% endif %} + +</script> +{% endblock scripts %} -- GitLab