Compare commits

...

4 Commits

Author SHA1 Message Date
Raphael Krut-Landau
40f187cc74 fix(webapp): Fix collection path overlap issues 2024-08-05 16:38:07 -04:00
Raphael Krut-Landau
db8490e760 fix(webapp/admin): Move collection path component into common/ for future re-use in Dashboard and question caching 2024-08-05 16:38:07 -04:00
Case Nelson
afdbd6191e fix: mongo ObjectId should be orderable (#46501)
* fix: mongo ObjectId should be orderable

Fixes #46259

Mongo ObjectId derive :type/TextLike but that can't be an orderable
type, so we mark the specific Mongo type as orderable for malli schemas.

* Fix test
2024-08-05 16:38:07 -04:00
Raphael Krut-Landau
88bfb3d680 fix(webapp/admin): Move collection path component into common/ for future re-use in Dashboard and question caching 2024-08-05 16:38:06 -04:00
16 changed files with 117 additions and 125 deletions

View File

@@ -1,13 +1,18 @@
import {
type PropsWithChildren,
useEffect,
useState,
type CSSProperties,
type PropsWithChildren,
} from "react";
import { push } from "react-router-redux";
import { t } from "ttag";
import { getCollectionName } from "metabase/collections/utils";
import {
CollectionPath,
SimpleCollectionDisplay,
} from "metabase/common/components/CollectionPath/CollectionPath";
import { CollectionsIcon } from "metabase/common/components/CollectionPath/CollectionPath.styled";
import { useLocale } from "metabase/common/hooks/use-locale/use-locale";
import EntityItem from "metabase/components/EntityItem";
import { SortableColumnHeader } from "metabase/components/ItemsTable/BaseItemsTable";
@@ -27,9 +32,9 @@ import * as Urls from "metabase/lib/urls";
import {
Flex,
Icon,
type IconProps,
type IconName,
Skeleton,
type IconName,
type IconProps,
} from "metabase/ui";
import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat";
import { SortDirection, type SortingOptions } from "metabase-types/api/sorting";
@@ -38,11 +43,6 @@ import { trackModelClick } from "../analytics";
import type { ModelResult } from "../types";
import { getIcon } from "../utils";
import {
CollectionBreadcrumbsWithTooltip,
SimpleCollectionDisplay,
} from "./CollectionBreadcrumbsWithTooltip";
import { CollectionsIcon } from "./CollectionBreadcrumbsWithTooltip.styled";
import { EllipsifiedWithMarkdownTooltip } from "./EllipsifiedWithMarkdownTooltip";
import {
ModelCell,
@@ -243,7 +243,7 @@ const TBodyRow = ({
{simpleDisplay ? (
<SimpleCollectionDisplay collection={model.collection} />
) : (
<CollectionBreadcrumbsWithTooltip
<CollectionPath
containerName={containerName}
collection={model.collection}
/>

View File

@@ -1,36 +1,10 @@
import { t } from "ttag";
import type { CollectionEssentials, SearchResult } from "metabase-types/api";
import { getCollectionPathString } from "metabase/common/components/CollectionPath/utils";
import type { SearchResult } from "metabase-types/api";
import { SortDirection, type SortingOptions } from "metabase-types/api/sorting";
import type { ModelResult } from "../types";
import { getCollectionName } from "../utils";
import { pathSeparatorChar } from "./constants";
export const getBreadcrumbMaxWidths = (
collections: CollectionEssentials["effective_ancestors"],
totalUnitsOfWidthAvailable: number,
isPathEllipsified: boolean,
) => {
if (!collections || collections.length < 2) {
return [];
}
const lengths = collections.map(
collection => getCollectionName(collection).length,
);
const ratio = lengths[0] / (lengths[0] + lengths[1]);
const firstWidth = Math.max(
Math.round(ratio * totalUnitsOfWidthAvailable),
25,
);
const secondWidth = totalUnitsOfWidthAvailable - firstWidth;
const padding = isPathEllipsified ? "2rem" : "1rem";
return [
`calc(${firstWidth}cqw - ${padding})`,
`calc(${secondWidth}cqw - ${padding})`,
];
};
export const isModel = (item: SearchResult) => item.model === "dataset";
@@ -42,16 +16,6 @@ export const getModelDescription = (item: SearchResult) => {
}
};
export const getCollectionPathString = (collection: CollectionEssentials) => {
const ancestors: CollectionEssentials[] =
collection.effective_ancestors || [];
const collections = ancestors.concat(collection);
const pathString = collections
.map(coll => getCollectionName(coll))
.join(` ${pathSeparatorChar} `);
return pathString;
};
const getValueForSorting = (
model: ModelResult,
sort_column: keyof ModelResult,

View File

@@ -4,37 +4,7 @@ import { SortDirection } from "metabase-types/api/sorting";
import { createMockModelResult } from "../test-utils";
import type { ModelResult } from "../types";
import {
getCollectionPathString,
sortModels,
getMaxRecentModelCount,
} from "./utils";
describe("getCollectionPathString", () => {
it("should return path for collection without ancestors", () => {
const collection = createMockCollection({
id: 0,
name: "Documents",
effective_ancestors: [],
});
const pathString = getCollectionPathString(collection);
expect(pathString).toBe("Documents");
});
it("should return path for collection with multiple ancestors", () => {
const ancestors = [
createMockCollection({ name: "Home" }),
createMockCollection({ name: "User" }),
createMockCollection({ name: "Files" }),
];
const collection = createMockCollection({
name: "Documents",
effective_ancestors: ancestors,
});
const pathString = getCollectionPathString(collection);
expect(pathString).toBe("Home / User / Files / Documents");
});
});
import { sortModels, getMaxRecentModelCount } from "./utils";
describe("sortModels", () => {
let id = 0;

View File

@@ -1,15 +1,9 @@
import type { MutableRefObject } from "react";
import type {
RecentCollectionItem,
RecentItem,
SearchResult,
} from "metabase-types/api";
export type RefProp<RefValue> = {
ref: MutableRefObject<RefValue> | ((value: RefValue) => void);
};
/** Model retrieved through the search endpoint */
export type ModelResult = SearchResult<number, "dataset">;

View File

@@ -1,31 +1,11 @@
import type { Dispatch, SetStateAction } from "react";
import { t } from "ttag";
import _ from "underscore";
import {
canonicalCollectionId,
coerceCollectionId,
isRootCollection,
} from "metabase/collections/utils";
import { entityForObject } from "metabase/lib/schema";
import type { IconName } from "metabase/ui";
import type { CollectionEssentials } from "metabase-types/api";
import type { FilterableModel } from "./types";
export const getCollectionName = (collection: CollectionEssentials) => {
if (isRootCollection(collection)) {
return t`Our analytics`;
}
return collection?.name || t`Untitled collection`;
};
/** The root collection's id might be null or 'root' in different contexts.
* Use 'root' instead of null, for the sake of sorting */
export const getCollectionIdForSorting = (collection: CollectionEssentials) => {
return coerceCollectionId(canonicalCollectionId(collection.id));
};
export type AvailableModelFilters = Record<
string,
{

View File

@@ -1,5 +1,6 @@
import styled from "@emotion/styled";
import { doNotForwardProps } from "metabase/common/utils/doNotForwardProps";
import { ResponsiveChild } from "metabase/components/ResponsiveContainer/ResponsiveContainer";
import Link from "metabase/core/components/Link";
import { FixedSizeIcon, Flex, Group } from "metabase/ui";
@@ -9,26 +10,31 @@ import { Ellipsis } from "./Ellipsis";
/** When a cell is narrower than this width, breadcrumbs within it change significantly */
const breadcrumbBreakpoint = "10rem";
export const Breadcrumb = styled(ResponsiveChild)<{
export const Breadcrumb = styled(
ResponsiveChild,
doNotForwardProps("maxWidth", "isSoleBreadcrumb", "index"),
)<{
maxWidth: string;
isSoleBreadcrumb: boolean;
index: number;
}>`
${({ maxWidth }) => {
return maxWidth ? `td & { max-width: ${maxWidth} };` : "";
${({ maxWidth, isSoleBreadcrumb }) => {
return maxWidth
? `td & { max-width: ${maxWidth} };`
: `max-width: calc(95cqw - ${isSoleBreadcrumb ? 1 : 3}rem);`;
}}
color: var(--mb-color-text-dark);
line-height: 1;
line-height: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: 1px;
padding-bottom: 1px;
${props => {
return `
@container ${props.containerName} (width < ${breadcrumbBreakpoint}) {
${props.index === 0 && !props.isSoleBreadcrumb ? `display: none;` : ""}
td & { max-width: calc(95cqw - ${props.isSoleBreadcrumb ? 1 : 3}rem); };
td & { max-width: calc(95cqw - ${
props.isSoleBreadcrumb ? 1 : 2.25
}rem); };
}`;
}}
`;

View File

@@ -16,16 +16,17 @@ import {
CollectionLink,
CollectionsIcon,
InitialEllipsis,
} from "./CollectionBreadcrumbsWithTooltip.styled";
} from "./CollectionPath.styled";
import { Ellipsis } from "./Ellipsis";
import { PathSeparator } from "./PathSeparator";
import { getBreadcrumbMaxWidths, getCollectionPathString } from "./utils";
export const CollectionBreadcrumbsWithTooltip = ({
export const CollectionPath = ({
collection,
containerName,
}: {
collection: CollectionEssentials;
/** Must be a valid css identifier. "a_b" is a valid value, but "a.b" is not */
containerName: string;
}) => {
const collections = (

View File

@@ -2,14 +2,11 @@ import { renderWithProviders, screen } from "__support__/ui";
import type { Collection } from "metabase-types/api";
import { createMockCollection } from "metabase-types/api/mocks";
import { CollectionBreadcrumbsWithTooltip } from "./CollectionBreadcrumbsWithTooltip";
import { CollectionPath } from "./CollectionPath";
const setup = (collection: Collection) => {
return renderWithProviders(
<CollectionBreadcrumbsWithTooltip
collection={collection}
containerName="Container"
/>,
<CollectionPath collection={collection} containerName="Container" />,
);
};
const collectionAlpha = createMockCollection({ id: 99, name: "Alpha" });
@@ -24,7 +21,7 @@ const collectionCharlie = createMockCollection({
effective_ancestors: [collectionAlpha, collectionBeta],
});
describe("CollectionBreadcrumbsWithTooltip", () => {
describe("CollectionPath", () => {
it("should show a single collection", () => {
setup(collectionAlpha);
expect(screen.getByLabelText("Alpha")).toBeInTheDocument();

View File

@@ -6,7 +6,7 @@ import { Text } from "metabase/ui";
import type { RefProp } from "../types";
import { EllipsisAndSeparator } from "./CollectionBreadcrumbsWithTooltip.styled";
import { EllipsisAndSeparator } from "./CollectionPath.styled";
import { PathSeparator } from "./PathSeparator";
type EllipsisProps = {
includeSep?: boolean;

View File

@@ -0,0 +1,47 @@
import { t } from "ttag";
import { isRootCollection } from "metabase/collections/utils";
import type { CollectionEssentials } from "metabase-types/api";
import { pathSeparatorChar } from "./constants";
export const getCollectionName = (collection: CollectionEssentials) => {
if (isRootCollection(collection)) {
return t`Our analytics`;
}
return collection?.name || t`Untitled collection`;
};
export const getBreadcrumbMaxWidths = (
collections: CollectionEssentials["effective_ancestors"],
totalUnitsOfWidthAvailable: number,
isPathEllipsified: boolean,
) => {
if (!collections || collections.length < 2) {
return [];
}
const lengths = collections.map(
collection => getCollectionName(collection).length,
);
const ratio = lengths[0] / (lengths[0] + lengths[1]);
const firstWidth = Math.max(
Math.round(ratio * totalUnitsOfWidthAvailable),
25,
);
const secondWidth = totalUnitsOfWidthAvailable - firstWidth;
const padding = isPathEllipsified ? "2rem" : "1rem";
return [
`calc(${firstWidth}cqw - ${padding})`,
`calc(${secondWidth}cqw - ${padding})`,
];
};
export const getCollectionPathString = (collection: CollectionEssentials) => {
const ancestors: CollectionEssentials[] =
collection.effective_ancestors || [];
const collections = ancestors.concat(collection);
const pathString = collections
.map(coll => getCollectionName(coll))
.join(` ${pathSeparatorChar} `);
return pathString;
};

View File

@@ -0,0 +1,29 @@
import { createMockCollection } from "metabase-types/api/mocks";
import { getCollectionPathString } from "./utils";
describe("getCollectionPathString", () => {
it("should return path for collection without ancestors", () => {
const collection = createMockCollection({
id: 0,
name: "Documents",
effective_ancestors: [],
});
const pathString = getCollectionPathString(collection);
expect(pathString).toBe("Documents");
});
it("should return path for collection with multiple ancestors", () => {
const ancestors = [
createMockCollection({ name: "Home" }),
createMockCollection({ name: "User" }),
createMockCollection({ name: "Files" }),
];
const collection = createMockCollection({
name: "Documents",
effective_ancestors: ancestors,
});
const pathString = getCollectionPathString(collection);
expect(pathString).toBe("Home / User / Files / Documents");
});
});

View File

@@ -0,0 +1,5 @@
import type { MutableRefObject } from "react";
export type RefProp<RefValue> = {
ref: MutableRefObject<RefValue> | ((value: RefValue) => void);
};

View File

@@ -1,6 +1,6 @@
import styled from "@emotion/styled";
import type { RefProp } from "metabase/browse/types";
import type { RefProp } from "metabase/common/components/types";
import { doNotForwardProps } from "metabase/common/utils/doNotForwardProps";
import { Box, type BoxProps } from "metabase/ui";

View File

@@ -40,10 +40,9 @@ const getIsTruncated = (element: Element): boolean => {
range.selectNodeContents(element);
const elementRect = element.getBoundingClientRect();
const rangeRect = range.getBoundingClientRect();
return (
rangeRect.height > elementRect.height || rangeRect.width > elementRect.width
);
const tooTall = rangeRect.height - elementRect.height > 0.01;
const tooWide = rangeRect.width - elementRect.width > 0.01;
return tooTall || tooWide;
};
export const useAreAnyTruncated = <E extends Element>({