Skip to content

Commit

Permalink
feat: additional properties UX (#1631)
Browse files Browse the repository at this point in the history
  • Loading branch information
RohinBhargava authored Oct 10, 2024
1 parent dc8d75d commit 7f41717
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 37 deletions.
1 change: 1 addition & 0 deletions fern/apis/fdr/generators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ groups:
config:
useBrandedStringAliases: true
noSerdeLayer: true
noOptionalProperties: true
outputSourceFiles: true
neverThrowErrors: true
timeoutInSeconds: infinity
Expand Down
7 changes: 5 additions & 2 deletions packages/fdr-sdk/src/api-definition/unwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type UnwrappedReference = {

export type UnwrappedObjectType = {
properties: Latest.ObjectProperty[];
extraProperties: Latest.TypeReference | undefined;
descriptions: FernDocs.MarkdownText[];
};

Expand Down Expand Up @@ -169,6 +170,7 @@ export function unwrapObjectType(
types: Record<string, Latest.TypeDefinition>,
): UnwrappedObjectType {
const directProperties = object.properties;
const extraProperties = object.extraProperties;
const descriptions: FernDocs.MarkdownText[] = [];
const extendedProperties = object.extends.flatMap((typeId): Latest.ObjectProperty[] => {
const typeDef = types[typeId];
Expand Down Expand Up @@ -230,7 +232,7 @@ export function unwrapObjectType(
(property) => unwrapReference(property.valueShape, types)?.isOptional,
(property) => AvailabilityOrder.indexOf(property.availability ?? Latest.Availability.Stable),
);
return { properties, descriptions };
return { properties, extraProperties, descriptions };
}
const propertyKeys = new Set(object.properties.map((property) => property.key));
const filteredExtendedProperties = extendedProperties.filter(
Expand All @@ -245,7 +247,7 @@ export function unwrapObjectType(
(property) => AvailabilityOrder.indexOf(property.availability ?? Latest.Availability.Stable),
(property) => property.key,
);
return { properties, descriptions };
return { properties, extraProperties, descriptions };
}

/**
Expand All @@ -270,6 +272,7 @@ export function unwrapDiscriminatedUnionVariant(
},
...properties,
],
extraProperties: undefined,
descriptions,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const DiscriminatedUnionVariant: React.FC<DiscriminatedUnionVariant.Props
},
...dereferenceObjectProperties(unionVariant, types),
],
extraProperties: undefined,
name: undefined,
description: undefined,
availability: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils";
import { Plus } from "iconoir-react";
import React, { ReactElement } from "react";
import { Markdown } from "../../../mdx/Markdown";
import { ResolvedTypeDefinition, ResolvedTypeShape, unwrapReference } from "../../../resolver/types";
import { InternalTypeDefinition } from "../type-definition/InternalTypeDefinition";
import { InternalTypeDefinitionError } from "../type-definition/InternalTypeDefinitionError";
Expand Down Expand Up @@ -90,13 +92,21 @@ export const InternalTypeReferenceDefinitions: React.FC<InternalTypeReferenceDef
const InternalShapeRenderer = applyErrorStyles ? InternalTypeDefinitionError : InternalTypeDefinition;
return visitDiscriminatedUnion(unwrapReference(shape, types), "type")._visit<ReactElement | null>({
object: (object) => (
<InternalShapeRenderer
typeShape={object}
isCollapsible={isCollapsible}
anchorIdParts={anchorIdParts}
slug={slug}
types={types}
/>
<div>
<InternalShapeRenderer
typeShape={object}
isCollapsible={isCollapsible}
anchorIdParts={anchorIdParts}
slug={slug}
types={types}
/>
{object.extraProperties != null && (
<div className="flex pt-2">
<Plus />
<Markdown mdx="Optional Additional Properties" className="!t-muted" size="sm" />
</div>
)}
</div>
),
enum: (enum_) => (
<InternalShapeRenderer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const PlaygroundWebSocketHandshakeForm: FC<PlaygroundWebSocketHandshakeFo
<PlaygroundObjectPropertiesForm
id="header"
properties={channel.requestHeaders}
extraProperties={undefined}
onChange={setHeaders}
value={formState?.headers}
types={types}
Expand All @@ -102,6 +103,7 @@ export const PlaygroundWebSocketHandshakeForm: FC<PlaygroundWebSocketHandshakeFo
<PlaygroundObjectPropertiesForm
id="path"
properties={channel.pathParameters}
extraProperties={undefined}
onChange={setPathParameters}
value={formState?.pathParameters}
types={types}
Expand All @@ -120,6 +122,7 @@ export const PlaygroundWebSocketHandshakeForm: FC<PlaygroundWebSocketHandshakeFo
<PlaygroundObjectPropertiesForm
id="query"
properties={channel.queryParameters}
extraProperties={undefined}
onChange={setQueryParameters}
value={formState?.queryParameters}
types={types}
Expand Down
33 changes: 21 additions & 12 deletions packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
<PlaygroundObjectPropertiesForm
id="header"
properties={endpoint.requestHeaders ?? EMPTY_ARRAY}
extraProperties={undefined}
onChange={setHeaders}
value={formState?.headers}
types={types}
Expand All @@ -114,6 +115,7 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
<PlaygroundObjectPropertiesForm
id="path"
properties={endpoint.pathParameters ?? EMPTY_ARRAY}
extraProperties={undefined}
onChange={setPathParameters}
value={formState?.pathParameters}
types={types}
Expand All @@ -126,6 +128,7 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
<PlaygroundObjectPropertiesForm
id="query"
properties={endpoint.queryParameters ?? EMPTY_ARRAY}
extraProperties={undefined}
onChange={setQueryParameters}
value={formState?.queryParameters}
types={types}
Expand Down Expand Up @@ -162,26 +165,32 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
/>
</PlaygroundEndpointFormSection>
),
object: (value) => (
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body Parameters">
<PlaygroundObjectPropertiesForm
id="body"
properties={unwrapObjectType(value, types).properties}
onChange={setBodyJson}
value={formState?.body?.value}
types={types}
/>
</PlaygroundEndpointFormSection>
),
object: (value) => {
const unwrappedObjectType = unwrapObjectType(value, types);
return (
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body Parameters">
<PlaygroundObjectPropertiesForm
id="body"
properties={unwrappedObjectType.properties}
extraProperties={unwrappedObjectType.extraProperties}
onChange={setBodyJson}
value={formState?.body?.value}
types={types}
/>
</PlaygroundEndpointFormSection>
);
},
alias: (alias) => {
const { shape, isOptional } = unwrapReference(alias.value, types);

if (shape.type === "object" && !isOptional) {
const unwrappedObjectType = unwrapObjectType(shape, types);
return (
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body Parameters">
<PlaygroundObjectPropertiesForm
id="body"
properties={unwrapObjectType(shape, types).properties}
properties={unwrappedObjectType.properties}
extraProperties={unwrappedObjectType.extraProperties}
onChange={setBodyJson}
value={formState?.body?.value}
types={types}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const PlaygroundDiscriminatedUnionForm = memo<PlaygroundDiscriminatedUnio
<div className="border-l border-border-default-soft pl-4">
<PlaygroundObjectPropertiesForm
properties={properties}
extraProperties={undefined}
value={value}
onChange={onChange}
id={id}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ObjectProperty, TypeDefinition, unwrapReference } from "@fern-api/fdr-sdk/api-definition";
import {
ObjectProperty,
PropertyKey,
TypeDefinition,
TypeReference,
unwrapReference,
} from "@fern-api/fdr-sdk/api-definition";
import { FernButton, FernDropdown } from "@fern-ui/components";
import { useBooleanState } from "@fern-ui/react-commons";
import cn from "clsx";
Expand Down Expand Up @@ -75,6 +81,7 @@ export const PlaygroundObjectPropertyForm: FC<PlaygroundObjectPropertyFormProps>
interface PlaygroundObjectPropertiesFormProps {
id: string;
properties: readonly ObjectProperty[];
extraProperties: TypeReference | undefined;
onChange: (value: unknown) => void;
value: unknown;
indent?: boolean;
Expand All @@ -83,7 +90,8 @@ interface PlaygroundObjectPropertiesFormProps {
}

export const PlaygroundObjectPropertiesForm = memo<PlaygroundObjectPropertiesFormProps>((props) => {
const { id, properties, onChange, value, indent = false, types, disabled } = props;
const { id, properties, onChange, value, indent = false, types, disabled, extraProperties } = props;

const onChangeObjectProperty = useCallback(
(key: string, newValue: unknown) => {
onChange((oldValue: unknown) => {
Expand All @@ -101,6 +109,43 @@ export const PlaygroundObjectPropertiesForm = memo<PlaygroundObjectPropertiesFor
},
[onChange],
);

const [additionalProperties, setAdditionalProperties] = useState<unknown>({});

const onChangeAdditionalObjectProperty = useCallback(
(key: string, newValue: unknown) => {
onChange((oldValue: unknown) => {
const oldObject = castToRecord(oldValue);
const val = castToRecord(newValue);

if (newValue === undefined) {
return Object.fromEntries(
Object.entries(oldObject).filter(
([k]) => !Object.keys(castToRecord(additionalProperties)).includes(k),
),
);
} else {
if (
JSON.stringify(Object.keys(castToRecord(additionalProperties))) !==
JSON.stringify(Object.keys(val))
) {
setAdditionalProperties(newValue);
}

return {
...Object.fromEntries(
Object.entries(oldObject).filter(
([k]) => !Object.keys(castToRecord(additionalProperties)).includes(k),
),
),
...newValue,
};
}
});
},
[onChange, additionalProperties],
);

const shownProperties = useMemo(() => {
return properties.filter((property) =>
shouldShowProperty(property.valueShape, castToRecord(value)[property.key]),
Expand Down Expand Up @@ -207,6 +252,55 @@ export const PlaygroundObjectPropertiesForm = memo<PlaygroundObjectPropertiesFor
/>
</FernDropdown>
)}

{extraProperties != null && (
<div className={cn("flex-1 shrink min-w-0 mt-8")}>
<PlaygroundObjectPropertyForm
id={"extraProperties"}
property={{
key: PropertyKey("Optional Extra Properties"),
valueShape: {
type: "optional",
shape: {
type: "map",
keyShape: {
type: "primitive",
value: {
type: "string",
regex: undefined,
minLength: undefined,
maxLength: undefined,
default: undefined,
},
},
valueShape: {
type: "primitive",
value: {
type: "string",
regex: undefined,
minLength: undefined,
maxLength: undefined,
default: undefined,
},
},
},
default: undefined,
},
description: undefined,
availability: undefined,
}}
onChange={onChangeAdditionalObjectProperty}
value={Object.keys(castToRecord(value)).reduce((acc: Record<string, unknown>, key) => {
if (!properties.some((p) => p.key === key)) {
acc[key] = castToRecord(value)[key];
}
return acc;
}, {})}
types={types}
disabled={disabled}
/>
</div>
)}
</div>
);
});
Expand Down
30 changes: 17 additions & 13 deletions packages/ui/app/src/playground/form/PlaygroundTypeReferenceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,23 @@ export const PlaygroundTypeReferenceForm = memo<PlaygroundTypeReferenceFormProps
onChange(undefined);
}, [onChange]);
return visitDiscriminatedUnion(unwrapReference(shape, types).shape)._visit<ReactElement | null>({
object: (object) => (
<WithLabel property={property} value={value} onRemove={onRemove} types={types}>
<PlaygroundObjectPropertiesForm
properties={unwrapObjectType(object, types).properties}
onChange={onChange}
value={value}
indent={indent}
id={id}
types={types}
disabled={disabled}
/>
</WithLabel>
),
object: (object) => {
const unwrappedObjectType = unwrapObjectType(object, types);
return (
<WithLabel property={property} value={value} onRemove={onRemove} types={types}>
<PlaygroundObjectPropertiesForm
properties={unwrappedObjectType.properties}
extraProperties={unwrappedObjectType.extraProperties}
onChange={onChange}
value={value}
indent={indent}
id={id}
types={types}
disabled={disabled}
/>
</WithLabel>
);
},
enum: ({ values }) => (
<WithLabel property={property} value={value} onRemove={onRemove} types={types}>
<PlaygroundEnumForm enumValues={values} onChange={onChange} value={value} id={id} disabled={disabled} />
Expand Down
Loading

0 comments on commit 7f41717

Please sign in to comment.