From cdcd0553d5447e588f7d71aa982ba8211c98b61c Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 14:59:51 -0500 Subject: [PATCH 01/10] Alphabetically sort index endpoint parameters of pages --- lib/galaxy/schema/schema.py | 8 ++++---- lib/galaxy/webapps/galaxy/api/pages.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index f94061a887c1..dd506242e9c8 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1459,14 +1459,14 @@ class InvocationIndexQueryPayload(Model): class PageIndexQueryPayload(Model): deleted: bool = False + limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") + offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") show_published: Optional[bool] = None show_shared: Optional[bool] = None - user_id: Optional[DecodedDatabaseIdField] = None + search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") sort_by: PageSortByEnum = Field("update_time", title="Sort By", description="Sort pages by this attribute.") sort_desc: Optional[bool] = Field(default=False, title="Sort descending", description="Sort in descending order.") - search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") - limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") - offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") + user_id: Optional[DecodedDatabaseIdField] = None class CreateHistoryPayload(Model): diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 7606aaa53b47..16077b08591c 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -105,26 +105,26 @@ async def index( response: Response, trans: ProvidesUserContext = DependsOnTrans, deleted: bool = DeletedQueryParam, - user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, + limit: int = LimitQueryParam, + offset: int = OffsetQueryParam, + search: Optional[str] = SearchQueryParam, show_published: bool = ShowPublishedQueryParam, show_shared: bool = ShowSharedQueryParam, sort_by: PageSortByEnum = SortByQueryParam, sort_desc: bool = SortDescQueryParam, - limit: int = LimitQueryParam, - offset: int = OffsetQueryParam, - search: Optional[str] = SearchQueryParam, + user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, ) -> PageSummaryList: """Get a list with summary information of all Pages available to the user.""" payload = PageIndexQueryPayload.model_construct( deleted=deleted, - user_id=user_id, + limit=limit, + offset=offset, + search=search, show_published=show_published, show_shared=show_shared, sort_by=sort_by, sort_desc=sort_desc, - limit=limit, - offset=offset, - search=search, + user_id=user_id, ) pages, total_matches = self.service.index(trans, payload, include_total_count=True) response.headers["total_matches"] = str(total_matches) From c0e0c7525ebadf36c2270b3aa61777d15a8174ce Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:01:03 -0500 Subject: [PATCH 02/10] Add show_own query parameter in alignment with visualizations index endpoint --- lib/galaxy/schema/schema.py | 1 + lib/galaxy/webapps/galaxy/api/pages.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index dd506242e9c8..e51b96bfb358 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1461,6 +1461,7 @@ class PageIndexQueryPayload(Model): deleted: bool = False limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") + show_own: Optional[bool] = None show_published: Optional[bool] = None show_shared: Optional[bool] = None search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 16077b08591c..7cea098afdf3 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -52,6 +52,8 @@ title="Encoded user ID to restrict query to, must be own id if not an admin user", ) +ShowOwnQueryParam: bool = Query(default=True, title="Show pages owned by user.", description="") + ShowPublishedQueryParam: bool = Query(default=True, title="Include published pages.", description="") ShowSharedQueryParam: bool = Query(default=False, title="Include pages shared with authenticated user.", description="") @@ -108,6 +110,7 @@ async def index( limit: int = LimitQueryParam, offset: int = OffsetQueryParam, search: Optional[str] = SearchQueryParam, + show_own: bool = ShowOwnQueryParam, show_published: bool = ShowPublishedQueryParam, show_shared: bool = ShowSharedQueryParam, sort_by: PageSortByEnum = SortByQueryParam, @@ -120,6 +123,7 @@ async def index( limit=limit, offset=offset, search=search, + show_own=show_own, show_published=show_published, show_shared=show_shared, sort_by=sort_by, From 3aab18cc0b0f7607881d96f64943ac55e51b8941 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:12:22 -0500 Subject: [PATCH 03/10] Align pages manager query to histories, and visualizations endpoint for consistency --- lib/galaxy/managers/pages.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 3157b026a4dd..fef8ed8a245c 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -143,36 +143,30 @@ def index_query( self, trans: ProvidesUserContext, payload: PageIndexQueryPayload, include_total_count: bool = False ) -> Tuple[sqlalchemy.engine.Result, int]: show_deleted = payload.deleted + show_own = payload.show_own + show_published = payload.show_published show_shared = payload.show_shared is_admin = trans.user_is_admin user = trans.user - if show_shared is None: - show_shared = not show_deleted - - if show_shared and show_deleted: - message = "show_shared and show_deleted cannot both be specified as true" + if not user: + message = "Requires user to log in." raise exceptions.RequestParameterInvalidException(message) - stmt = select(Page) - - if not is_admin: - filters = [Page.user == trans.user] - if payload.show_published: - filters.append(Page.published == true()) - if user and show_shared: - filters.append(PageUserShareAssociation.user == user) - stmt = stmt.outerjoin(Page.users_shared_with) - stmt = stmt.where(or_(*filters)) + stmt = select(self.model_class) - if not show_deleted: - stmt = stmt.where(Page.deleted == false()) - elif not is_admin: - # don't let non-admins see other user's deleted pages - stmt = stmt.where(or_(Page.deleted == false(), Page.user == user)) + filters = [] + if show_own or (not show_published and not is_admin): + filters = [self.model_class.user == user] + if show_published: + filters.append(self.model_class.published == true()) + if user and show_shared: + filters.append(self.user_share_model.user == user) + stmt = stmt.outerjoin(self.model_class.users_shared_with) + stmt = stmt.where(or_(*filters)) if payload.user_id: - stmt = stmt.where(Page.user_id == payload.user_id) + stmt = stmt.where(self.model_class.user_id == payload.user_id) if payload.search: search_query = payload.search @@ -222,6 +216,12 @@ def p_tag_filter(term_text: str, quoted: bool): term, ) ) + + if show_published and not is_admin: + show_deleted = False + + stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())) + if include_total_count: total_matches = get_count(trans.sa_session, stmt) else: From c2210555d05efdf51220d80eb306059580f57192 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:17:16 -0500 Subject: [PATCH 04/10] Ensure that visualizations and pages index endpoints do not produce duplicate entries --- lib/galaxy/managers/pages.py | 2 +- lib/galaxy/managers/visualizations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index fef8ed8a245c..2e5bc651f825 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -220,7 +220,7 @@ def p_tag_filter(term_text: str, quoted: bool): if show_published and not is_admin: show_deleted = False - stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())) + stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())).distinct() if include_total_count: total_matches = get_count(trans.sa_session, stmt) diff --git a/lib/galaxy/managers/visualizations.py b/lib/galaxy/managers/visualizations.py index 9aa2808f147f..c6fe3f365b74 100644 --- a/lib/galaxy/managers/visualizations.py +++ b/lib/galaxy/managers/visualizations.py @@ -155,7 +155,7 @@ def p_tag_filter(term_text: str, quoted: bool): if show_published and not is_admin: show_deleted = False - query = query.filter(self.model_class.deleted == (true() if show_deleted else false())) + query = query.filter(self.model_class.deleted == (true() if show_deleted else false())).distinct() if include_total_count: total_matches = query.count() From f7cf53ea7c0dc356e8ddaa577f5d21fbd9c190d0 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:22:20 -0500 Subject: [PATCH 05/10] Allow to sort pages by create time --- lib/galaxy/schema/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index e51b96bfb358..57fd36da1855 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1454,7 +1454,7 @@ class InvocationIndexQueryPayload(Model): offset: Optional[int] = Field(default=0, description="Number of invocations to skip") -PageSortByEnum = Literal["update_time", "title", "username"] +PageSortByEnum = Literal["create_time", "title", "update_time", "username"] class PageIndexQueryPayload(Model): From 95725e2b383fcb4afe1eaa502f9d731691523442 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:24:11 -0500 Subject: [PATCH 06/10] Update client api schema --- client/src/api/schema/schema.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index b888d567942d..d00a85caa270 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -18570,8 +18570,6 @@ export interface operations { */ parameters?: { /** @description Whether to include deleted pages in the result. */ - /** @description Sort page index by this specified attribute on the page model */ - /** @description Sort in descending order? */ /** * @description A mix of free text and GitHub-style tags used to filter the index operation. * @@ -18608,16 +18606,19 @@ export interface operations { * Free text search terms will be searched against the following attributes of the * Pages: `title`, `slug`, `tag`, `user`. */ + /** @description Sort page index by this specified attribute on the page model */ + /** @description Sort in descending order? */ query?: { deleted?: boolean; - user_id?: string | null; - show_published?: boolean; - show_shared?: boolean; - sort_by?: "update_time" | "title" | "username"; - sort_desc?: boolean; limit?: number; offset?: number; search?: string | null; + show_own?: boolean; + show_published?: boolean; + show_shared?: boolean; + sort_by?: "create_time" | "title" | "update_time" | "username"; + sort_desc?: boolean; + user_id?: string | null; }; /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ header?: { From abc13f99294459b65239cfbf6c45a4cf02d00d80 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:38:01 -0500 Subject: [PATCH 07/10] Add page grid config --- client/src/components/Grid/configs/pages.ts | 186 ++++++++++++++++++++ client/src/entry/analysis/router.js | 8 + 2 files changed, 194 insertions(+) create mode 100644 client/src/components/Grid/configs/pages.ts diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts new file mode 100644 index 000000000000..517a812f78e9 --- /dev/null +++ b/client/src/components/Grid/configs/pages.ts @@ -0,0 +1,186 @@ +import { faEdit, faEye, faPlus, faShareAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { fetcher } from "@/api/schema"; +import { getGalaxyInstance } from "@/app"; +import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering"; +import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Api endpoint handlers + */ +const getPages = fetcher.path("/api/pages").method("get").create(); +const deletePage = fetcher.path("/api/pages/{id}").method("delete").create(); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "title" | "update_time" | "username" | undefined; +type PageEntry = Record; + +/** + * Request and return data from server + */ +async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { + // TODO: Avoid using Galaxy instance to identify current user + const Galaxy = getGalaxyInstance(); + const userId = !Galaxy.isAnonymous && Galaxy.user.id; + if (!userId) { + rethrowSimple("Please login to access this page."); + } + const { data, headers } = await getPages({ + limit, + offset, + search, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + show_published: false, + user_id: userId, + }); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Create", + icon: faPlus, + handler: () => { + emit("/pages/create"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + title: "Title", + key: "title", + type: "operations", + width: 40, + operations: [ + { + title: "View", + icon: faEye, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/published/page?id=${data.id}`); + }, + }, + { + title: "Edit Content", + icon: faEdit, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/edit?id=${data.id}`); + }, + }, + { + title: "Edit Content", + icon: faEdit, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/editor?id=${data.id}`); + }, + }, + { + title: "Share and Publish", + icon: faShareAlt, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/sharing?id=${data.id}`); + }, + }, + { + title: "Delete", + icon: faTrash, + condition: (data: PageEntry) => !data.deleted, + handler: async (data: PageEntry) => { + try { + await deletePage({ id: String(data.id) }); + return { + status: "success", + message: `'${data.title}' has been deleted.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to delete '${data.title}': ${errorMessageAsString(e)}.`, + }; + } + }, + }, + ], + }, + { + key: "create_time", + title: "Created", + type: "date", + }, + { + key: "update_time", + title: "Updated", + type: "date", + }, + { + key: "sharing", + title: "Status", + type: "sharing", + }, +]; + +/** + * Declare filter options + */ +const validFilters: Record> = { + title: { placeholder: "title", type: String, handler: contains("title"), menuItem: true }, + slug: { handler: contains("slug"), menuItem: false }, + published: { + placeholder: "Filter on published pages", + type: Boolean, + boolType: "is", + handler: equals("published", "published", toBool), + menuItem: true, + }, + importable: { + placeholder: "Filter on importable pages", + type: Boolean, + boolType: "is", + handler: equals("importable", "importable", toBool), + menuItem: true, + }, + deleted: { + placeholder: "Filter on deleted pages", + type: Boolean, + boolType: "is", + handler: equals("deleted", "deleted", toBool), + menuItem: true, + }, +}; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "pages-grid", + actions: actions, + fields: fields, + filtering: new Filtering(validFilters, undefined, false, false), + getData: getData, + plural: "Pages", + sortBy: "update_time", + sortDesc: true, + sortKeys: ["create_time", "title", "update_time"], + title: "Saved Pages", +}; + +export default gridConfig; diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 21ff320e7ce1..94030de2d77b 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -10,6 +10,7 @@ import FormGeneric from "components/Form/FormGeneric"; import historiesGridConfig from "components/Grid/configs/histories"; import historiesPublishedGridConfig from "components/Grid/configs/historiesPublished"; import historiesSharedGridConfig from "components/Grid/configs/historiesShared"; +import pagesGridConfig from "components/Grid/configs/pages"; import visualizationsGridConfig from "components/Grid/configs/visualizations"; import visualizationsPublishedGridConfig from "components/Grid/configs/visualizationsPublished"; import GridList from "components/Grid/GridList"; @@ -373,6 +374,13 @@ export function getRouter(Galaxy) { modelClass: "Page", }), }, + { + path: "pages/list", + component: GridList, + props: { + gridConfig: pagesGridConfig, + }, + }, { path: "pages/:actionId", component: PageList, From b3a984cdbc7be0147ec2ef761a019afd653d7ffe Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:42:38 -0500 Subject: [PATCH 08/10] Add grid config for published pages --- .../components/Grid/configs/pagesPublished.ts | 121 ++++++++++++++++++ client/src/entry/analysis/router.js | 11 +- 2 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 client/src/components/Grid/configs/pagesPublished.ts diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts new file mode 100644 index 000000000000..c0135d39bb9f --- /dev/null +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -0,0 +1,121 @@ +import { faEye, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { fetcher } from "@/api/schema"; +import { getGalaxyInstance } from "@/app"; +import Filtering, { contains, type ValidFilter } from "@/utils/filtering"; +import { rethrowSimple } from "@/utils/simple-error"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Api endpoint handlers + */ +const getPages = fetcher.path("/api/pages").method("get").create(); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "title" | "update_time" | "username" | undefined; +type PageEntry = Record; + +/** + * Request and return data from server + */ +async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { + // TODO: Avoid using Galaxy instance to identify current user + const Galaxy = getGalaxyInstance(); + const userId = !Galaxy.isAnonymous && Galaxy.user.id; + if (!userId) { + rethrowSimple("Please login to access this page."); + } + const { data, headers } = await getPages({ + limit, + offset, + search, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + show_published: false, + user_id: userId, + }); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Create", + icon: faPlus, + handler: () => { + emit("/pages/create"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + title: "Title", + key: "title", + type: "operations", + width: 40, + operations: [ + { + title: "View", + icon: faEye, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/published/page?id=${data.id}`); + }, + }, + ], + }, + { + key: "create_time", + title: "Created", + type: "date", + }, + { + key: "update_time", + title: "Updated", + type: "date", + }, + { + key: "owner", + title: "Owner", + type: "text", + }, +]; + +/** + * Declare filter options + */ +const validFilters: Record> = { + title: { placeholder: "title", type: String, handler: contains("title"), menuItem: true }, + slug: { handler: contains("slug"), menuItem: false }, +}; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "pages-published-grid", + actions: actions, + fields: fields, + filtering: new Filtering(validFilters, undefined, false, false), + getData: getData, + plural: "Pages", + sortBy: "update_time", + sortDesc: true, + sortKeys: ["create_time", "title", "update_time"], + title: "Published Pages", +}; + +export default gridConfig; diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 94030de2d77b..f6658bcc6a6d 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -11,6 +11,7 @@ import historiesGridConfig from "components/Grid/configs/histories"; import historiesPublishedGridConfig from "components/Grid/configs/historiesPublished"; import historiesSharedGridConfig from "components/Grid/configs/historiesShared"; import pagesGridConfig from "components/Grid/configs/pages"; +import pagesPublishedGridConfig from "components/Grid/configs/pagesPublished"; import visualizationsGridConfig from "components/Grid/configs/visualizations"; import visualizationsPublishedGridConfig from "components/Grid/configs/visualizationsPublished"; import GridList from "components/Grid/GridList"; @@ -382,11 +383,11 @@ export function getRouter(Galaxy) { }, }, { - path: "pages/:actionId", - component: PageList, - props: (route) => ({ - published: route.params.actionId == "list_published" ? true : false, - }), + path: "pages/list_published", + component: GridList, + props: { + gridConfig: pagesPublishedGridConfig, + }, }, { path: "storage/history/:historyId", From 7ff048365df70a63c258f7acd7e12fe920cdfd0b Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:43:34 -0500 Subject: [PATCH 09/10] Remove previous custom page grids --- .../src/components/Page/PageDropdown.test.ts | 193 ----------- client/src/components/Page/PageDropdown.vue | 97 ------ .../components/Page/PageIndexActions.test.ts | 32 -- .../src/components/Page/PageIndexActions.vue | 22 -- client/src/components/Page/PageList.test.js | 324 ------------------ client/src/components/Page/PageList.vue | 320 ----------------- 6 files changed, 988 deletions(-) delete mode 100644 client/src/components/Page/PageDropdown.test.ts delete mode 100644 client/src/components/Page/PageDropdown.vue delete mode 100644 client/src/components/Page/PageIndexActions.test.ts delete mode 100644 client/src/components/Page/PageIndexActions.vue delete mode 100644 client/src/components/Page/PageList.test.js delete mode 100644 client/src/components/Page/PageList.vue diff --git a/client/src/components/Page/PageDropdown.test.ts b/client/src/components/Page/PageDropdown.test.ts deleted file mode 100644 index 4a4be6327a33..000000000000 --- a/client/src/components/Page/PageDropdown.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import "jest-location-mock"; - -import { expect, jest } from "@jest/globals"; -import { createTestingPinia } from "@pinia/testing"; -import { createWrapper, mount, shallowMount, Wrapper } from "@vue/test-utils"; -import { createPinia, PiniaVuePlugin } from "pinia"; -import { getLocalVue } from "tests/jest/helpers"; - -import { mockFetcher } from "@/api/schema/__mocks__"; -import { useUserStore } from "@/stores/userStore"; - -import PageDropdown from "./PageDropdown.vue"; - -jest.mock("@/api/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: Wrapper; - - 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, - stubs: { - RouterLink: true, - }, - }); - const userStore = useUserStore(); - userStore.currentUser = { - email: "my@email", - id: "1", - tags_used: [], - isAnonymous: false, - total_disk_usage: 1048576, - }; - }); - - 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(), - stubs: { - RouterLink: true, - }, - }); - }); - - 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({ confirm = true } = {}) { - const propsData = { - root: "/rootprefixdelete/", - page: PAGE_DATA_OWNED, - }; - wrapper = mount(PageDropdown, { - propsData, - localVue, - pinia: pinia, - stubs: { - transition: false, - RouterLink: true, - }, - }); - const userStore = useUserStore(); - userStore.currentUser = { - email: "my@email", - id: "1", - tags_used: [], - isAnonymous: false, - total_disk_usage: 1048576, - }; - wrapper.find(".page-dropdown").trigger("click"); - await wrapper.vm.$nextTick(); - wrapper.find(".dropdown-item-delete").trigger("click"); - - if (confirm) { - await confirmDeletion(); - } - } - - async function confirmDeletion() { - // 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 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(); - }); - - it("should not fire deletion API request if not confirmed", async () => { - await mountAndDelete({ confirm: false }); - const emitted = wrapper.emitted(); - expect(emitted["onRemove"]).toBeFalsy(); - expect(emitted["onSuccess"]).toBeFalsy(); - }); - }); -}); diff --git a/client/src/components/Page/PageDropdown.vue b/client/src/components/Page/PageDropdown.vue deleted file mode 100644 index 270a9168648a..000000000000 --- a/client/src/components/Page/PageDropdown.vue +++ /dev/null @@ -1,97 +0,0 @@ - - diff --git a/client/src/components/Page/PageIndexActions.test.ts b/client/src/components/Page/PageIndexActions.test.ts deleted file mode 100644 index 48bf76b2b2af..000000000000 --- a/client/src/components/Page/PageIndexActions.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import "jest-location-mock"; - -import { shallowMount } from "@vue/test-utils"; -import { getLocalVue } from "tests/jest/helpers"; - -import PageIndexActions from "./PageIndexActions.vue"; - -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"); - }); - }); -}); diff --git a/client/src/components/Page/PageIndexActions.vue b/client/src/components/Page/PageIndexActions.vue deleted file mode 100644 index b4563d38a521..000000000000 --- a/client/src/components/Page/PageIndexActions.vue +++ /dev/null @@ -1,22 +0,0 @@ - - diff --git a/client/src/components/Page/PageList.test.js b/client/src/components/Page/PageList.test.js deleted file mode 100644 index ef61d1f07397..000000000000 --- a/client/src/components/Page/PageList.test.js +++ /dev/null @@ -1,324 +0,0 @@ -import { createTestingPinia } from "@pinia/testing"; -import { mount } from "@vue/test-utils"; -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { formatDistanceToNow, parseISO } from "date-fns"; -import flushPromises from "flush-promises"; -import { PiniaVuePlugin } from "pinia"; -import { useUserStore } from "stores/userStore"; -import { getLocalVue, wait } from "tests/jest/helpers"; - -import PageList from "./PageList.vue"; - -jest.mock("app"); - -const localVue = getLocalVue(); -localVue.use(PiniaVuePlugin); - -const debounceDelay = 500; // in ms - see FilterMenu.props.debounceDelay - -describe("PgeList.vue", () => { - let axiosMock; - let wrapper; - - beforeEach(async () => { - axiosMock = new MockAdapter(axios); - }); - - afterEach(() => { - axiosMock.restore(); - }); - - const personalGridApiParams = { - limit: 20, - offset: 0, - sort_by: "update_time", - sort_desc: true, - show_published: false, - show_shared: true, - }; - const publishedGridApiParams = { - ...personalGridApiParams, - show_published: true, - search: "is:published", - show_shared: false, - }; - const publishedGridApiParamsSortTitleAsc = { - sort_by: "title", - limit: 20, - offset: 0, - search: "is:published", - show_published: true, - show_shared: false, - }; - const publishedGridApiParamsSortTitleDesc = { - ...publishedGridApiParamsSortTitleAsc, - sort_desc: true, - }; - - const propsDataPersonalGrid = { - inputDebounceDelay: 0, - published: false, - }; - const propsDataPublishedGrid = { - ...propsDataPersonalGrid, - published: true, - }; - - const privatePage = { - id: "5f1915bcf9f3561a", - update_time: "2023-05-23T17:51:51.069761", - create_time: "2023-01-04T17:40:58.407793", - deleted: false, - importable: false, - published: false, - slug: "raynor-here", - tags: ["tagThis", "tagIs", "tagJimmy"], - title: "jimmy's page", - username: "jimmyPage", - }; - const publishedPage = { - ...privatePage, - id: "5f1915bcf9f3561b", - published: true, - importable: true, - update_time: "2023-05-25T17:51:51.069761", - }; - const pageA = { - id: "5f1915bcf9f3561c", - update_time: "2023-05-21T17:51:51.069761", - create_time: "2023-01-04T17:40:58.407793", - deleted: false, - importable: true, - published: true, - slug: "a-page", - title: "a page title", - username: "APageUser", - }; - const mockPrivatePageData = [privatePage]; - const mockPublishedPageData = [publishedPage]; - const mockTwoPageData = [privatePage, pageA]; - - function mountGrid(propsData) { - const pinia = createTestingPinia(); - const userStore = useUserStore(); - userStore.currentUser = { username: "jimmyPage", tags_used: [] }; - - wrapper = mount(PageList, { - propsData, - localVue, - pinia, - stubs: { - icon: { template: "
" }, - }, - }); - } - - function mountPersonalGrid() { - mountGrid(propsDataPersonalGrid); - } - - function mountPublishedGrid() { - mountGrid(propsDataPublishedGrid); - } - - describe(" with empty page list", () => { - beforeEach(async () => { - axiosMock.onAny().reply(200, [], { total_matches: "0" }); - mountPersonalGrid(); - }); - - it("title should be shown", async () => { - expect(wrapper.find("#pages-title").text()).toBe("Pages"); - }); - - it("no invocations message should be shown when not loading", async () => { - expect(wrapper.find("#no-pages").exists()).toBe(true); - }); - }); - describe("with server error", () => { - beforeEach(async () => { - axiosMock.onAny().reply(403, { err_msg: "this is a problem" }); - mountPersonalGrid(); - }); - - it("renders error message", async () => { - expect(wrapper.find(".index-grid-message").text()).toContain("this is a problem"); - }); - }); - - describe("with single private page", () => { - function findSearchBar(wrapper) { - return wrapper.find("[data-description='filter text input']"); - } - - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: { search: "", ...personalGridApiParams } }) - .reply(200, mockPrivatePageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPersonalGrid(); - await flushPromises(); - }); - - it("'no pages' message is gone", async () => { - expect(wrapper.find("#no-pages").exists()).toBe(false); - }); - - it("renders one row with correct sharing options", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(1); - const row = rows[0]; - const columns = row.findAll("td"); - expect(columns.at(0).text()).toContain("jimmy's page"); - expect(columns.at(1).text()).toContain("tagThis"); - expect(columns.at(1).text()).toContain("tagIs"); - expect(columns.at(1).text()).toContain("tagJimmy"); - expect(columns.at(3).text()).toBe( - formatDistanceToNow(parseISO(`${mockPrivatePageData[0].update_time}Z`), { addSuffix: true }) - ); - expect(row.find(".share-this-page").exists()).toBe(true); - expect(row.find(".sharing-indicator-published").exists()).toBe(false); - expect(row.find(".sharing-indicator-importable").exists()).toBe(false); - expect(row.find(".sharing-indicator-shared").exists()).toBe(false); - }); - - it("starts with an empty filter", async () => { - expect(findSearchBar(wrapper).element.value).toBe(""); - }); - - it("fetches filtered results when search filter is used", async () => { - await findSearchBar(wrapper).setValue("mytext"); - expect(findSearchBar(wrapper).element.value).toBe("mytext"); - await wait(debounceDelay); - expect(wrapper.vm.filterText).toBe("mytext"); - }); - - it("updates filter when a tag is clicked", async () => { - const tags = wrapper.findAll("tbody > tr .tag").wrappers; - expect(tags.length).toBe(3); - tags[0].trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("tag:'tagThis'"); - }); - - it("updates filter when a tag is clicked only on the first click", async () => { - const tags = wrapper.findAll("tbody > tr .tag").wrappers; - expect(tags.length).toBe(3); - tags[0].trigger("click"); - tags[0].trigger("click"); - tags[0].trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("tag:'tagThis'"); - }); - }); - describe("with single published and importable page on personal grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: { search: "", ...personalGridApiParams } }) - .reply(200, mockPublishedPageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = true; - }); - mountPersonalGrid(); - await flushPromises(); - }); - it("updates filter when published icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-published").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:published"); - }); - - it("updates filter when shared with me icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-shared").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:shared_with_me"); - }); - - it("updates filter when importable icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-importable").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:importable"); - }); - }); - describe("with single page on published grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: publishedGridApiParams }) - .reply(200, mockPublishedPageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPublishedGrid(); - await flushPromises(); - }); - - it("renders one row with correct sharing options", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(1); - const row = rows[0]; - const columns = row.findAll("td"); - expect(columns.at(0).text()).toContain("jimmy's page"); - expect(columns.at(1).text()).toContain("tagThis"); - expect(columns.at(1).text()).toContain("tagIs"); - expect(columns.at(1).text()).toContain("tagJimmy"); - expect(columns.at(2).text()).toBe("jimmyPage"); - expect(columns.at(3).text()).toBe( - formatDistanceToNow(parseISO(`${mockPublishedPageData[0].update_time}Z`), { addSuffix: true }) - ); - expect(row.find(".share-this-page").exists()).toBe(false); - expect(row.find(".sharing-indicator-published").exists()).toBe(false); - expect(row.find(".sharing-indicator-importable").exists()).toBe(false); - expect(row.find(".sharing-indicator-shared").exists()).toBe(false); - }); - }); - describe("with two pages on published grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: publishedGridApiParams }) - .reply(200, mockTwoPageData, { total_matches: "2" }); - axiosMock - .onGet("/api/pages", { params: publishedGridApiParamsSortTitleAsc }) - .reply(200, [pageA, privatePage], { total_matches: "2" }); - axiosMock - .onGet("/api/pages", { params: publishedGridApiParamsSortTitleDesc }) - .reply(200, mockTwoPageData, { total_matches: "2" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPublishedGrid(); - await flushPromises(); - }); - - it("should render both rows", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(2); - }); - - it("should sort asc/desc when title column is clicked", async () => { - let firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("jimmy's page"); - const titleColumn = wrapper.findAll("th").wrappers[0]; - // default sort is by update_time - expect(titleColumn.attributes("aria-sort")).toBe("none"); - await titleColumn.trigger("click"); - await flushPromises(); - expect(titleColumn.attributes("aria-sort")).toBe("ascending"); - firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("a page title"); - await titleColumn.trigger("click"); - await flushPromises(); - expect(titleColumn.attributes("aria-sort")).toBe("descending"); - firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("jimmy's page"); - }); - }); -}); diff --git a/client/src/components/Page/PageList.vue b/client/src/components/Page/PageList.vue deleted file mode 100644 index 2f941d190b5a..000000000000 --- a/client/src/components/Page/PageList.vue +++ /dev/null @@ -1,320 +0,0 @@ - - From 2b20442ba7b06b21206ccba7940efffdcc6b1b2f Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:45:03 -0500 Subject: [PATCH 10/10] Remove unused import, fix username in published grid config --- client/src/components/Grid/configs/pagesPublished.ts | 2 +- client/src/entry/analysis/router.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts index c0135d39bb9f..3db352182da3 100644 --- a/client/src/components/Grid/configs/pagesPublished.ts +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -88,7 +88,7 @@ const fields: FieldArray = [ type: "date", }, { - key: "owner", + key: "username", title: "Owner", type: "text", }, diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index f6658bcc6a6d..820cbb487f57 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -25,7 +25,6 @@ import InteractiveTools from "components/InteractiveTools/InteractiveTools"; import JobDetails from "components/JobInformation/JobDetails"; import CarbonEmissionsCalculations from "components/JobMetrics/CarbonEmissions/CarbonEmissionsCalculations"; import NewUserWelcome from "components/NewUserWelcome/NewUserWelcome"; -import PageList from "components/Page/PageList"; import PageDisplay from "components/PageDisplay/PageDisplay"; import PageEditor from "components/PageEditor/PageEditor"; import ToolSuccess from "components/Tool/ToolSuccess";