mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
1e1592c254
commit
c930e2a4d1
@ -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',
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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} />;
|
||||
|
||||
|
@ -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',
|
@ -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"}'
|
||||
);
|
||||
});
|
||||
};
|
@ -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',
|
@ -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',
|
@ -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',
|
@ -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)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user