From d29366636e806ce20c09d1516e0d7766c39805f1 Mon Sep 17 00:00:00 2001 From: Aleksandr Lesnenko <alxnddr@users.noreply.github.com> Date: Tue, 5 Jul 2022 23:11:29 +0400 Subject: [PATCH] Fix saved question picker issues (#23704) --- .../src/metabase-types/api/mocks/index.ts | 1 + .../src/metabase-types/api/mocks/table.ts | 14 +++ frontend/src/metabase-types/api/table.ts | 2 +- .../src/metabase/components/tree/TreeNode.tsx | 2 +- .../SavedQuestionList.jsx | 37 ++++---- .../SavedQuestionPicker.jsx | 2 +- .../SavedQuestionPicker.unit.spec.jsx | 95 +++++++++++++++++++ 7 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 frontend/src/metabase-types/api/mocks/table.ts create mode 100644 frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.unit.spec.jsx diff --git a/frontend/src/metabase-types/api/mocks/index.ts b/frontend/src/metabase-types/api/mocks/index.ts index deac792384d..eaa71fb13bc 100644 --- a/frontend/src/metabase-types/api/mocks/index.ts +++ b/frontend/src/metabase-types/api/mocks/index.ts @@ -5,6 +5,7 @@ export * from "./dashboard"; export * from "./database"; export * from "./dataset"; export * from "./models"; +export * from "./table"; export * from "./timeline"; export * from "./settings"; export * from "./user"; diff --git a/frontend/src/metabase-types/api/mocks/table.ts b/frontend/src/metabase-types/api/mocks/table.ts new file mode 100644 index 00000000000..07bb37ae3d3 --- /dev/null +++ b/frontend/src/metabase-types/api/mocks/table.ts @@ -0,0 +1,14 @@ +import { Table } from "metabase-types/api"; + +export const createMockTable = (opts?: Partial<Table>): Table => { + return { + id: 1, + db_id: 1, + display_name: "Table", + name: "table", + schema: "public", + description: null, + visibility_type: "normal", + ...opts, + }; +}; diff --git a/frontend/src/metabase-types/api/table.ts b/frontend/src/metabase-types/api/table.ts index 7dade30cc15..003007194f9 100644 --- a/frontend/src/metabase-types/api/table.ts +++ b/frontend/src/metabase-types/api/table.ts @@ -13,7 +13,7 @@ export type VisibilityType = | "cruft"; export interface Table { - id: number; + id: number | string; // can be string for virtual questions (e.g. "card__17") db_id: number; db?: Database; name: string; diff --git a/frontend/src/metabase/components/tree/TreeNode.tsx b/frontend/src/metabase/components/tree/TreeNode.tsx index 78dd543edb8..a61997a4995 100644 --- a/frontend/src/metabase/components/tree/TreeNode.tsx +++ b/frontend/src/metabase/components/tree/TreeNode.tsx @@ -75,7 +75,7 @@ const BaseTreeNode = React.memo( <Icon {...iconProps} /> </IconContainer> )} - <NameContainer>{name}</NameContainer> + <NameContainer data-testid="tree-item-name">{name}</NameContainer> </TreeNodeRoot> ); }), diff --git a/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionList.jsx b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionList.jsx index e956deb7ecc..1ac5e9d4ba1 100644 --- a/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionList.jsx +++ b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionList.jsx @@ -57,24 +57,25 @@ function SavedQuestionList({ : schema.tables; return ( <React.Fragment> - {_.sortBy(tables, "display_name").map(t => ( - <SavedQuestionListItem - id={t.id} - isSelected={selectedId === t.id} - key={t.id} - size="small" - name={t.display_name} - icon={{ - name: isDatasets ? "model" : "table2", - size: 16, - }} - onSelect={() => onSelect(t)} - rightIcon={PLUGIN_MODERATION.getStatusIcon( - t.moderated_status, - )} - /> - ))} - + {tables + .sort((a, b) => a.display_name.localeCompare(b.display_name)) + .map(t => ( + <SavedQuestionListItem + id={t.id} + isSelected={selectedId === t.id} + key={t.id} + size="small" + name={t.display_name} + icon={{ + name: isDatasets ? "model" : "table2", + size: 16, + }} + onSelect={() => onSelect(t)} + rightIcon={PLUGIN_MODERATION.getStatusIcon( + t.moderated_status, + )} + /> + ))} {tables.length === 0 ? emptyState : null} </React.Fragment> ); diff --git a/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.jsx b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.jsx index 5c6014e7894..885671eff41 100644 --- a/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.jsx +++ b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.jsx @@ -69,8 +69,8 @@ function SavedQuestionPicker({ nonPersonalOrArchivedCollection, ); - preparedCollections.push(...nonPersonalOrArchivedCollections); preparedCollections.push(...userPersonalCollections); + preparedCollections.push(...nonPersonalOrArchivedCollections); if (currentUser.is_superuser) { const otherPersonalCollections = collections.filter( diff --git a/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.unit.spec.jsx b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.unit.spec.jsx new file mode 100644 index 00000000000..947f79694f0 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/saved-question-picker/SavedQuestionPicker.unit.spec.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import xhrMock from "xhr-mock"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { + createMockCollection, + createMockTable, +} from "metabase-types/api/mocks"; +import SavedQuestionPicker from "./SavedQuestionPicker"; + +const CURRENT_USER = { + id: 1, + personal_collection_id: 222, + is_superuser: true, +}; + +const COLLECTIONS = { + PERSONAL: createMockCollection({ + id: CURRENT_USER.personal_collection_id, + name: "My personal collection", + personal_owner_id: CURRENT_USER.id, + }), + REGULAR: createMockCollection({ id: 1, name: "Regular collection" }), +}; + +function mockCollectionTreeEndpoint() { + xhrMock.get("/api/collection/tree?tree=true", { + body: Object.values(COLLECTIONS), + }); +} + +function mockCollectionEndpoint() { + xhrMock.get("/api/database/-1337/schema/Everything%20else", { + body: [ + createMockTable({ + id: "card__1", + display_name: "B", + schema: "Everything else", + }), + createMockTable({ + id: "card__2", + display_name: "a", + schema: "Everything else", + }), + createMockTable({ + id: "card__3", + display_name: "A", + schema: "Everything else", + }), + ], + }); +} + +async function setup() { + mockCollectionTreeEndpoint(); + mockCollectionEndpoint(); + renderWithProviders( + <SavedQuestionPicker onSelect={jest.fn()} onBack={jest.fn()} />, + ); + await waitForElementToBeRemoved(() => screen.queryAllByText("Loading...")); +} + +describe("SavedQuestionPicker", () => { + beforeEach(() => { + xhrMock.setup(); + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + }); + + afterEach(() => { + xhrMock.teardown(); + }); + + it("shows the current user personal collection on the top after the root", async () => { + await setup(); + + expect( + screen.getAllByTestId("tree-item-name").map(node => node.innerHTML), + ).toEqual([ + "Our analytics", + "Your personal collection", + "Regular collection", + ]); + }); + + it("sorts saved questions case-insensitive (metabase#23693)", async () => { + await setup(); + + expect( + screen.getAllByTestId("option-text").map(node => node.innerHTML), + ).toEqual(["a", "A", "B"]); + }); +}); -- GitLab