diff --git a/packages/terriajs/lib/ReactViews/Preview/DataPreview.jsx b/packages/terriajs/lib/ReactViews/Preview/DataPreview.jsx
index a4cfb1aacb..4d0dedb465 100644
--- a/packages/terriajs/lib/ReactViews/Preview/DataPreview.jsx
+++ b/packages/terriajs/lib/ReactViews/Preview/DataPreview.jsx
@@ -15,6 +15,7 @@ import MappablePreview from "./MappablePreview";
import WarningBox from "./WarningBox";
import Styles from "./data-preview.scss";
import Icon from "../../Styled/Icon";
+import { getCustomPreviewRenderer } from "./PreviewRenderers";
/**
* Data preview section, for the preview map see DataPreviewMap
@@ -61,9 +62,11 @@ class DataPreview extends Component {
);
} else if (previewed && previewed.isMappable) {
+ const MappablePreviewRenderer =
+ getCustomPreviewRenderer(previewed.type) ?? MappablePreview;
return (
-
);
} else if (previewed && CatalogFunctionMixin.isMixedInto(previewed)) {
+ const FunctionRenderer =
+ getCustomPreviewRenderer(previewed.type) ?? InvokeFunction;
return (
-
);
} else if (previewed && previewed.isGroup) {
+ const GroupPreviewRenderer =
+ getCustomPreviewRenderer(previewed.type) ?? GroupPreview;
return (
-
;
+
+/**
+ * Custom preview renderer registry.
+ */
+const PreviewRenderers = observable(new Map());
+
+/**
+ * Add a custom preview renderer for the given catalog item type.
+ *
+ * Preview renderers, render the `About data` view.
+ *
+ * @param type Catalog item type
+ * @param renderer Preview renderer component
+ */
+export function addCustomPreviewRenderer(
+ type: string,
+ renderer: PreviewRendererType
+) {
+ runInAction(() => PreviewRenderers.set(type, renderer));
+}
+
+/**
+ * Remove custom preview renderer for the given catalog item type.
+ *
+ * The type falls back to its built-in preview renderer.
+ *
+ * @param type Catalog item type
+ * @returns `true` if a custom renderer was registered for the type, `false` otherwise.
+ */
+export function removeCustomPreviewRenderer(type: string): boolean {
+ return runInAction(() => PreviewRenderers.delete(type));
+}
+
+/**
+ * Get registered custom previewer renderer for the given catalog item type
+ *
+ * @param type Catalog item type
+ * @returns Registered custom renderer or `undefined` if no custom renderer is registered for the type.
+ */
+export function getCustomPreviewRenderer(
+ type: string
+): PreviewRendererType | undefined {
+ return PreviewRenderers.get(type);
+}
diff --git a/packages/terriajs/test/ReactViews/Preview/DataPreviewSpec.tsx b/packages/terriajs/test/ReactViews/Preview/DataPreviewSpec.tsx
new file mode 100644
index 0000000000..08ba51ada2
--- /dev/null
+++ b/packages/terriajs/test/ReactViews/Preview/DataPreviewSpec.tsx
@@ -0,0 +1,135 @@
+import { screen } from "@testing-library/react";
+import { runInAction } from "mobx";
+import CatalogMemberMixin from "../../../lib/ModelMixins/CatalogMemberMixin";
+import CatalogGroup from "../../../lib/Models/Catalog/CatalogGroup";
+import GeoJsonCatalogItem from "../../../lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem";
+import WebProcessingServiceCatalogFunction from "../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction";
+import CommonStrata from "../../../lib/Models/Definition/CommonStrata";
+import Terria from "../../../lib/Models/Terria";
+import ViewState from "../../../lib/ReactViewModels/ViewState";
+import DataPreview from "../../../lib/ReactViews/Preview/DataPreview";
+import {
+ PreviewRendererType,
+ addCustomPreviewRenderer,
+ removeCustomPreviewRenderer
+} from "../../../lib/ReactViews/Preview/PreviewRenderers";
+import { renderWithContexts } from "../withContext";
+
+const CustomRenderer: PreviewRendererType = ({
+ previewed
+}: {
+ previewed: CatalogMemberMixin.Instance;
+}) => Custom renderer for {previewed.name}
;
+
+describe("DataPreview", function () {
+ describe("preview rendering", function () {
+ let terria: Terria;
+ let viewState: ViewState;
+ let mappableItem: GeoJsonCatalogItem;
+ let functionItem: WebProcessingServiceCatalogFunction;
+ let groupItem: CatalogGroup;
+
+ beforeEach(function () {
+ terria = new Terria({ baseUrl: "./" });
+ viewState = new ViewState({ terria });
+
+ mappableItem = new GeoJsonCatalogItem("test-geojson", terria);
+ functionItem = new WebProcessingServiceCatalogFunction(
+ "test-wps",
+ terria
+ );
+ groupItem = new CatalogGroup("test-group", terria);
+
+ runInAction(() => {
+ mappableItem.setTrait(CommonStrata.definition, "name", "Test GeoJSON");
+ // Avoid spinning up the preview map, which isn't what we're testing here.
+ mappableItem.setTrait(CommonStrata.definition, "disablePreview", true);
+ functionItem.setTrait(CommonStrata.definition, "name", "Test WPS");
+ groupItem.setTrait(CommonStrata.definition, "name", "Test Group");
+ });
+ });
+
+ // The renderer registry is module global, so leave it as we found it.
+ afterEach(function () {
+ removeCustomPreviewRenderer(mappableItem.type);
+ removeCustomPreviewRenderer(functionItem.type);
+ removeCustomPreviewRenderer(groupItem.type);
+ });
+
+ function renderPreview(previewed: CatalogMemberMixin.Instance) {
+ return renderWithContexts(
+ ,
+ viewState
+ );
+ }
+
+ it("renders the built-in renderer when no custom renderer is registered", function () {
+ // MappablePreview, InvokeFunction and GroupPreview all title the preview
+ // with the catalog member name.
+ const mappablePreview = renderPreview(mappableItem);
+ expect(
+ screen.getByRole("heading", { name: "Test GeoJSON" })
+ ).toBeVisible();
+ expect(screen.queryByText(/Custom renderer for/)).toBeNull();
+ mappablePreview.unmount();
+
+ const functionPreview = renderPreview(functionItem);
+ expect(screen.getByRole("heading", { name: "Test WPS" })).toBeVisible();
+ expect(screen.queryByText(/Custom renderer for/)).toBeNull();
+ functionPreview.unmount();
+
+ const groupPreview = renderPreview(groupItem);
+ expect(screen.getByRole("heading", { name: "Test Group" })).toBeVisible();
+ expect(screen.queryByText(/Custom renderer for/)).toBeNull();
+ groupPreview.unmount();
+ });
+
+ it("renders the custom renderer registered for the catalog member type", function () {
+ addCustomPreviewRenderer(mappableItem.type, CustomRenderer);
+ addCustomPreviewRenderer(functionItem.type, CustomRenderer);
+ addCustomPreviewRenderer(groupItem.type, CustomRenderer);
+
+ const mappablePreview = renderPreview(mappableItem);
+ expect(
+ screen.getByText("Custom renderer for Test GeoJSON")
+ ).toBeVisible();
+ expect(
+ screen.queryByRole("heading", { name: "Test GeoJSON" })
+ ).toBeNull();
+ mappablePreview.unmount();
+
+ const functionPreview = renderPreview(functionItem);
+ expect(screen.getByText("Custom renderer for Test WPS")).toBeVisible();
+ expect(screen.queryByRole("heading", { name: "Test WPS" })).toBeNull();
+ functionPreview.unmount();
+
+ const groupPreview = renderPreview(groupItem);
+ expect(screen.getByText("Custom renderer for Test Group")).toBeVisible();
+ expect(screen.queryByRole("heading", { name: "Test Group" })).toBeNull();
+ groupPreview.unmount();
+ });
+
+ it("passes the preview props to the custom renderer", function () {
+ let receivedProps: Record | undefined;
+ const PropCapturingRenderer: PreviewRendererType = (props) => {
+ receivedProps = props;
+ return null;
+ };
+ addCustomPreviewRenderer(mappableItem.type, PropCapturingRenderer);
+
+ renderPreview(mappableItem);
+
+ expect(receivedProps).toEqual(
+ jasmine.objectContaining({
+ previewed: mappableItem,
+ terria,
+ viewState
+ })
+ );
+ });
+ });
+});