feature (console): add support for enum type inputs in the OpenApi3 config schema

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6652
Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com>
GitOrigin-RevId: c4410dd220dee0f18989065dc932abdbf8ef167b
This commit is contained in:
Vijay Prasanna 2022-11-03 03:33:35 +05:30 committed by hasura-bot
parent 1e1592c254
commit c930e2a4d1
9 changed files with 199 additions and 7 deletions

View File

@ -0,0 +1,36 @@
import { Select } from '@/new-components/Form';
import { OpenApiSchema } from '@hasura/dc-api-types';
import React from 'react';
import { getInputAttributes } from '../utils';
export const isEnumInputField = (configSchema: OpenApiSchema): boolean => {
const type = configSchema.type;
const enumValues = configSchema.enum;
/**
* if its of type 'string'
*/
return type === 'string' && !!enumValues;
};
export const EnumInputField = ({
name,
configSchema,
}: {
name: string;
configSchema: OpenApiSchema;
}) => {
const { tooltip, label } = getInputAttributes(name, configSchema);
return (
<Select
name={name}
label={label}
tooltip={tooltip}
options={(configSchema.enum ?? []).map(_enum => ({
label: _enum ?? 'null',
value: _enum ?? 'null',
}))}
/>
);
};

View File

@ -19,6 +19,7 @@ import {
ObjectArrayInputField,
} from './ObjectArrayInputField';
import { isOneOf, OneOfInputField } from './OneOfInputField';
import { EnumInputField, isEnumInputField } from './EnumInputField';
export const RenderProperty = ({
name,
@ -44,6 +45,9 @@ export const RenderProperty = ({
/**
* Basic input conditions -> these field are terminal. They do not have any recursive behaviour to them.
*/
if (isEnumInputField(configSchema))
return <EnumInputField name={name} configSchema={configSchema} />;
if (isTextInputField(configSchema))
return <TextInputField name={name} configSchema={configSchema} />;

View File

@ -4,7 +4,7 @@ import React from 'react';
import { screen } from '@testing-library/dom';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RenderOpenApi3Form } from '../common/RenderOpenApi3Form';
import { RenderOpenApi3Form } from './common/RenderOpenApi3Form';
export default {
title: 'Components/OpenApi3Form ⚛️ /Boolean',

View File

@ -0,0 +1,117 @@
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import { screen } from '@testing-library/dom';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RenderOpenApi3Form } from './common/RenderOpenApi3Form';
export default {
title: 'Components/OpenApi3Form ⚛️/EnumInput',
component: RenderOpenApi3Form,
parameters: {
docs: {
description: {
component: `This component demonstrates how to use a Enum input via the OpenApi3Form component`,
},
source: { type: 'code' },
},
},
decorators: [
ReactQueryDecorator(),
Story => <div className="p-4 w-full">{Story()}</div>,
],
} as ComponentMeta<typeof RenderOpenApi3Form>;
export const EnumInput: ComponentStory<typeof RenderOpenApi3Form> = () => {
return (
<RenderOpenApi3Form
name="enumInput"
getSchema={() => [
{
title: 'Enum Input',
type: 'string',
enum: ['option1', 'option2'],
},
{},
]}
defaultValues={{}}
/>
);
};
export const EnumInputWithNullableAsOption: ComponentStory<
typeof RenderOpenApi3Form
> = () => {
return (
<RenderOpenApi3Form
name="enumInput"
getSchema={() => [
{
title: 'Enum Input',
type: 'string',
enum: ['option1', 'option2', null],
},
{},
]}
defaultValues={{}}
/>
);
};
export const EnumInputWithExistingValues: ComponentStory<
typeof RenderOpenApi3Form
> = () => {
return (
<RenderOpenApi3Form
name="enumInput"
getSchema={() => [
{
title: 'Enum Input',
type: 'string',
enum: ['option1', 'option2', null],
},
{},
]}
defaultValues={{
enumInput: 'option1',
}}
/>
);
};
export const Test: ComponentStory<typeof RenderOpenApi3Form> = () => (
<RenderOpenApi3Form
name="enumInput"
getSchema={() => [
{
title: 'Enum Input',
type: 'string',
enum: ['option1', 'option2', null],
},
{},
]}
defaultValues={{}}
rawOutput
/>
);
Test.storyName = '🧪 Testing - input interaction';
Test.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(async () => {
await userEvent.selectOptions(canvas.getByTestId('enumInput'), 'option1');
});
await waitFor(async () => {
await userEvent.click(canvas.getByTestId('submit-form-btn'));
});
await waitFor(async () => {
await expect(screen.getByTestId('output').textContent).toBe(
'{"enumInput":"option1"}'
);
});
};

View File

@ -4,7 +4,7 @@ import React from 'react';
import { screen } from '@testing-library/dom';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RenderOpenApi3Form } from '../common/RenderOpenApi3Form';
import { RenderOpenApi3Form } from './common/RenderOpenApi3Form';
export default {
title: 'Components/OpenApi3Form ⚛️/NumberInput',

View File

@ -4,7 +4,7 @@ import React from 'react';
import { screen } from '@testing-library/dom';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RenderOpenApi3Form } from '../common/RenderOpenApi3Form';
import { RenderOpenApi3Form } from './common/RenderOpenApi3Form';
export default {
title: 'Components/OpenApi3Form ⚛️/ObjectInput',

View File

@ -4,7 +4,7 @@ import React from 'react';
import { screen } from '@testing-library/dom';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RenderOpenApi3Form } from '../common/RenderOpenApi3Form';
import { RenderOpenApi3Form } from './common/RenderOpenApi3Form';
export default {
title: 'Components/OpenApi3Form ⚛️/TextInput',

View File

@ -39,7 +39,7 @@ export const RenderOpenApi3Form = ({
setSubmittedValues(values as any);
}}
>
{() => {
{options => {
return (
<>
<OpenApi3Form
@ -58,6 +58,7 @@ export const RenderOpenApi3Form = ({
<ReactJson src={submittedValues} name={false} />
)}
</div>
{console.log(options.formState.errors)}
</>
);
}}

View File

@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle */
import get from 'lodash.get';
// import pickBy from 'lodash.pickby';
import { OpenApiSchema, OpenApiReference } from '@hasura/dc-api-types';
import { z, ZodSchema } from 'zod';
import pickBy from 'lodash.pickby';
@ -25,6 +24,26 @@ export const getStringZodSchema = (schema: OpenApiSchema): ZodSchema => {
return z.string().min(1, `${schema.title ?? 'value'} cannot be empty`);
};
export const getEnumZodSchema = (schema: OpenApiSchema): ZodSchema => {
const enumOptions = schema.enum ?? [];
const literals = enumOptions.map(enumValue => z.literal(enumValue));
return z
.union([z.literal('null'), ...literals] as any)
.transform((value, ctx) => {
if (value === 'null') return null;
if (value) return value;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'value not part of the enum list',
});
return z.never;
});
};
export const getBooleanZodSchema = (schema: OpenApiSchema): ZodSchema => {
if (schema.nullable === true) return z.boolean().optional();
@ -52,6 +71,17 @@ export const getArrayZodSchema = (
? getReferenceObject(items.$ref, references)
: items;
/**
* Enum
*/
if (itemSchema.type === 'string' && itemSchema.enum) {
/**
* No nullable case in enum, as mentioned in doc -
* Note that null must be explicitly included in the list of enum values. Using nullable: true alone is not enough here.
*/
return getEnumZodSchema(schema);
}
/**
* String Array
*/
@ -102,6 +132,8 @@ export const transformSchemaToZodObject = (
const type = schema.type;
if (type === 'string' && schema.enum) return getEnumZodSchema(schema);
if (type === 'string') return getStringZodSchema(schema);
if (type === 'number' || type === 'integer')
@ -136,7 +168,9 @@ export const transformSchemaToZodObject = (
if (itemSchema.type === 'object') {
if (schema.nullable === true)
return z.array(transformSchemaToZodObject(itemSchema, references));
return z
.array(transformSchemaToZodObject(itemSchema, references))
.optional();
return z
.array(transformSchemaToZodObject(itemSchema, references))