Skip to content

Commit

Permalink
feat: add support for 'links' on Results
Browse files Browse the repository at this point in the history
  • Loading branch information
jbottigliero committed Apr 23, 2024
1 parent a539724 commit 943f89d
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 44 deletions.
115 changes: 99 additions & 16 deletions src/components/Result.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React from "react";
import React, { useEffect } from "react";
import {
Heading,
Text,
Expand All @@ -16,14 +16,30 @@ import {
useDisclosure,
Divider,
Spacer,
ButtonGroup,
Link,
} from "@chakra-ui/react";

import { getAttribute, getAttributeFrom } from "../../static";
import {
getAttribute,
getValueFrom,
getValueFromAttribute,
} from "../../static";
import { Error } from "./Error";
import { isGError, type GError, type GMetaResult } from "@/globus/search";
import { Field, type FieldDefinition } from "./Field";
import { JSONTree } from "./JSONTree";

type LinkDefinition = {
label: string | { property: string; fallback?: string };
href:
| string
| {
property: string;
fallback?: string;
};
};

export type ResultComponentOptions = {
/**
* The field to use as the title for the result.
Expand Down Expand Up @@ -51,31 +67,86 @@ export type ResultComponentOptions = {
* ]
*/
fields?: FieldDefinition[];
links?: LinkDefinition[];
};

export default function Result({ result }: { result?: GMetaResult | GError }) {
const [heading, setHeading] = React.useState<string>();
const [summary, setSummary] = React.useState<string>();
type ProcessedLink = {
label: string | undefined;
href: string | undefined;
};

/**
* A basic wrapper for the `<Result />` component that will render a result or an error.
*/
export default function ResultWrapper({
result,
}: {
result?: GMetaResult | GError;
}) {
if (!result) {
return null;
}
if (isGError(result)) {
return <Error error={result} />;
}
return <Result result={result} />;
}

getAttributeFrom<string>(result, "components.ResultListing.heading").then(
(result) => {
setHeading(result);
},
);
function Result({ result }: { result: GMetaResult }) {
const [heading, setHeading] = React.useState<string>();
const [summary, setSummary] = React.useState<string>();
const [fields, setFields] = React.useState<FieldDefinition[]>([]);
const [links, setLinks] = React.useState<ProcessedLink[]>([]);

getAttributeFrom<string>(result, "components.ResultListing.summary").then(
(result) => {
setSummary(result);
},
);
useEffect(() => {
async function bootstrap() {
const heading = await getValueFromAttribute<string>(
result,
"components.ResultListing.heading",
);
const summary = await getValueFromAttribute<string>(
result,
"components.ResultListing.summary",
);
const fields = getAttribute("components.Result.fields", []);
const links = await Promise.all(
getAttribute("components.Result.links", []).map(
async (link: LinkDefinition) => {
const processedLink: ProcessedLink = {
label: undefined,
href: undefined,
};
if (typeof link.label === "string") {
processedLink.label = link.label;
} else {
processedLink.label = await getValueFrom<string>(
result,
link.label.property,
link.label.fallback,
);
}
if (typeof link.href === "string") {
processedLink.href = link.href;
} else {
processedLink.href = await getValueFrom<string>(
result,
link.href.property,
link.href.fallback,
);
}
console.log(processedLink);
return processedLink;
},
),
);

const fields = getAttribute("components.Result.fields", []);
setHeading(heading);
setSummary(summary);
setFields(fields);
setLinks(links);
}
bootstrap();
}, [result]);

return (
<>
Expand All @@ -85,6 +156,18 @@ export default function Result({ result }: { result?: GMetaResult | GError }) {

<Divider my={2} />

{links.length > 0 && (
<ButtonGroup>
{links.map((link: ProcessedLink, i: number) => {
return (
<Button key={link.href || i} as={Link} href={link.href} size="sm">
{link.label}
</Button>
);
})}
</ButtonGroup>
)}

<Flex>
<Box p="2">
{summary && (
Expand Down
9 changes: 4 additions & 5 deletions src/components/ResultListing.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useEffect } from "react";
import NextLink from "next/link";
import {
LinkBox,
Card,
CardHeader,
CardBody,
Expand All @@ -15,7 +14,7 @@ import {
HStack,
Link,
} from "@chakra-ui/react";
import { getAttributeFrom, getAttribute } from "../../static";
import { getValueFromAttribute, getAttribute } from "../../static";

import type { GMetaResult } from "@/globus/search";
import {
Expand Down Expand Up @@ -129,15 +128,15 @@ export default function ResultListing({ gmeta }: { gmeta: GMetaResult }) {

useEffect(() => {
async function resolveAttributes() {
const heading = await getAttributeFrom<string>(
const heading = await getValueFromAttribute<string>(
gmeta,
"components.ResultListing.heading",
);
const summary = await getAttributeFrom<string>(
const summary = await getValueFromAttribute<string>(
gmeta,
"components.ResultListing.summary",
);
let image = await getAttributeFrom<
let image = await getValueFromAttribute<
| string
| {
src: string;
Expand Down
66 changes: 43 additions & 23 deletions static.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _STATIC from "./static.json";
import { defaultsDeep, get } from "lodash";
import { defaultsDeep, get as _get } from "lodash";
import type { ResultComponentOptions } from "@/components/Result";
import { ResultListingComponentOptions } from "@/components/ResultListing";
import { ThemeSettings } from "@/theme";
Expand Down Expand Up @@ -45,7 +45,6 @@ export type Data = {
*/
version: string;
attributes: {

features?: {
/**
* Enable JSONata support for processing the `static.json` file.
Expand Down Expand Up @@ -197,45 +196,66 @@ export function getRedirectUri() {
}

/**
* Get a value by key (JSONPath) from the `static.json`.
* @private
*/
export function getAttribute(key: string, defaultValue?: any) {
return get(STATIC, `data.attributes.${key}`, defaultValue);
export function get(key: string, defaultValue?: any) {
return _get(STATIC, key, defaultValue);
}

let jsonata: typeof import("jsonata") | null = null;
/**
* Get an attribute (`data.attributes` member) by key from the `static.json`.
* @private
*/
export async function getAttributeFrom<T>(
obj: Record<string, any>,
key: string,
defaultValue?: T,
): Promise<T | undefined> {
const useJSONata = isFeatureEnabled("jsonata");
if (useJSONata && !jsonata) {
jsonata = (await import("jsonata")).default;
}
const lookup = getAttribute(key);
if (useJSONata && jsonata && lookup) {
const expression = jsonata(lookup);
return (await expression.evaluate(obj)) || defaultValue;
}
return get(obj, lookup, defaultValue);
export function getAttribute(key: string, defaultValue?: any) {
return get(`data.attributes.${key}`, defaultValue);
}

/**
* Whether or not a feature is enabled in the `static.json`.
* @private
*/
export function isFeatureEnabled(key: string, defaultValue?: boolean) {
return Boolean(get(STATIC, `data.attributes.features.${key}`, defaultValue));
return Boolean(get(`data.attributes.features.${key}`, defaultValue));
}

/**
* @private
*/
export function withFeature<T>(
key: string,
a: () => T,
b: () => T | null = () => null,
) {
return isFeatureEnabled(key) ? a() : b();
}

let jsonata: typeof import("jsonata") | null = null;

/**
* - Resolve a value for the provided attribute`key` from the `static.json` file.
* - Call `getValueFrom` with the resolved key.
* @private
*/
export async function getValueFromAttribute<T>(
obj: Record<string, unknown>,
key: string,
defaultValue?: T,
): Promise<T | undefined> {
const resolvedKey = getAttribute(key);
return await getValueFrom<T>(obj, resolvedKey, defaultValue);
}

export async function getValueFrom<T>(
obj: Record<string, any>,
key: string,
defaultValue?: T,
): Promise<T | undefined> {
const useJSONata = isFeatureEnabled("jsonata");
if (useJSONata && !jsonata) {
jsonata = (await import("jsonata")).default;
}
if (useJSONata && jsonata && key) {
const expression = jsonata(key);
return (await expression.evaluate(obj)) || defaultValue;
}
return _get(obj, key, defaultValue);
}

0 comments on commit 943f89d

Please sign in to comment.