Skip to content
Snippets Groups Projects
Unverified Commit 5ba1b69e authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Convert SearchResult Component to Typescript (#31783)

* convert searchResult to typescript

* expand searchResult tests and convert to typescript

* remove prop types

* cleanup tests and infoText component
parent 58c7b0ff
No related branches found
No related tags found
No related merge requests found
Showing
with 463 additions and 123 deletions
...@@ -3,7 +3,7 @@ import { REGULAR_COLLECTION } from "./constants"; ...@@ -3,7 +3,7 @@ import { REGULAR_COLLECTION } from "./constants";
export function isRegularCollection({ export function isRegularCollection({
authority_level, authority_level,
}: Bookmark | Collection) { }: Bookmark | Partial<Collection>) {
// Root, personal collections don't have `authority_level` // Root, personal collections don't have `authority_level`
return !authority_level || authority_level === REGULAR_COLLECTION.type; return !authority_level || authority_level === REGULAR_COLLECTION.type;
} }
...@@ -15,6 +15,7 @@ export * from "./modelIndexes"; ...@@ -15,6 +15,7 @@ export * from "./modelIndexes";
export * from "./parameters"; export * from "./parameters";
export * from "./query"; export * from "./query";
export * from "./schema"; export * from "./schema";
export * from "./search";
export * from "./segment"; export * from "./segment";
export * from "./series"; export * from "./series";
export * from "./session"; export * from "./session";
......
import { SearchResult, SearchScore } from "metabase-types/api";
import { createMockCollection } from "./collection";
export const createMockSearchResult = (
options: Partial<SearchResult> = {},
): SearchResult => {
const collection = createMockCollection(options?.collection ?? undefined);
return {
id: 1,
name: "Mock search result",
description: "Mock search result description",
model: "card",
model_id: null,
archived: null,
collection,
collection_position: null,
table_id: 1,
table_name: null,
bookmark: null,
database_id: 1,
pk_ref: null,
table_schema: null,
collection_authority_level: null,
updated_at: "2023-01-01T00:00:00.000Z",
moderated_status: null,
model_name: null,
table_description: null,
initial_sync_status: null,
dashboard_count: null,
context: null,
scores: [createMockSearchScore()],
...options,
};
};
export const createMockSearchScore = (
options: Partial<SearchScore> = {},
): SearchScore => ({
score: 1,
weight: 1,
name: "text-total-occurrences",
...options,
});
import { CardId } from "./card";
import { Collection } from "./collection";
import { DatabaseId } from "./database"; import { DatabaseId } from "./database";
import { FieldReference } from "./query";
import { TableId } from "./table";
export type SearchModelType = export type SearchModelType =
| "card" | "card"
...@@ -8,7 +12,65 @@ export type SearchModelType = ...@@ -8,7 +12,65 @@ export type SearchModelType =
| "dataset" | "dataset"
| "table" | "table"
| "indexed-entity" | "indexed-entity"
| "pulse"; | "pulse"
| "segment"
| "metric"
| "action";
export interface SearchScore {
weight: number;
score: number;
name:
| "pinned"
| "bookmarked"
| "recency"
| "dashboard"
| "model"
| "official collection score"
| "verified"
| "text-consecutivity"
| "text-total-occurrences"
| "text-fullness";
match?: string;
"match-context-thunk"?: string;
column?: string;
}
export interface SearchResults {
data: SearchResult[];
models: SearchModelType[] | null;
available_models: SearchModelType[];
limit: number;
offset: number;
table_db_id: DatabaseId | null;
total: number;
}
export interface SearchResult {
id: number | undefined;
name: string;
model: SearchModelType;
description: string | null;
archived: boolean | null;
collection_position: number | null;
collection: Pick<Collection, "id" | "name" | "authority_level">;
table_id: TableId;
bookmark: boolean | null;
database_id: DatabaseId;
pk_ref: FieldReference | null;
table_schema: string | null;
collection_authority_level: "official" | null;
updated_at: string;
moderated_status: boolean | null;
model_id: CardId | null;
model_name: string | null;
table_description: string | null;
table_name: string | null;
initial_sync_status: "complete" | "incomplete" | null;
dashboard_count: number | null;
context: any; // this might be a dead property
scores: SearchScore[];
}
export interface SearchListQuery { export interface SearchListQuery {
q?: string; q?: string;
......
...@@ -7,7 +7,7 @@ import _ from "underscore"; ...@@ -7,7 +7,7 @@ import _ from "underscore";
import { DEFAULT_SEARCH_LIMIT } from "metabase/lib/constants"; import { DEFAULT_SEARCH_LIMIT } from "metabase/lib/constants";
import Search from "metabase/entities/search"; import Search from "metabase/entities/search";
import SearchResult from "metabase/search/components/SearchResult"; import { SearchResult } from "metabase/search/components/SearchResult";
import EmptyState from "metabase/components/EmptyState"; import EmptyState from "metabase/components/EmptyState";
import { useListKeyboardNavigation } from "metabase/hooks/use-list-keyboard-navigation"; import { useListKeyboardNavigation } from "metabase/hooks/use-list-keyboard-navigation";
import { EmptyStateContainer } from "./SearchResults.styled"; import { EmptyStateContainer } from "./SearchResults.styled";
......
...@@ -150,7 +150,7 @@ export const PLUGIN_COLLECTIONS = { ...@@ -150,7 +150,7 @@ export const PLUGIN_COLLECTIONS = {
[JSON.stringify(AUTHORITY_LEVEL_REGULAR.type)]: AUTHORITY_LEVEL_REGULAR, [JSON.stringify(AUTHORITY_LEVEL_REGULAR.type)]: AUTHORITY_LEVEL_REGULAR,
}, },
REGULAR_COLLECTION: AUTHORITY_LEVEL_REGULAR, REGULAR_COLLECTION: AUTHORITY_LEVEL_REGULAR,
isRegularCollection: (_: Collection | Bookmark) => true, isRegularCollection: (_: Partial<Collection> | Bookmark) => true,
getAuthorityLevelMenuItems: ( getAuthorityLevelMenuItems: (
_collection: Collection, _collection: Collection,
_onUpdate: (collection: Collection, values: Partial<Collection>) => void, _onUpdate: (collection: Collection, values: Partial<Collection>) => void,
......
...@@ -3,7 +3,7 @@ import PropTypes from "prop-types"; ...@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { t } from "ttag"; import { t } from "ttag";
import { Icon } from "metabase/core/components/Icon"; import { Icon } from "metabase/core/components/Icon";
import SearchResult from "metabase/search/components/SearchResult"; import { SearchResult } from "metabase/search/components/SearchResult";
import { DEFAULT_SEARCH_LIMIT } from "metabase/lib/constants"; import { DEFAULT_SEARCH_LIMIT } from "metabase/lib/constants";
import Search from "metabase/entities/search"; import Search from "metabase/entities/search";
......
import PropTypes from "prop-types";
import { t, jt } from "ttag"; import { t, jt } from "ttag";
import * as Urls from "metabase/lib/urls"; import * as Urls from "metabase/lib/urls";
...@@ -11,55 +10,64 @@ import Database from "metabase/entities/databases"; ...@@ -11,55 +10,64 @@ import Database from "metabase/entities/databases";
import Table from "metabase/entities/tables"; import Table from "metabase/entities/tables";
import { PLUGIN_COLLECTIONS } from "metabase/plugins"; import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { getTranslatedEntityName } from "metabase/nav/utils"; import { getTranslatedEntityName } from "metabase/nav/utils";
import { CollectionBadge } from "./CollectionBadge";
const searchResultPropTypes = { import type { Collection } from "metabase-types/api";
database_id: PropTypes.number, import type TableType from "metabase-lib/metadata/Table";
table_id: PropTypes.number,
model: PropTypes.string, import { CollectionBadge } from "./CollectionBadge";
getCollection: PropTypes.func, import type { WrappedResult } from "./types";
collection: PropTypes.object,
table_schema: PropTypes.string,
};
const infoTextPropTypes = { export function InfoText({ result }: { result: WrappedResult }) {
result: PropTypes.shape(searchResultPropTypes), let textContent: string | string[] | JSX.Element;
};
export function InfoText({ result }) {
switch (result.model) { switch (result.model) {
case "card": case "card":
return jt`Saved question in ${formatCollection( textContent = jt`Saved question in ${formatCollection(
result, result,
result.getCollection(), result.getCollection(),
)}`; )}`;
break;
case "dataset": case "dataset":
return jt`Model in ${formatCollection(result, result.getCollection())}`; textContent = jt`Model in ${formatCollection(
result,
result.getCollection(),
)}`;
break;
case "collection": case "collection":
return getCollectionInfoText(result.collection); textContent = getCollectionInfoText(result.collection);
break;
case "database": case "database":
return t`Database`; textContent = t`Database`;
break;
case "table": case "table":
return <TablePath result={result} />; textContent = <TablePath result={result} />;
break;
case "segment": case "segment":
return jt`Segment of ${(<TableLink result={result} />)}`; textContent = jt`Segment of ${(<TableLink result={result} />)}`;
break;
case "metric": case "metric":
return jt`Metric for ${(<TableLink result={result} />)}`; textContent = jt`Metric for ${(<TableLink result={result} />)}`;
break;
case "action": case "action":
return jt`for ${result.model_name}`; textContent = jt`for ${result.model_name}`;
break;
case "indexed-entity": case "indexed-entity":
return jt`in ${result.model_name}`; textContent = jt`in ${result.model_name}`;
break;
default: default:
return jt`${getTranslatedEntityName(result.model)} in ${formatCollection( textContent = jt`${getTranslatedEntityName(
result, result.model,
result.getCollection(), )} in ${formatCollection(result, result.getCollection())}`;
)}`; break;
} }
}
InfoText.propTypes = infoTextPropTypes; return <>{textContent}</>;
}
function formatCollection(result, collection) { function formatCollection(
result: WrappedResult,
collection: Partial<Collection>,
) {
return ( return (
collection.id && ( collection.id && (
<CollectionBadge key={result.model} collection={collection} /> <CollectionBadge key={result.model} collection={collection} />
...@@ -67,59 +75,60 @@ function formatCollection(result, collection) { ...@@ -67,59 +75,60 @@ function formatCollection(result, collection) {
); );
} }
function getCollectionInfoText(collection) { function getCollectionInfoText(collection: Partial<Collection>) {
if (PLUGIN_COLLECTIONS.isRegularCollection(collection)) { if (
PLUGIN_COLLECTIONS.isRegularCollection(collection) ||
!collection.authority_level
) {
return t`Collection`; return t`Collection`;
} }
const level = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[collection.authority_level]; const level = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[collection.authority_level];
return `${level.name} ${t`Collection`}`; return `${level.name} ${t`Collection`}`;
} }
function TablePath({ result }) { function TablePath({ result }: { result: WrappedResult }) {
return jt`Table in ${( return (
<span key="table-path"> <>
<Database.Link id={result.database_id} />{" "} {jt`Table in ${(
{result.table_schema && ( <span key="table-path">
<Schema.ListLoader <Database.Link id={result.database_id} />{" "}
query={{ dbId: result.database_id }} {result.table_schema && (
loadingAndErrorWrapper={false} <Schema.ListLoader
> query={{ dbId: result.database_id }}
{({ list }) => loadingAndErrorWrapper={false}
list?.length > 1 ? ( >
<span> {({ list }: { list: typeof Schema[] }) =>
<Icon name="chevronright" mx="4px" size={10} /> list?.length > 1 ? (
{/* we have to do some {} manipulation here to make this look like the table object that browseSchema was written for originally */} <span>
<Link <Icon name="chevronright" size={10} />
to={Urls.browseSchema({ {/* we have to do some {} manipulation here to make this look like the table object that browseSchema was written for originally */}
db: { id: result.database_id }, <Link
schema_name: result.table_schema, to={Urls.browseSchema({
})} db: { id: result.database_id },
> schema_name: result.table_schema,
{result.table_schema} } as TableType)}
</Link> >
</span> {result.table_schema}
) : null </Link>
} </span>
</Schema.ListLoader> ) : null
)} }
</span> </Schema.ListLoader>
)}`; )}
</span>
)}`}
</>
);
} }
TablePath.propTypes = { function TableLink({ result }: { result: WrappedResult }) {
result: PropTypes.shape(searchResultPropTypes),
};
function TableLink({ result }) {
return ( return (
<Link to={Urls.tableRowsQuery(result.database_id, result.table_id)}> <Link to={Urls.tableRowsQuery(result.database_id, result.table_id)}>
<Table.Loader id={result.table_id} loadingAndErrorWrapper={false}> <Table.Loader id={result.table_id} loadingAndErrorWrapper={false}>
{({ table }) => (table ? <span>{table.display_name}</span> : null)} {({ table }: { table: TableType }) =>
table ? <span>{table.display_name}</span> : null
}
</Table.Loader> </Table.Loader>
</Link> </Link>
); );
} }
TableLink.propTypes = {
result: PropTypes.shape(searchResultPropTypes),
};
...@@ -6,23 +6,41 @@ import Link from "metabase/core/components/Link"; ...@@ -6,23 +6,41 @@ import Link from "metabase/core/components/Link";
import Text from "metabase/components/type/Text"; import Text from "metabase/components/type/Text";
import LoadingSpinner from "metabase/components/LoadingSpinner"; import LoadingSpinner from "metabase/components/LoadingSpinner";
function getColorForIconWrapper(props: { import type { SearchModelType } from "metabase-types/api";
type SearchEntity = any;
interface ResultStylesProps {
compact: boolean;
active: boolean;
isSelected: boolean;
}
function getColorForIconWrapper({
item,
active,
type,
}: {
item: SearchEntity;
active: boolean; active: boolean;
type: string; type: SearchModelType;
item: { collection_position?: unknown };
}) { }) {
if (!props.active) { if (!active) {
return color("text-medium"); return color("text-medium");
} else if (props.item.collection_position) { } else if (item.collection_position) {
return color("saturated-yellow"); return color("saturated-yellow");
} else if (props.type === "collection") { } else if (type === "collection") {
return lighten("brand", 0.35); return lighten("brand", 0.35);
} else { } else {
return color("brand"); return color("brand");
} }
} }
export const IconWrapper = styled.div` export const IconWrapper = styled.div<{
item: SearchEntity;
active: boolean;
type: SearchModelType;
}>`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
...@@ -50,13 +68,7 @@ export const Title = styled("h3")<{ active: boolean }>` ...@@ -50,13 +68,7 @@ export const Title = styled("h3")<{ active: boolean }>`
color: ${props => color(props.active ? "text-dark" : "text-medium")}; color: ${props => color(props.active ? "text-dark" : "text-medium")};
`; `;
interface ResultButtonProps { export const ResultButton = styled.button<ResultStylesProps>`
isSelected: boolean;
compact: boolean;
active: boolean;
}
export const ResultButton = styled.button<ResultButtonProps>`
${props => resultStyles(props)} ${props => resultStyles(props)}
padding-right: 0.5rem; padding-right: 0.5rem;
text-align: left; text-align: left;
...@@ -68,27 +80,25 @@ export const ResultButton = styled.button<ResultButtonProps>` ...@@ -68,27 +80,25 @@ export const ResultButton = styled.button<ResultButtonProps>`
} }
`; `;
export const ResultLink = styled(Link)<ResultButtonProps>` export const ResultLink = styled(Link)<ResultStylesProps>`
${props => resultStyles(props)} ${props => resultStyles(props)}
`; `;
const resultStyles = (props: ResultButtonProps) => ` const resultStyles = ({ compact, active, isSelected }: ResultStylesProps) => `
display: block; display: block;
background-color: ${ background-color: ${isSelected ? lighten("brand", 0.63) : "transparent"};
props.isSelected ? lighten("brand", 0.63) : "transparent" min-height: ${compact ? "36px" : "54px"};
};
min-height: ${props.compact ? "36px" : "54px"};
padding-top: ${space(1)}; padding-top: ${space(1)};
padding-bottom: ${space(1)}; padding-bottom: ${space(1)};
padding-left: 14px; padding-left: 14px;
padding-right: ${props.compact ? "20px" : space(3)}; padding-right: ${compact ? "20px" : space(3)};
cursor: ${props.active ? "pointer" : "default"}; cursor: ${active ? "pointer" : "default"};
&:hover { &:hover {
background-color: ${props.active ? lighten("brand", 0.63) : ""}; background-color: ${active ? lighten("brand", 0.63) : ""};
h3 { h3 {
color: ${props.active || props.isSelected ? color("brand") : ""}; color: ${active || isSelected ? color("brand") : ""};
} }
} }
...@@ -98,8 +108,8 @@ const resultStyles = (props: ResultButtonProps) => ` ...@@ -98,8 +108,8 @@ const resultStyles = (props: ResultButtonProps) => `
text-decoration-style: dashed; text-decoration-style: dashed;
&:hover { &:hover {
color: ${props.active ? color("brand") : ""}; color: ${active ? color("brand") : ""};
text-decoration-color: ${props.active ? color("brand") : ""}; text-decoration-color: ${active ? color("brand") : ""};
} }
} }
...@@ -111,11 +121,11 @@ const resultStyles = (props: ResultButtonProps) => ` ...@@ -111,11 +121,11 @@ const resultStyles = (props: ResultButtonProps) => `
} }
h3 { h3 {
font-size: ${props.compact ? "14px" : "16px"}; font-size: ${compact ? "14px" : "16px"};
line-height: 1.2em; line-height: 1.2em;
overflow-wrap: anywhere; overflow-wrap: anywhere;
margin-bottom: 0; margin-bottom: 0;
color: ${props.active && props.isSelected ? color("brand") : ""}; color: ${active && isSelected ? color("brand") : ""};
} }
.Icon-info { .Icon-info {
......
/* eslint-disable react/prop-types */
import { color } from "metabase/lib/colors"; import { color } from "metabase/lib/colors";
import { isSyncCompleted } from "metabase/lib/syncing"; import { isSyncCompleted } from "metabase/lib/syncing";
...@@ -7,6 +6,10 @@ import Text from "metabase/components/type/Text"; ...@@ -7,6 +6,10 @@ import Text from "metabase/components/type/Text";
import { PLUGIN_COLLECTIONS, PLUGIN_MODERATION } from "metabase/plugins"; import { PLUGIN_COLLECTIONS, PLUGIN_MODERATION } from "metabase/plugins";
import type { SearchScore, SearchModelType } from "metabase-types/api";
import type { WrappedResult } from "./types";
import { import {
IconWrapper, IconWrapper,
ResultButton, ResultButton,
...@@ -27,7 +30,7 @@ function TableIcon() { ...@@ -27,7 +30,7 @@ function TableIcon() {
return <Icon name="database" />; return <Icon name="database" />;
} }
function CollectionIcon({ item }) { function CollectionIcon({ item }: { item: WrappedResult }) {
const iconProps = { ...item.getIcon() }; const iconProps = { ...item.getIcon() };
const isRegular = PLUGIN_COLLECTIONS.isRegularCollection(item.collection); const isRegular = PLUGIN_COLLECTIONS.isRegularCollection(item.collection);
if (isRegular) { if (isRegular) {
...@@ -44,12 +47,24 @@ const ModelIconComponentMap = { ...@@ -44,12 +47,24 @@ const ModelIconComponentMap = {
collection: CollectionIcon, collection: CollectionIcon,
}; };
function DefaultIcon({ item }) { function DefaultIcon({ item }: { item: WrappedResult }) {
return <Icon {...item.getIcon()} size={DEFAULT_ICON_SIZE} />; return <Icon {...item.getIcon()} size={DEFAULT_ICON_SIZE} />;
} }
export function ItemIcon({ item, type, active }) { export function ItemIcon({
const IconComponent = ModelIconComponentMap[type] || DefaultIcon; item,
type,
active,
}: {
item: WrappedResult;
type: SearchModelType;
active: boolean;
}) {
const IconComponent =
type in Object.keys(ModelIconComponentMap)
? ModelIconComponentMap[type as keyof typeof ModelIconComponentMap]
: DefaultIcon;
return ( return (
<IconWrapper item={item} type={type} active={active}> <IconWrapper item={item} type={type} active={active}>
<IconComponent item={item} /> <IconComponent item={item} />
...@@ -57,13 +72,14 @@ export function ItemIcon({ item, type, active }) { ...@@ -57,13 +72,14 @@ export function ItemIcon({ item, type, active }) {
); );
} }
function Score({ scores }) { function Score({ scores }: { scores: SearchScore[] }) {
return ( return (
<pre className="hide search-score">{JSON.stringify(scores, null, 2)}</pre> <pre className="hide search-score">{JSON.stringify(scores, null, 2)}</pre>
); );
} }
function Context({ context }) { // I think it's very likely that this is a dead codepath: RL 2023-06-21
function Context({ context }: { context: any[] }) {
if (!context) { if (!context) {
return null; return null;
} }
...@@ -71,7 +87,7 @@ function Context({ context }) { ...@@ -71,7 +87,7 @@ function Context({ context }) {
return ( return (
<ContextContainer> <ContextContainer>
<ContextText> <ContextText>
{context.map(({ is_match, text }, i) => { {context.map(({ is_match, text }, i: number) => {
if (!is_match) { if (!is_match) {
return <span key={i}> {text}</span>; return <span key={i}> {text}</span>;
} }
...@@ -88,12 +104,18 @@ function Context({ context }) { ...@@ -88,12 +104,18 @@ function Context({ context }) {
); );
} }
export default function SearchResult({ export function SearchResult({
result, result,
compact = false, compact = false,
hasDescription = true, hasDescription = true,
onClick = undefined, onClick = undefined,
isSelected = false, isSelected = false,
}: {
result: WrappedResult;
compact?: boolean;
hasDescription?: boolean;
onClick?: (result: WrappedResult) => void;
isSelected?: boolean;
}) { }) {
const active = isItemActive(result); const active = isItemActive(result);
const loading = isItemLoading(result); const loading = isItemLoading(result);
...@@ -106,7 +128,7 @@ export default function SearchResult({ ...@@ -106,7 +128,7 @@ export default function SearchResult({
isSelected={isSelected} isSelected={isSelected}
active={active} active={active}
compact={compact} compact={compact}
to={!onClick ? result.getUrl() : undefined} to={!onClick ? result.getUrl() : ""}
onClick={onClick ? () => onClick(result) : undefined} onClick={onClick ? () => onClick(result) : undefined}
data-testid="search-result-item" data-testid="search-result-item"
> >
...@@ -137,7 +159,7 @@ export default function SearchResult({ ...@@ -137,7 +159,7 @@ export default function SearchResult({
); );
} }
const isItemActive = result => { const isItemActive = (result: WrappedResult) => {
switch (result.model) { switch (result.model) {
case "table": case "table":
return isSyncCompleted(result); return isSyncCompleted(result);
...@@ -146,7 +168,7 @@ const isItemActive = result => { ...@@ -146,7 +168,7 @@ const isItemActive = result => {
} }
}; };
const isItemLoading = result => { const isItemLoading = (result: WrappedResult) => {
switch (result.model) { switch (result.model) {
case "database": case "database":
case "table": case "table":
......
import { render, screen } from "@testing-library/react";
import { setupEnterpriseTest } from "__support__/enterprise";
import SearchResult from "./SearchResult";
function collection({
id = 1,
name = "Marketing",
authority_level = null,
getIcon = () => ({ name: "folder" }),
getUrl = () => `/collection/${id}`,
getCollection = () => {},
} = {}) {
const collection = {
id,
name,
authority_level,
getIcon,
getUrl,
getCollection,
model: "collection",
};
collection.collection = collection;
return collection;
}
describe("SearchResult > Collections", () => {
const regularCollection = collection();
describe("OSS", () => {
const officialCollection = collection({
authority_level: "official",
});
it("renders regular collection correctly", () => {
render(<SearchResult result={regularCollection} />);
expect(screen.getByText(regularCollection.name)).toBeInTheDocument();
expect(screen.getByText("Collection")).toBeInTheDocument();
expect(screen.getByLabelText("folder icon")).toBeInTheDocument();
expect(screen.queryByLabelText("badge icon")).not.toBeInTheDocument();
});
it("renders official collections as regular", () => {
render(<SearchResult result={officialCollection} />);
expect(screen.getByText(regularCollection.name)).toBeInTheDocument();
expect(screen.getByText("Collection")).toBeInTheDocument();
expect(screen.getByLabelText("folder icon")).toBeInTheDocument();
expect(screen.queryByLabelText("badge icon")).not.toBeInTheDocument();
});
});
describe("EE", () => {
const officialCollection = collection({
authority_level: "official",
getIcon: () => ({ name: "badge" }),
});
beforeAll(() => {
setupEnterpriseTest();
});
it("renders regular collection correctly", () => {
render(<SearchResult result={regularCollection} />);
expect(screen.getByText(regularCollection.name)).toBeInTheDocument();
expect(screen.getByText("Collection")).toBeInTheDocument();
expect(screen.getByLabelText("folder icon")).toBeInTheDocument();
expect(screen.queryByLabelText("badge icon")).not.toBeInTheDocument();
});
it("renders official collections correctly", () => {
render(<SearchResult result={officialCollection} />);
expect(screen.getByText(regularCollection.name)).toBeInTheDocument();
expect(screen.getByText("Official Collection")).toBeInTheDocument();
expect(screen.getByLabelText("badge icon")).toBeInTheDocument();
expect(screen.queryByLabelText("folder icon")).not.toBeInTheDocument();
});
});
});
import { render, screen } from "@testing-library/react";
import { setupEnterpriseTest } from "__support__/enterprise";
import { createMockSearchResult } from "metabase-types/api/mocks";
import { getIcon, queryIcon } from "__support__/ui";
import type { WrappedResult } from "./types";
import { SearchResult } from "./SearchResult";
const createWrappedSearchResult = (
options: Partial<WrappedResult>,
): WrappedResult => {
const result = createMockSearchResult(options);
return {
...result,
getUrl: options.getUrl ?? (() => "/collection/root"),
getIcon: options.getIcon ?? (() => ({ name: "folder" })),
getCollection: options.getCollection ?? (() => result.collection),
};
};
describe("SearchResult", () => {
it("renders a search result question item", () => {
const result = createWrappedSearchResult({
name: "My Item",
model: "card",
description: "My Item Description",
getIcon: () => ({ name: "table" }),
});
render(<SearchResult result={result} />);
expect(screen.getByText(result.name)).toBeInTheDocument();
expect(screen.getByText(result.description as string)).toBeInTheDocument();
expect(getIcon("table")).toBeInTheDocument();
});
it("renders a search result collection item", () => {
const result = createWrappedSearchResult({
name: "My Folder of Goodies",
model: "collection",
collection: {
id: 1,
name: "This should not appear",
authority_level: null,
},
});
render(<SearchResult result={result} />);
expect(screen.getByText(result.name)).toBeInTheDocument();
expect(screen.getByText("Collection")).toBeInTheDocument();
expect(screen.queryByText(result.collection.name)).not.toBeInTheDocument();
expect(getIcon("folder")).toBeInTheDocument();
});
});
describe("SearchResult > Collections", () => {
const resultInRegularCollection = createWrappedSearchResult({
name: "My Regular Item",
collection_authority_level: null,
collection: {
id: 1,
name: "Regular Collection",
authority_level: null,
},
});
const resultInOfficalCollection = createWrappedSearchResult({
name: "My Official Item",
collection_authority_level: "official",
collection: {
id: 1,
name: "Official Collection",
authority_level: "official",
},
});
describe("OSS", () => {
it("renders regular collection correctly", () => {
render(<SearchResult result={resultInRegularCollection} />);
expect(
screen.getByText(resultInRegularCollection.name),
).toBeInTheDocument();
expect(screen.getByText("Regular Collection")).toBeInTheDocument();
expect(getIcon("folder")).toBeInTheDocument();
expect(queryIcon("badge")).not.toBeInTheDocument();
});
it("renders official collections as regular", () => {
render(<SearchResult result={resultInOfficalCollection} />);
expect(
screen.getByText(resultInOfficalCollection.name),
).toBeInTheDocument();
expect(screen.getByText("Official Collection")).toBeInTheDocument();
expect(getIcon("folder")).toBeInTheDocument();
expect(queryIcon("badge")).not.toBeInTheDocument();
});
});
describe("EE", () => {
const resultInOfficalCollectionEE: WrappedResult = {
...resultInOfficalCollection,
getIcon: () => ({ name: "badge" }),
};
beforeAll(() => {
setupEnterpriseTest();
});
it("renders regular collection correctly", () => {
render(<SearchResult result={resultInRegularCollection} />);
expect(
screen.getByText(resultInRegularCollection.name),
).toBeInTheDocument();
expect(screen.getByText("Regular Collection")).toBeInTheDocument();
expect(getIcon("folder")).toBeInTheDocument();
expect(queryIcon("badge")).not.toBeInTheDocument();
});
it("renders official collections correctly", () => {
render(<SearchResult result={resultInOfficalCollectionEE} />);
expect(
screen.getByText(resultInOfficalCollectionEE.name),
).toBeInTheDocument();
expect(screen.getByText("Official Collection")).toBeInTheDocument();
expect(getIcon("badge")).toBeInTheDocument();
expect(queryIcon("folder")).not.toBeInTheDocument();
});
});
});
import type { IconName } from "metabase/core/components/Icon";
import type { SearchResult, Collection } from "metabase-types/api";
export interface WrappedResult extends SearchResult {
getUrl: () => string;
getIcon: () => {
name: IconName;
size?: number;
width?: number;
height?: number;
};
getCollection: () => Partial<Collection>;
}
...@@ -9,7 +9,7 @@ import Search from "metabase/entities/search"; ...@@ -9,7 +9,7 @@ import Search from "metabase/entities/search";
import Card from "metabase/components/Card"; import Card from "metabase/components/Card";
import EmptyState from "metabase/components/EmptyState"; import EmptyState from "metabase/components/EmptyState";
import SearchResult from "metabase/search/components/SearchResult"; import { SearchResult } from "metabase/search/components/SearchResult";
import Subhead from "metabase/components/type/Subhead"; import Subhead from "metabase/components/type/Subhead";
import { Icon } from "metabase/core/components/Icon"; import { Icon } from "metabase/core/components/Icon";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment