Merge pull request #10754 from guerler/history_clipboard

Revision of sharing component, adds url copy to clipboard feature
This commit is contained in:
Dannon
2020-11-20 09:26:28 -05:00
committed by GitHub
3 changed files with 209 additions and 214 deletions
@@ -0,0 +1,44 @@
<template>
<b-input
v-model="slugInput"
@change="onChange"
@keydown.enter="onChange"
@keydown.esc="onCancel"
class="d-inline w-auto h-auto px-1 py-0"
ref="input"
/>
</template>
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
export default {
props: {
slug: {
type: String,
},
},
data() {
return {
slugInput: this.slug,
};
},
mounted() {
this.$refs.input.select();
},
methods: {
onChange() {
const slugFormatted = this.slugInput
.replace(/\s+/g, "-")
.replace(/[^a-zA-Z0-9-]/g, "")
.toLowerCase();
this.$emit("onChange", slugFormatted);
},
onCancel() {
this.$emit("onChange", this.slug);
},
},
};
</script>
+158 -207
View File
@@ -1,120 +1,62 @@
<template>
<div v-if="ready">
<h2>Share or Publish {{ model_class }} `{{ item.title }}`</h2>
<b-alert :show="show_danger" variant="danger" dismissible> {{ err_msg }} </b-alert>
<h3>Share or Publish {{ model_class }} `{{ item.title }}`</h3>
<b-alert :show="showDanger" variant="danger" dismissible> {{ errMsg }} </b-alert>
<br />
<div v-if="!has_username">
<div>
To make a {{ model_class_lc }} accessible via link or publish it, you must create a public username:
</div>
<div v-if="!hasUsername">
<div>To make a {{ model_class }} accessible via link or publish it, you must create a public username:</div>
<form class="form-group" @submit.prevent="setUsername()">
<label /> <input class="form-control" type="text" v-model="new_username" />
<input class="form-control" type="text" v-model="newUsername" />
</form>
<b-button type="submit" variant="primary" @click="setUsername()">Set Username</b-button>
</div>
<div v-else>
<b-form-checkbox switch v-model="item.importable" @change="onImportable">
Make {{ model_class }} accessible.
</b-form-checkbox>
<b-form-checkbox v-if="item.importable" switch v-model="item.published" @change="onPublish">
Make {{ model_class }} publicly available in
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section.
</b-form-checkbox>
<br />
<h3>Make {{ model_class }} Accessible via Link and Publish It</h3>
<div v-if="item.importable">
This {{ model_class_lc }} is currently <strong>{{ item_status }}</strong
>.
<div>
<p>Anyone can view and import this {{ model_class_lc }} by visiting the following URL:</p>
<div>
<div v-if="item.importable">
<div>
This {{ model_class }} is currently <strong>{{ itemStatus }}</strong
>.
</div>
<p>Anyone can view and import this {{ model_class }} by visiting the following URL:</p>
<blockquote>
<a id="item-url" :href="item_url" target="_top">{{ item_url }}</a>
<span id="item-url-text" style="display: none;">
{{ item_url_parts[0] }}<span id="item-identifier">{{ item_url_parts[1] }}</span>
<b-button title="Edit URL" @click="onEdit" v-b-tooltip.hover variant="link" size="sm">
<font-awesome-icon icon="edit" />
</b-button>
<b-button id="tooltip-clipboard" @click="onCopy" @mouseout="onCopyOut" variant="link" size="sm">
<font-awesome-icon icon="link" />
</b-button>
<b-tooltip target="tooltip-clipboard" triggers="hover">
{{ tooltipClipboard }}
</b-tooltip>
<a v-if="showUrl" id="item-url" :href="itemUrl" target="_top" class="ml-2">
{{ itemUrl }}
</a>
<span v-else id="item-url-text">
{{ itemUrlParts[0] }}<SlugInput class="ml-1" :slug="itemUrlParts[1]" @onChange="onChange" />
</span>
<a href="javascript:void(0)" id="edit-identifier"
><img :src="pencil_url" alt="Edit Share Url"
/></a>
</blockquote>
<div v-if="item.published">
<p>
This {{ model_class_lc }} is publicly listed and searchable in Galaxy's
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section.
</p>
<p>You can:</p>
</div>
</div>
<div v-if="!item.published">
<!-- Item is importable but not published. User can disable importable or publish. -->
<b-button @click="setSharing('disable_link_access')"
>Disable Access to {{ model_class }} Link</b-button
>
<div class="toolParamHelp">Disables {{ model_class_lc }}'s link so that it is not accessible.</div>
<br />
<b-button id="make_accessible_and_publish" @click="setSharing('publish')"
>Publish {{ model_class }}</b-button
>
<div class="toolParamHelp">
Publishes the {{ model_class_lc }} to Galaxy's
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section, where it is
publicly listed and searchable.
</div>
<br />
</div>
<div v-else>
<!-- Item is importable and published. User can unpublish or disable import and unpublish. -->
<b-button
id="disable_link_access_and_unpublish"
@click="setSharing('disable_link_access_and_unpublish')"
>Disable Access to {{ model_class }} via Link and Unpublish</b-button
>
<div class="toolParamHelp">
Disables this {{ model_class_lc }}'s link so that it is not accessible and removes
{{ model_class_lc }} from Galaxy's
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section so that it is not
publicly listed or searchable.
</div>
<br />
<b-button @click="setSharing('unpublish')">Unpublish {{ model_class }}</b-button>
<div class="toolParamHelp">
Removes this {{ model_class_lc }} from Galaxy's
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section so that it is not
publicly listed or searchable.
</div>
Access to this {{ model_class }} is currently restricted so that only you and the users listed below
can access it. Note that sharing a History will also allow access to all of its datasets.
</div>
</div>
<div v-else>
<p>
This {{ model_class_lc }} is currently restricted so that only you and the users listed below can
access it. You can:
</p>
<b-button @click="setSharing('make_accessible_via_link')"
>Make {{ model_class }} Accessible via Link</b-button
>
<p v-if="has_possible_members" class="mt-2">
Also make all objects within the {{ model_class }} accessible.
<input type="checkbox" v-model="make_members_public" id="chk_make_members_public" />
</p>
<div class="toolParamHelp">
Generates a web link that you can share with other people so that they can view and import the
{{ model_class_lc }}.
</div>
<br />
<b-button id="make_accessible_and_publish" @click="setSharing('make_accessible_and_publish')"
>Make {{ model_class }} Accessible and Publish</b-button
>
<p v-if="has_possible_members" class="mt-2">
Also make all objects within the {{ model_class }} accessible.
<input type="checkbox" v-model="make_members_public" id="chk_make_members_public" />
</p>
<div class="toolParamHelp">
Makes the {{ model_class_lc }} accessible via link (see above) and publishes the
{{ model_class_lc }} to Galaxy's
<a :href="published_url" target="_top">Published {{ plural_name }}</a> section, where it is publicly
listed and searchable.
</div>
</div>
<br /><br />
<h3>Share {{ model_class }} with Individual Users</h3>
<br />
<h4>Share {{ model_class }} with Individual Users</h4>
<div>
<div v-if="item.users_shared_with && item.users_shared_with.length > 0">
<b-table small caption-top :fields="share_fields" :items="item.users_shared_with">
<b-table small caption-top :fields="shareFields" :items="item.users_shared_with">
<template v-slot:table-caption>
The following users will see this {{ model_class_lc }} in their {{ model_class_lc }} list
and will be able to view, import and run it.
The following users will see this {{ model_class }} in their {{ model_class }} list and will
be able to view, import and run it.
</template>
<template v-slot:cell(id)="cell">
<b-button
@@ -127,26 +69,35 @@
</b-table>
</div>
<div v-else>
<p>You have not shared this {{ model_class_lc }} with any users.</p>
<p>You have not shared this {{ model_class }} with any users.</p>
</div>
<b-button :href="share_url" id="share_with_a_user"> <span>Share with a user</span> </b-button>
<b-button :href="shareUrl" id="share_with_a_user"> <span>Share with a user</span> </b-button>
</div>
</div>
</div>
</template>
<script>
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import axios from "axios";
import async_save_text from "utils/async-save-text";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faLink, faEdit } from "@fortawesome/free-solid-svg-icons";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import SlugInput from "components/Common/SlugInput";
import axios from "axios";
Vue.use(BootstrapVue);
library.add(faLink);
library.add(faEdit);
export default {
components: {
FontAwesomeIcon,
SlugInput,
},
props: {
id: {
type: String,
@@ -161,51 +112,13 @@ export default {
required: true,
},
},
computed: {
model_class_lc() {
return this.model_class.toLowerCase();
},
plural_name_lc() {
return this.plural_name.toLowerCase();
},
item_status() {
return this.item.published ? "accessible via link and published" : "accessible via link";
},
item_url() {
const port = window.location.port ? `:${window.location.port}` : "";
return `${window.location.protocol}//${window.location.hostname}${port}${getAppRoot()}${
this.item.username_and_slug
}`;
},
item_url_parts() {
const str = this.item_url;
const index = str.lastIndexOf("/");
return [str.substring(0, index + 1), str.substring(index + 1)];
},
published_url() {
return `${getAppRoot()}${this.plural_name_lc}/list_published`;
},
share_url() {
return `${getAppRoot()}${this.model_class_lc}/share/?id=${this.id}`;
},
slug_url() {
return `${getAppRoot()}${this.model_class_lc}/set_slug_async/?id=${this.id}`;
},
has_possible_members() {
return ["history"].indexOf(this.model_class_lc) > -1;
},
show_danger() {
return this.err_msg !== null;
},
},
data() {
const Galaxy = getGalaxyInstance();
return {
ready: false,
has_username: Galaxy.user.get("username"),
new_username: "",
err_msg: null,
pencil_url: `${getAppRoot()}static/images/fugue/pencil.png`,
hasUsername: Galaxy.user.get("username"),
newUsername: "",
errMsg: null,
item: {
title: "title",
username_and_slug: "username_and_slug",
@@ -213,97 +126,135 @@ export default {
published: false,
users_shared_with: [],
},
share_fields: ["email", { key: "id", label: "" }],
make_members_public: false,
shareFields: ["email", { key: "id", label: "" }],
makeMembersPublic: false,
showUrl: true,
tooltipClipboard: "Copy URL",
};
},
computed: {
modelClassLower() {
return this.model_class.toLowerCase();
},
pluralNameLower() {
return this.plural_name.toLowerCase();
},
itemStatus() {
return this.item.published ? "accessible via link and published" : "accessible via link";
},
itemRoot() {
const port = window.location.port ? `:${window.location.port}` : "";
return `${window.location.protocol}//${window.location.hostname}${port}${getAppRoot()}`;
},
itemUrl() {
return `${this.itemRoot}${this.item.username_and_slug}`;
},
itemSlugParts() {
const str = this.item.username_and_slug;
const index = str.lastIndexOf("/");
return [str.substring(0, index + 1), str.substring(index + 1)];
},
itemUrlParts() {
const str = this.itemUrl;
const index = str.lastIndexOf("/");
return [str.substring(0, index + 1), str.substring(index + 1)];
},
published_url() {
return `${getAppRoot()}${this.pluralNameLower}/list_published`;
},
shareUrl() {
return `${getAppRoot()}${this.modelClassLower}/share/?id=${this.id}`;
},
slugUrl() {
return `${getAppRoot()}${this.modelClassLower}/set_slug_async/?id=${this.id}`;
},
showDanger() {
return this.errMsg !== null;
},
},
created: function () {
this.getModel();
},
updated: function () {
this.createSlugHandler();
},
methods: {
getModel: function () {
onCopy() {
const clipboard = document.createElement("input");
document.body.appendChild(clipboard);
clipboard.value = this.itemUrl;
clipboard.select();
document.execCommand("copy");
document.body.removeChild(clipboard);
this.tooltipClipboard = "Copied!";
},
onCopyOut() {
this.tooltipClipboard = "Copy URL";
},
onEdit() {
this.showUrl = false;
},
onChange(newSlug) {
this.showUrl = true;
this.item.username_and_slug = `${this.itemSlugParts[0]}${newSlug}`;
const requestUrl = `${this.slugUrl}&new_slug=${newSlug}`;
axios.get(requestUrl).catch((error) => (this.errMsg = error.response.data.err_msg));
},
onImportable(importable) {
if (importable) {
this.setSharing("make_accessible_via_link");
if (this.item.published) {
this.setSharing("publish");
} else {
this.setSharing("unpublish");
}
} else {
this.item.published = false;
this.setSharing("disable_link_access");
this.setSharing("unpublish");
}
},
onPublish(published) {
if (published) {
this.item.importable = true;
this.setSharing("make_accessible_and_publish");
} else {
this.setSharing("unpublish");
}
},
getModel() {
this.ready = false;
axios
.get(`${getAppRoot()}api/${this.plural_name_lc}/${this.id}/sharing`)
.get(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/sharing`)
.then((response) => {
this.item = response.data;
this.ready = true;
})
.catch((error) => (this.err_msg = error.response.data.err_msg));
.catch((error) => (this.errMsg = error.response.data.err_msg));
},
setUsername: function () {
setUsername() {
const Galaxy = getGalaxyInstance();
axios
.put(`${getAppRoot()}api/users/${Galaxy.user.id}/information/inputs`, {
username: this.new_username || "",
username: this.newUsername || "",
})
.then((response) => {
this.err_msg = null;
this.has_username = true;
this.errMsg = null;
this.hasUsername = true;
this.getModel();
})
.catch((error) => (this.err_msg = error.response.data.err_msg));
.catch((error) => (this.errMsg = error.response.data.err_msg));
},
setSharing: function (action, user_id) {
setSharing(action, user_id) {
const data = {
action: action,
user_id: user_id,
};
if (this.has_possible_members) {
data.make_members_public = this.make_members_public;
}
axios
.post(`${getAppRoot()}api/${this.plural_name_lc}/${this.id}/sharing`, data)
.post(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/sharing`, data)
.then((response) => {
Object.assign(this.item, response.data);
if (response.data.skipped) {
this.err_msg = "Some of the items within this object were not published due to an error.";
this.errMsg = "Some of the items within this object were not published due to an error.";
}
})
.catch((error) => (this.err_msg = error.response.data.err_msg));
},
createSlugHandler: function () {
const on_start = function (text_elt) {
// Replace URL with URL text.
$("#item-url").hide();
$("#item-url-text").show();
// Allow only lowercase alphanumeric and '-' characters in slug.
text_elt.keyup(function () {
text_elt.val(
$(this)
.val()
.replace(/\s+/g, "-")
.replace(/[^a-zA-Z0-9-]/g, "")
.toLowerCase()
);
});
};
const on_finish = function (text_elt) {
// Replace URL text with URL.
$("#item-url-text").hide();
$("#item-url").show();
// Set URL to new value.
const new_url = $("#item-url-text").text();
const item_url_obj = $("#item-url");
item_url_obj.attr("href", new_url);
item_url_obj.text(new_url);
};
async_save_text(
"edit-identifier",
"item-identifier",
this.slug_url,
"new_slug",
null,
false,
0,
on_start,
on_finish
);
.catch((error) => (this.errMsg = error.response.data.err_msg));
},
},
};
+7 -7
View File
@@ -1424,18 +1424,18 @@ class SharableMixin:
action = payload.get("action")
if action == "make_accessible_via_link":
self._make_item_accessible(trans.sa_session, item)
if hasattr(item, "has_possible_members") and item.has_possible_members and payload.get("make_members_public", False):
shared, skipped = self._make_members_public(trans, item)
if hasattr(item, "has_possible_members") and item.has_possible_members:
skipped = self._make_members_public(trans, item)
elif action == "make_accessible_and_publish":
self._make_item_accessible(trans.sa_session, item)
if hasattr(item, "has_possible_members") and item.has_possible_members and payload.get("make_members_public", False):
shared, skipped = self._make_members_public(trans, item)
if hasattr(item, "has_possible_members") and item.has_possible_members:
skipped = self._make_members_public(trans, item)
item.published = True
elif action == "publish":
if item.importable:
item.published = True
if hasattr(item, "has_possible_members") and item.has_possible_members and payload.get("make_members_public", False):
shared, skipped = self._make_members_public(trans, item)
if hasattr(item, "has_possible_members") and item.has_possible_members:
skipped = self._make_members_public(trans, item)
else:
raise exceptions.MessageException("%s not importable." % class_name)
elif action == "disable_link_access":
@@ -1482,7 +1482,7 @@ class SharableMixin:
else:
log.warning("User without permissions tried to make dataset with id: %s public", dataset.id)
skipped = True
return item, skipped
return skipped
# -- Abstract methods. --