Skip to content

Commit 943f89d

Browse files
committed
feat: add support for 'links' on Results
1 parent a539724 commit 943f89d

File tree

3 files changed

+146
-44
lines changed

3 files changed

+146
-44
lines changed

src/components/Result.tsx

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useEffect } from "react";
44
import {
55
Heading,
66
Text,
@@ -16,14 +16,30 @@ import {
1616
useDisclosure,
1717
Divider,
1818
Spacer,
19+
ButtonGroup,
20+
Link,
1921
} from "@chakra-ui/react";
2022

21-
import { getAttribute, getAttributeFrom } from "../../static";
23+
import {
24+
getAttribute,
25+
getValueFrom,
26+
getValueFromAttribute,
27+
} from "../../static";
2228
import { Error } from "./Error";
2329
import { isGError, type GError, type GMetaResult } from "@/globus/search";
2430
import { Field, type FieldDefinition } from "./Field";
2531
import { JSONTree } from "./JSONTree";
2632

33+
type LinkDefinition = {
34+
label: string | { property: string; fallback?: string };
35+
href:
36+
| string
37+
| {
38+
property: string;
39+
fallback?: string;
40+
};
41+
};
42+
2743
export type ResultComponentOptions = {
2844
/**
2945
* The field to use as the title for the result.
@@ -51,31 +67,86 @@ export type ResultComponentOptions = {
5167
* ]
5268
*/
5369
fields?: FieldDefinition[];
70+
links?: LinkDefinition[];
5471
};
5572

56-
export default function Result({ result }: { result?: GMetaResult | GError }) {
57-
const [heading, setHeading] = React.useState<string>();
58-
const [summary, setSummary] = React.useState<string>();
73+
type ProcessedLink = {
74+
label: string | undefined;
75+
href: string | undefined;
76+
};
77+
78+
/**
79+
* A basic wrapper for the `<Result />` component that will render a result or an error.
80+
*/
81+
export default function ResultWrapper({
82+
result,
83+
}: {
84+
result?: GMetaResult | GError;
85+
}) {
5986
if (!result) {
6087
return null;
6188
}
6289
if (isGError(result)) {
6390
return <Error error={result} />;
6491
}
92+
return <Result result={result} />;
93+
}
6594

66-
getAttributeFrom<string>(result, "components.ResultListing.heading").then(
67-
(result) => {
68-
setHeading(result);
69-
},
70-
);
95+
function Result({ result }: { result: GMetaResult }) {
96+
const [heading, setHeading] = React.useState<string>();
97+
const [summary, setSummary] = React.useState<string>();
98+
const [fields, setFields] = React.useState<FieldDefinition[]>([]);
99+
const [links, setLinks] = React.useState<ProcessedLink[]>([]);
71100

72-
getAttributeFrom<string>(result, "components.ResultListing.summary").then(
73-
(result) => {
74-
setSummary(result);
75-
},
76-
);
101+
useEffect(() => {
102+
async function bootstrap() {
103+
const heading = await getValueFromAttribute<string>(
104+
result,
105+
"components.ResultListing.heading",
106+
);
107+
const summary = await getValueFromAttribute<string>(
108+
result,
109+
"components.ResultListing.summary",
110+
);
111+
const fields = getAttribute("components.Result.fields", []);
112+
const links = await Promise.all(
113+
getAttribute("components.Result.links", []).map(
114+
async (link: LinkDefinition) => {
115+
const processedLink: ProcessedLink = {
116+
label: undefined,
117+
href: undefined,
118+
};
119+
if (typeof link.label === "string") {
120+
processedLink.label = link.label;
121+
} else {
122+
processedLink.label = await getValueFrom<string>(
123+
result,
124+
link.label.property,
125+
link.label.fallback,
126+
);
127+
}
128+
if (typeof link.href === "string") {
129+
processedLink.href = link.href;
130+
} else {
131+
processedLink.href = await getValueFrom<string>(
132+
result,
133+
link.href.property,
134+
link.href.fallback,
135+
);
136+
}
137+
console.log(processedLink);
138+
return processedLink;
139+
},
140+
),
141+
);
77142

78-
const fields = getAttribute("components.Result.fields", []);
143+
setHeading(heading);
144+
setSummary(summary);
145+
setFields(fields);
146+
setLinks(links);
147+
}
148+
bootstrap();
149+
}, [result]);
79150

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

86157
<Divider my={2} />
87158

159+
{links.length > 0 && (
160+
<ButtonGroup>
161+
{links.map((link: ProcessedLink, i: number) => {
162+
return (
163+
<Button key={link.href || i} as={Link} href={link.href} size="sm">
164+
{link.label}
165+
</Button>
166+
);
167+
})}
168+
</ButtonGroup>
169+
)}
170+
88171
<Flex>
89172
<Box p="2">
90173
{summary && (

src/components/ResultListing.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useEffect } from "react";
22
import NextLink from "next/link";
33
import {
4-
LinkBox,
54
Card,
65
CardHeader,
76
CardBody,
@@ -15,7 +14,7 @@ import {
1514
HStack,
1615
Link,
1716
} from "@chakra-ui/react";
18-
import { getAttributeFrom, getAttribute } from "../../static";
17+
import { getValueFromAttribute, getAttribute } from "../../static";
1918

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

130129
useEffect(() => {
131130
async function resolveAttributes() {
132-
const heading = await getAttributeFrom<string>(
131+
const heading = await getValueFromAttribute<string>(
133132
gmeta,
134133
"components.ResultListing.heading",
135134
);
136-
const summary = await getAttributeFrom<string>(
135+
const summary = await getValueFromAttribute<string>(
137136
gmeta,
138137
"components.ResultListing.summary",
139138
);
140-
let image = await getAttributeFrom<
139+
let image = await getValueFromAttribute<
141140
| string
142141
| {
143142
src: string;

static.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _STATIC from "./static.json";
2-
import { defaultsDeep, get } from "lodash";
2+
import { defaultsDeep, get as _get } from "lodash";
33
import type { ResultComponentOptions } from "@/components/Result";
44
import { ResultListingComponentOptions } from "@/components/ResultListing";
55
import { ThemeSettings } from "@/theme";
@@ -45,7 +45,6 @@ export type Data = {
4545
*/
4646
version: string;
4747
attributes: {
48-
4948
features?: {
5049
/**
5150
* Enable JSONata support for processing the `static.json` file.
@@ -197,45 +196,66 @@ export function getRedirectUri() {
197196
}
198197

199198
/**
199+
* Get a value by key (JSONPath) from the `static.json`.
200200
* @private
201201
*/
202-
export function getAttribute(key: string, defaultValue?: any) {
203-
return get(STATIC, `data.attributes.${key}`, defaultValue);
202+
export function get(key: string, defaultValue?: any) {
203+
return _get(STATIC, key, defaultValue);
204204
}
205-
206-
let jsonata: typeof import("jsonata") | null = null;
207205
/**
206+
* Get an attribute (`data.attributes` member) by key from the `static.json`.
208207
* @private
209208
*/
210-
export async function getAttributeFrom<T>(
211-
obj: Record<string, any>,
212-
key: string,
213-
defaultValue?: T,
214-
): Promise<T | undefined> {
215-
const useJSONata = isFeatureEnabled("jsonata");
216-
if (useJSONata && !jsonata) {
217-
jsonata = (await import("jsonata")).default;
218-
}
219-
const lookup = getAttribute(key);
220-
if (useJSONata && jsonata && lookup) {
221-
const expression = jsonata(lookup);
222-
return (await expression.evaluate(obj)) || defaultValue;
223-
}
224-
return get(obj, lookup, defaultValue);
209+
export function getAttribute(key: string, defaultValue?: any) {
210+
return get(`data.attributes.${key}`, defaultValue);
225211
}
226212

227213
/**
228214
* Whether or not a feature is enabled in the `static.json`.
229215
* @private
230216
*/
231217
export function isFeatureEnabled(key: string, defaultValue?: boolean) {
232-
return Boolean(get(STATIC, `data.attributes.features.${key}`, defaultValue));
218+
return Boolean(get(`data.attributes.features.${key}`, defaultValue));
233219
}
234-
220+
/**
221+
* @private
222+
*/
235223
export function withFeature<T>(
236224
key: string,
237225
a: () => T,
238226
b: () => T | null = () => null,
239227
) {
240228
return isFeatureEnabled(key) ? a() : b();
241229
}
230+
231+
let jsonata: typeof import("jsonata") | null = null;
232+
233+
/**
234+
* - Resolve a value for the provided attribute`key` from the `static.json` file.
235+
* - Call `getValueFrom` with the resolved key.
236+
* @private
237+
*/
238+
export async function getValueFromAttribute<T>(
239+
obj: Record<string, unknown>,
240+
key: string,
241+
defaultValue?: T,
242+
): Promise<T | undefined> {
243+
const resolvedKey = getAttribute(key);
244+
return await getValueFrom<T>(obj, resolvedKey, defaultValue);
245+
}
246+
247+
export async function getValueFrom<T>(
248+
obj: Record<string, any>,
249+
key: string,
250+
defaultValue?: T,
251+
): Promise<T | undefined> {
252+
const useJSONata = isFeatureEnabled("jsonata");
253+
if (useJSONata && !jsonata) {
254+
jsonata = (await import("jsonata")).default;
255+
}
256+
if (useJSONata && jsonata && key) {
257+
const expression = jsonata(key);
258+
return (await expression.evaluate(obj)) || defaultValue;
259+
}
260+
return _get(obj, key, defaultValue);
261+
}

0 commit comments

Comments
 (0)