Skip to content

Commit 224351d

Browse files
authored
so much (#43)
1 parent fca423e commit 224351d

File tree

6 files changed

+116
-36
lines changed

6 files changed

+116
-36
lines changed

src/App.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import { selectResources, store } from './state/store';
1111
import { Provider } from 'react-redux'
1212
import { useAppSelector } from "./hooks/store";
1313

14+
function transformUrlForRouter(url: string): string {
15+
return url.replace(/\{([^}]+)\}/g, ':$1');
16+
}
17+
1418
function createRoutes(resources: ResourceSchema[]): RouteObject[] {
1519
const routes = [{
1620
path: "/",
@@ -21,21 +25,22 @@ function createRoutes(resources: ResourceSchema[]): RouteObject[] {
2125
element: <SpecSpecifierPage />,
2226
},
2327
].concat(resources.map((resource) => {
28+
const baseUrl = transformUrlForRouter(resource.base_url());
2429
return [
2530
{
26-
path: resource.base_url(),
31+
path: baseUrl,
2732
element: <ResourceListPage resource={resource} />
2833
},
2934
{
30-
path: `${resource.base_url()}/_create`,
35+
path: `${baseUrl}/_create`,
3136
element: <CreateForm resource={resource} />
3237
},
3338
{
34-
path: `${resource.base_url()}/:resourceId`,
39+
path: `${baseUrl}/:resourceId`,
3540
element: <InfoPage resource={resource} />
3641
},
3742
{
38-
path: `${resource.base_url()}/:resourceId/_update`,
43+
path: `${baseUrl}/:resourceId/_update`,
3944
element: <UpdateForm schema={resource} />
4045
}
4146
]

src/app/explorer/form.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
22
import { Button } from "@/components/ui/button"
33
import { Input } from "@/components/ui/input";
4-
import { useMemo, } from "react";
4+
import { useMemo, useEffect } from "react";
55
import { useForm } from "react-hook-form";
6-
import { useNavigate } from "react-router-dom";
6+
import { useNavigate, useParams } from "react-router-dom";
77
import { toast } from "@/hooks/use-toast";
88
import { useAppSelector } from "@/hooks/store";
99
import { selectHeaders } from "@/state/store";
@@ -15,6 +15,7 @@ type CreateFormProps = {
1515

1616
export default function CreateForm(props: CreateFormProps) {
1717
const form = useForm();
18+
const params = useParams();
1819
const navigate = useNavigate();
1920

2021
const headers = useAppSelector(selectHeaders);
@@ -52,6 +53,17 @@ export default function CreateForm(props: CreateFormProps) {
5253
});
5354
}, [props, form.control]);
5455

56+
useEffect(() => {
57+
// Set parent parameters from URL params, excluding resourceId
58+
const parentParams = new Map<string, string>();
59+
for (const [key, value] of Object.entries(params)) {
60+
if (key !== 'resourceId' && value) {
61+
parentParams.set(key, value);
62+
}
63+
}
64+
props.resource.parents = parentParams;
65+
}, [params, props.resource])
66+
5567
return (
5668
<Form {...form}>
5769
<form>

src/app/explorer/info.tsx

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,46 @@ import { useParams } from "react-router-dom";
33
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
44
import { ResourceInstance } from "@/state/fetch";
55
import { ResourceSchema } from "@/state/openapi";
6+
import { useAppSelector } from "@/hooks/store";
7+
import { selectChildResources } from "@/state/store";
8+
import ResourceListPage from "./resource_list";
9+
10+
type ResourceProperties = {
11+
path: string;
12+
[key: string]: string | number | boolean;
13+
}
614

715
type InfoPageProps = {
816
resource: ResourceSchema
917
}
1018

1119
export default function InfoPage(props: InfoPageProps) {
12-
const { resourceId }= useParams();
13-
const [state, setState] = useState<ResourceInstance>({});
20+
const params = useParams();
21+
const [state, setState] = useState<ResourceInstance | null>(null);
22+
23+
const childResources = useAppSelector((globalState) =>
24+
state ? selectChildResources(globalState, props.resource, (state.properties as ResourceProperties)?.path?.split('/').pop() || '') : []
25+
);
1426

1527
useEffect(() => {
16-
props.resource.get(resourceId!).then((instance) => setState(instance));
17-
}, [resourceId, props.resource])
28+
// Set parent parameters from URL params, excluding resourceId
29+
const parentParams = new Map<string, string>();
30+
for (const [key, value] of Object.entries(params)) {
31+
if (key !== 'resourceId' && value) {
32+
parentParams.set(key, value);
33+
}
34+
}
35+
props.resource.parents = parentParams;
36+
37+
// Fetch the resource instance
38+
props.resource.get(params['resourceId']!).then((instance) => setState(instance));
39+
}, [params, props.resource])
1840

1941
const properties = useMemo(() => {
20-
if(state.properties) {
21-
let results = [];
22-
for (const [key, value] of Object.entries(state.properties)) {
23-
results.push(<p>
42+
if(state?.properties) {
43+
const results = [];
44+
for (const [key, value] of Object.entries(state.properties as ResourceProperties)) {
45+
results.push(<p key={key}>
2446
<b>{key}:</b> {value}
2547
</p>)
2648
}
@@ -30,13 +52,23 @@ export default function InfoPage(props: InfoPageProps) {
3052
}, [state]);
3153

3254
return (
33-
<Card>
34-
<CardHeader>
35-
<CardTitle>{state.properties?.path}</CardTitle>
36-
</CardHeader>
37-
<CardContent>
38-
{properties}
39-
</CardContent>
40-
</Card>
55+
<div className="space-y-6">
56+
{/* Resource Instance card. */}
57+
<Card>
58+
<CardHeader>
59+
<CardTitle>{(state?.properties as ResourceProperties)?.path}</CardTitle>
60+
</CardHeader>
61+
<CardContent>
62+
{properties}
63+
</CardContent>
64+
</Card>
65+
66+
{/* Listing Child Resources */}
67+
{childResources.map((childResource) => (
68+
<div key={childResource.singular_name} className="mt-6">
69+
<ResourceListPage resource={childResource} />
70+
</div>
71+
))}
72+
</div>
4173
)
4274
}

src/app/explorer/resource_list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function ResourceListPage(props: ResourceListProps) {
4545
<div className="flex items-center justify-between mb-4">
4646
<h1>{props.resource.plural_name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}</h1>
4747
<div className="flex gap-2">
48-
<Button variant="outline" size="icon" onClick={() => navigate("_create")}>
48+
<Button variant="outline" size="icon" onClick={() => navigate(props.resource.substituteUrlParameters(props.resource.base_url()) + "/_create")}>
4949
<Plus className="h-4 w-4" />
5050
</Button>
5151
<Button variant="outline" size="icon" onClick={refreshList}>

src/state/openapi.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {List, ResourceInstance, Create, Get} from './fetch';
1+
import { List, ResourceInstance, Create, Get } from './fetch';
22
// Responsible for handling the schema for a given resource.
33
class ResourceSchema {
44
schema: any;
55
singular_name: string;
66
plural_name: string;
77
server_url: string;
8-
parents: Map<string, ResourceInstance>;
8+
parents: Map<string, string>;
99

1010
constructor(
1111
singular_name: string,
@@ -20,22 +20,17 @@ class ResourceSchema {
2020
this.parents = new Map();
2121
}
2222

23-
private substituteUrlParameters(url: string): string {
23+
public substituteUrlParameters(url: string): string {
2424
const paramRegex = /\{([^}]+)\}/g;
2525
let match;
2626
let resultUrl = url;
2727

2828
while ((match = paramRegex.exec(url)) !== null) {
2929
const paramName = match[1];
30-
const parent = this.parents.get(paramName);
31-
32-
if (!parent) {
33-
throw new Error(`Missing required parent resource: ${paramName}`);
34-
}
30+
const parentId = this.parents.get(paramName);
3531

36-
const parentId = parent.properties.id;
3732
if (!parentId) {
38-
throw new Error(`Parent resource ${paramName} has no id property`);
33+
throw new Error(`Missing required parent resource: ${paramName}`);
3934
}
4035

4136
resultUrl = resultUrl.replace(`{${paramName}}`, parentId);
@@ -60,7 +55,7 @@ class ResourceSchema {
6055
const baseUrl = this.base_url();
6156
let url = `${this.server_url}${baseUrl}`;
6257
if (this.properties().find(prop => prop.name === 'id')) {
63-
url += `?id=${body.id}`;
58+
url += `?id=${body.id}`;
6459
}
6560
url = this.substituteUrlParameters(url);
6661
return Create(url, body, headers);
@@ -69,7 +64,7 @@ class ResourceSchema {
6964
base_url(): string {
7065
const pattern = this.schema["x-aep-resource"]["patterns"][0];
7166
const subset = pattern.substring(0, pattern.lastIndexOf("/"));
72-
if(subset[0] != "/") {
67+
if (subset[0] != "/") {
7368
return "/" + subset;
7469
}
7570
return subset;
@@ -132,6 +127,34 @@ class OpenAPI {
132127
}
133128
throw new Error(`Resource not found: ${plural}`);
134129
}
130+
131+
childResources(r: ResourceSchema, id: string): ResourceSchema[] {
132+
const children: ResourceSchema[] = [];
133+
const allResources = this.resources();
134+
135+
for (const resource of allResources) {
136+
const parents = resource.schema["x-aep-resource"]["parents"] || [];
137+
// Get all valid parent names (current resource + its parents)
138+
const validParents = new Set([r.singular_name, ...r.parents.keys()]);
139+
140+
// Check if all parents in the resource's parents array are valid
141+
const allParentsValid =
142+
parents.length === validParents.size &&
143+
parents.every(parent => validParents.has(parent));
144+
145+
if (allParentsValid) {
146+
// Copy parent relationships from the parent resource
147+
for (const [key, value] of r.parents.entries()) {
148+
resource.parents.set(key, value);
149+
}
150+
// Add the parent resource's ID
151+
resource.parents.set(r.singular_name, id);
152+
children.push(resource);
153+
}
154+
}
155+
156+
return children;
157+
}
135158
}
136159

137160
function parseOpenAPI(jsonString: string): OpenAPI {

src/state/store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { configureStore } from '@reduxjs/toolkit'
22
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
3-
import { OpenAPI } from './openapi'
3+
import { OpenAPI, ResourceSchema } from './openapi'
44

55
// Schema reducers + selectors.
66
interface SchemaState {
@@ -35,6 +35,14 @@ export const selectResources = (state: RootState) => {
3535
}
3636
}
3737

38+
export const selectChildResources = (state: RootState, resource: ResourceSchema, id: string) => {
39+
if (state.schema.value != null) {
40+
return state.schema.value.childResources(resource, id);
41+
} else {
42+
return [];
43+
}
44+
}
45+
3846
export const schemaState = (state: RootState) => state.schema.state
3947

4048
// Headers reducers + selectors.

0 commit comments

Comments
 (0)