Skip to content

Commit

Permalink
Merge pull request #16209 from martenson/more-pages
Browse files Browse the repository at this point in the history
bring grids for (published) pages on par with workflows
  • Loading branch information
jdavcs committed Jun 21, 2023
2 parents df36f65 + 711eaac commit e12c799
Show file tree
Hide file tree
Showing 25 changed files with 1,391 additions and 108 deletions.
12 changes: 9 additions & 3 deletions client/src/components/Indices/IndexFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
title="Advanced Filtering Help"
:size="size"
@click="onHelp">
<icon icon="question" />
<font-awesome-icon icon="question" />
</b-button>
<b-button
v-b-tooltip.hover
aria-haspopup="true"
title="Clear Filters (esc)"
:size="size"
@click="onReset">
<icon icon="times" />
<font-awesome-icon icon="times" />
</b-button>
</b-input-group-append>
</b-input-group>
Expand All @@ -40,9 +40,14 @@
</template>

<script>
import { BInputGroup, BInputGroupAppend, BButton, BModal } from "bootstrap-vue";
import DebouncedInput from "components/DebouncedInput";
import { BInputGroup, BInputGroupAppend, BButton, BModal } from "bootstrap-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faTimes, faQuestion } from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
library.add(faTimes, faQuestion);
/**
* Component for the search/filter button on the top of Galaxy object index grids.
Expand All @@ -54,6 +59,7 @@ export default {
BInputGroupAppend,
BButton,
BModal,
FontAwesomeIcon,
},
props: {
id: {
Expand Down
167 changes: 167 additions & 0 deletions client/src/components/Page/PageDropdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { expect, jest } from "@jest/globals";

import { mount, shallowMount, createWrapper } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import { PiniaVuePlugin, createPinia } from "pinia";
import { createTestingPinia } from "@pinia/testing";
import { mockFetcher } from "@/schema/__mocks__";
import { useUserStore } from "@/stores/userStore";

import PageDropdown from "./PageDropdown.vue";

import "jest-location-mock";

jest.mock("@/schema");

const waitRAF = () => new Promise((resolve) => requestAnimationFrame(resolve));

const localVue = getLocalVue(true);
localVue.use(PiniaVuePlugin);

const PAGE_DATA_OWNED = {
id: "page1235",
title: "My Page Title",
description: "A description derived from an annotation.",
shared: false,
};

const PAGE_DATA_SHARED = {
id: "page1235",
title: "My Page Title",
description: "A description derived from an annotation.",
shared: true,
};

describe("PageDropdown.vue", () => {
let wrapper: any;

function pageOptions() {
return wrapper.findAll(".dropdown-menu .dropdown-item");
}

describe("navigation on owned pages", () => {
beforeEach(async () => {
const pinia = createPinia();
const propsData = {
root: "/rootprefix/",
page: PAGE_DATA_OWNED,
};
wrapper = shallowMount(PageDropdown, {
propsData,
localVue,
pinia: pinia,
});
const userStore = useUserStore();
userStore.currentUser = { email: "my@email", id: "1", tags_used: [] };
});

it("should show page title", async () => {
const titleWrapper = await wrapper.find(".page-title");
expect(titleWrapper.text()).toBe("My Page Title");
});

it("should decorate dropdown with page ID for automation", async () => {
const linkWrapper = await wrapper.find("[data-page-dropdown='page1235']");
expect(linkWrapper.exists()).toBeTruthy();
});

it("should have a 'Share' option", async () => {
expect(wrapper.find(".dropdown-menu .dropdown-item-share").exists()).toBeTruthy();
});

it("should provide 5 options", () => {
expect(pageOptions().length).toBe(5);
});
});

describe("navigation on shared pages", () => {
beforeEach(async () => {
const propsData = {
root: "/rootprefixshared/",
page: PAGE_DATA_SHARED,
};
wrapper = shallowMount(PageDropdown, {
propsData,
localVue,
pinia: createTestingPinia(),
});
});

it("should have the 'View' option", async () => {
expect(wrapper.find(".dropdown-menu .dropdown-item-view").exists()).toBeTruthy();
});

it("should have only single option", () => {
expect(pageOptions().length).toBe(1);
});
});

describe("clicking page deletion on owned page", () => {
const pinia = createPinia();

async function mountAndDelete() {
const propsData = {
root: "/rootprefixdelete/",
page: PAGE_DATA_OWNED,
};
wrapper = mount(PageDropdown, {
propsData,
localVue,
pinia: pinia,
stubs: {
transition: false,
},
});
const userStore = useUserStore();
userStore.currentUser = { email: "my@email", id: "1", tags_used: [] };
wrapper.find(".page-dropdown").trigger("click");
await wrapper.vm.$nextTick();
wrapper.find(".dropdown-item-delete").trigger("click");
// this is here because b-modal is lazy loading and portalling
// see https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js#L233
await wrapper.vm.$nextTick();
await waitRAF();
await wrapper.vm.$nextTick();
await waitRAF();
await wrapper.vm.$nextTick();
await waitRAF();
const foot: any = document.getElementById("delete-page-modal-page1235___BV_modal_footer_");
createWrapper(foot).find(".btn-primary").trigger("click");
await wrapper.vm.$nextTick();
await waitRAF();
await wrapper.vm.$nextTick();
await waitRAF();
}

afterEach(() => {
mockFetcher.clearMocks();
});

it("should fire deletion API request upon confirmation", async () => {
mockFetcher.path("/api/pages/{id}").method("delete").mock({ status: 204 });
await mountAndDelete();
const emitted = wrapper.emitted();
expect(emitted["onRemove"][0][0]).toEqual("page1235");
expect(emitted["onSuccess"]).toBeTruthy();
});

it("should not fire deletion API request if not confirmed", async () => {
await mountAndDelete();
const emitted = wrapper.emitted();
expect(emitted["onRemove"]).toBeFalsy();
expect(emitted["onSuccess"]).toBeFalsy();
});

it("should emit an error on API fail", async () => {
mockFetcher
.path("/api/pages/{id}")
.method("delete")
.mock(() => {
throw Error("mock error");
});
await mountAndDelete();
const emitted = wrapper.emitted();
expect(emitted["onError"]).toBeTruthy();
});
});
});
93 changes: 93 additions & 0 deletions client/src/components/Page/PageDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<script setup lang="ts">
import _l from "@/utils/localization";
import { computed, unref } from "vue";
import { deletePage } from "./services";
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/userStore";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
library.add(faCaretDown);
interface Page {
id: string;
shared?: Boolean;
title?: string;
description?: string;
}
interface PageDropdownProps {
page: Page;
root: string;
published?: Boolean;
}
const props = defineProps<PageDropdownProps>();
const { isAnonymous } = storeToRefs(useUserStore());
const emit = defineEmits(["onRemove", "onSuccess", "onError"]);
const urlEdit = computed(() => `${props.root}pages/editor?id=${props.page.id}`);
const urlEditAttributes = computed(() => `${props.root}pages/edit?id=${props.page.id}`);
const urlShare = computed(() => `${props.root}pages/sharing?id=${props.page.id}`);
const urlView = computed(() => `${props.root}published/page?id=${props.page.id}`);
const readOnly = computed(() => props.page.shared || props.published || unref(isAnonymous));
function onDelete(page_id: string) {
deletePage(page_id)
.then((response) => {
emit("onRemove", page_id);
emit("onSuccess");
})
.catch((error) => {
emit("onError", error);
});
}
</script>
<template>
<div>
<b-link
class="page-dropdown"
data-toggle="dropdown"
aria-haspopup="true"
:data-page-dropdown="props.page.id"
aria-expanded="false">
<font-awesome-icon icon="caret-down" class="fa-lg" />
<span class="page-title">{{ props.page.title }}</span>
</b-link>
<p v-if="props.page.description">{{ props.page.description }}</p>
<div class="dropdown-menu" aria-labelledby="page-dropdown">
<a class="dropdown-item dropdown-item-view" :href="urlView">
<span class="fa fa-eye fa-fw mr-1" />
<span>View</span>
</a>
<a v-if="!readOnly" class="dropdown-item dropdown-item-edit" :href="urlEdit">
<span class="fa fa-edit fa-fw mr-1" />
<span>Edit content</span>
</a>
<a v-if="!readOnly" class="dropdown-item dropdown-item-attributes" :href="urlEditAttributes">
<span class="fa fa-pencil fa-fw mr-1" />
<span>Edit attributes</span>
</a>
<a v-if="!readOnly" class="dropdown-item dropdown-item-share" :href="urlShare">
<span class="fa fa-share-alt fa-fw mr-1" />
<span>Control sharing</span>
</a>
<a
v-if="!readOnly"
v-b-modal="`delete-page-modal-${props.page.id}`"
class="dropdown-item dropdown-item-delete">
<span class="fa fa-trash fa-fw mr-1" />
<span>Delete</span>
</a>
<b-modal
:id="`delete-page-modal-${props.page.id}`"
hide-backdrop
title="Confirm page deletion"
title-tag="h2"
@ok="onDelete(props.page.id)">
<p v-localize>Really delete the page titled: "{{ props.page.title }}"?</p>
</b-modal>
</div>
</div>
</template>
31 changes: 31 additions & 0 deletions client/src/components/Page/PageIndexActions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import PageIndexActions from "./PageIndexActions.vue";
import { shallowMount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";

import "jest-location-mock";

const localVue = getLocalVue();

describe("PageIndexActions.vue", () => {
let wrapper: any;
const mockRouter = {
push: jest.fn(),
};

beforeEach(async () => {
wrapper = shallowMount(PageIndexActions, {
mocks: {
$router: mockRouter,
},
localVue,
});
});

describe("navigation", () => {
it("should create a page when create is clicked", async () => {
await wrapper.find("#page-create").trigger("click");
expect(mockRouter.push).toHaveBeenCalledTimes(1);
expect(mockRouter.push).toHaveBeenCalledWith("/pages/create");
});
});
});
23 changes: 23 additions & 0 deletions client/src/components/Page/PageIndexActions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { BButton } from "bootstrap-vue";
import { useRouter } from "vue-router/composables";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
const router = useRouter();
library.add(faPlus);
function create() {
router.push("/pages/create");
}
</script>
<template>
<span>
<b-button id="page-create" class="m-1" @click="create">
<font-awesome-icon icon="plus" />
{{ "Create" | localize }}
</b-button>
</span>
</template>
Loading

0 comments on commit e12c799

Please sign in to comment.