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

bring grids for (published) pages on par with workflows #16209

Merged
merged 29 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4a45579
add page dropdown for debug sharing
martenson Jun 3, 2023
625d872
change the test to not directly access comp function
martenson Jun 3, 2023
68ca49c
standardize indexfilter icon handling
martenson Jun 5, 2023
ab7daa5
add new component for page index actions
martenson Jun 5, 2023
90f8ad2
standardize icon handling in pagedropdown
martenson Jun 6, 2023
d189b21
finish client changes to the page grid components
martenson Jun 6, 2023
d2b1e45
add backend changed for pages grids
martenson Jun 7, 2023
3ba401f
juggle the enums in schema
martenson Jun 7, 2023
06125f3
adjust test_pages selenium test for new grid markup
martenson Jun 9, 2023
8cb5188
correct grammar in desc
martenson Jun 12, 2023
e7904a7
unify punctuation handling in pages api desc
martenson Jun 12, 2023
0940ef2
rename method to have a more proper name
martenson Jun 12, 2023
c52a431
prettier than the prettier
martenson Jun 13, 2023
baf823a
use router instead of window
martenson Jun 13, 2023
3c1579a
pretty
martenson Jun 13, 2023
d057da2
wait for the render before setting form values
martenson Jun 14, 2023
cb38299
refactor window.confirm to b-modal as suggested in a review
martenson Jun 15, 2023
c40f674
re-add the jest unit tests with workadrounds for lazy load and portal…
martenson Jun 15, 2023
7ffee64
adjust test for new modal
martenson Jun 15, 2023
381d3ba
wait for render after page creation test method
martenson Jun 16, 2023
8a7942b
increase waiting time
martenson Jun 16, 2023
814b80c
Merge branch 'dev' into more-pages
martenson Jun 17, 2023
837f96a
adapt to new (conflicting?) navigation.yml
martenson Jun 17, 2023
d2dbafa
Merge branch 'dev' into more-pages
martenson Jun 19, 2023
64b4d83
format
martenson Jun 19, 2023
bc971d9
attempt to fix selenium mby?
martenson Jun 19, 2023
443036a
attempt to please selenium diety
martenson Jun 19, 2023
dd0750f
revert pdf_export test changes, that broke things for no reason
martenson Jun 19, 2023
711eaac
remove custom prefix handling
martenson Jun 21, 2023
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
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