-
Notifications
You must be signed in to change notification settings - Fork 201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: parameters inputs #755
Changes from 2 commits
69f950c
b199312
78477c5
524984c
ec8e5a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,17 @@ | ||
import { Flex, Input, Panel, Text } from '@stoplight/mosaic'; | ||
import { Dictionary, IHttpHeaderParam, IHttpPathParam, IHttpQueryParam } from '@stoplight/types'; | ||
import { sortBy } from 'lodash'; | ||
import { Flex, Input, Panel, Select, Text } from '@stoplight/mosaic'; | ||
import { | ||
Dictionary, | ||
IHttpHeaderParam, | ||
IHttpParam, | ||
IHttpPathParam, | ||
IHttpQueryParam, | ||
INodeExample, | ||
INodeExternalExample, | ||
} from '@stoplight/types'; | ||
import { map, sortBy } from 'lodash'; | ||
import * as React from 'react'; | ||
|
||
interface OperationParameters { | ||
export interface OperationParameters { | ||
path?: IHttpPathParam[]; | ||
query?: IHttpQueryParam[]; | ||
headers?: IHttpHeaderParam[]; | ||
|
@@ -15,41 +23,120 @@ interface OperationParametersProps { | |
onChangeValues: (newValues: Dictionary<string, string>) => void; | ||
} | ||
|
||
const booleanOptions = [ | ||
{ label: 'Not Set', value: '' }, | ||
{ label: 'False', value: 'false' }, | ||
{ label: 'True', value: 'true' }, | ||
]; | ||
|
||
const selectExampleOption = { value: '', label: 'Pick an example' }; | ||
|
||
export const OperationParameters: React.FC<OperationParametersProps> = ({ | ||
operationParameters, | ||
values, | ||
onChangeValues, | ||
}) => { | ||
const pathParameters = sortBy(operationParameters.path ?? [], ['name']); | ||
const queryParameters = sortBy(operationParameters.query ?? [], ['name']); | ||
const headerParameters = sortBy(operationParameters.headers ?? [], ['name']); | ||
const parameters = [...pathParameters, ...queryParameters, ...headerParameters]; | ||
const parameters = flattenParameters(operationParameters); | ||
|
||
const onChange = (parameter: IHttpParam) => ( | ||
e: React.FormEvent<HTMLSelectElement> | React.ChangeEvent<HTMLInputElement>, | ||
) => { | ||
const newValue = e.currentTarget.value; | ||
onChangeValues({ ...values, [parameter.name]: newValue }); | ||
}; | ||
|
||
return ( | ||
<Panel id="collapse-open" defaultIsOpen> | ||
<Panel.Titlebar>Parameters</Panel.Titlebar> | ||
<Panel.Content className="sl-overflow-y-auto OperationParametersContent"> | ||
{parameters.map(parameter => { | ||
const parameterValueOptions = parameterOptions(parameter); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we could clean up this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point - I moved it. |
||
const examples = exampleOptions(parameter); | ||
const selectedExample = examples?.find(e => e.value === values[parameter.name]) ?? selectExampleOption; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might be separated into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if it's worth it as |
||
return ( | ||
<Flex key={parameter.name} alignItems="center"> | ||
<Flex align="center" key={parameter.name}> | ||
<Input appearance="minimal" readOnly value={parameter.name} /> | ||
<Text mx={3}>:</Text> | ||
<Input | ||
appearance="minimal" | ||
flexGrow | ||
placeholder={parameter.schema?.type as string} | ||
type={parameter.schema?.type as string} | ||
required | ||
value={values[parameter.name] ?? ''} | ||
onChange={e => { | ||
const newValue = e.currentTarget.value; | ||
onChangeValues({ ...values, [parameter.name]: newValue }); | ||
}} | ||
/> | ||
{parameterValueOptions ? ( | ||
<Select | ||
flexGrow | ||
options={parameterValueOptions} | ||
value={values[parameter.name]} | ||
onChange={onChange(parameter)} | ||
/> | ||
) : ( | ||
<Flex flexGrow> | ||
<Input | ||
style={{ paddingLeft: 15 }} | ||
appearance="minimal" | ||
flexGrow | ||
placeholder={(parameter.schema?.default ?? parameter.schema?.type) as string} | ||
type={parameter.schema?.type as string} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. types say that this might be an array as well. How do we know it will be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might not be a string, but this was wrong anyway. That's not a change I did in this PR, but input type shouldn't be set to parameter type. It should be either |
||
required | ||
value={values[parameter.name] ?? ''} | ||
onChange={onChange(parameter)} | ||
/> | ||
{examples && ( | ||
<Select flexGrow value={selectedExample.value} options={examples} onChange={onChange(parameter)} /> | ||
)} | ||
</Flex> | ||
)} | ||
</Flex> | ||
); | ||
})} | ||
</Panel.Content> | ||
</Panel> | ||
); | ||
}; | ||
|
||
function flattenParameters(parameters: OperationParameters) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Haskell etc. it is a common practice to put "util" functions alongside the defined interface. So in this case, the interface Parhaps worth considering here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. At that point those will become exports which is more commitment than some random internal functions here alongside the component. I see your point but I'd say it isn't necessarily worth here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's an idea that I haven't heard before. We could add it to consideration along others code structure ideas. |
||
const pathParameters = sortBy(parameters.path ?? [], ['name']); | ||
const queryParameters = sortBy(parameters.query ?? [], ['name']); | ||
const headerParameters = sortBy(parameters.headers ?? [], ['name']); | ||
return [...pathParameters, ...queryParameters, ...headerParameters]; | ||
} | ||
|
||
export function initialParameterValues(operationParameters: OperationParameters) { | ||
const parameters = flattenParameters(operationParameters); | ||
|
||
const enums = Object.fromEntries( | ||
parameters | ||
.map(p => [p.name, p.schema?.enum ?? []] as const) | ||
.filter(([, enums]) => enums.length > 0) | ||
.map(([name, enums]) => [name, String(enums[0])]), | ||
); | ||
|
||
const examples = Object.fromEntries( | ||
parameters | ||
.map(p => [p.name, p.examples ?? []] as const) | ||
.filter(([, examples]) => examples.length > 0) | ||
.map(([name, examples]) => [name, exampleValue(examples[0])]), | ||
); | ||
|
||
return { | ||
// order matters - enums should be override examples | ||
...examples, | ||
...enums, | ||
}; | ||
} | ||
|
||
function parameterOptions(parameter: IHttpParam) { | ||
return parameter.schema?.type === 'boolean' | ||
? booleanOptions | ||
: parameter.schema?.enum !== undefined | ||
? map(parameter.schema.enum, v => (Number.isNaN(Number(v)) ? String(v) : Number(v))) | ||
: null; | ||
} | ||
|
||
function exampleOptions(parameter: IHttpParam) { | ||
return parameter.examples?.length && parameter.examples.length > 1 | ||
? [ | ||
selectExampleOption, | ||
...parameter.examples.map(example => ({ label: example.key, value: exampleValue(example) })), | ||
] | ||
: null; | ||
} | ||
|
||
function exampleValue(example: INodeExample | INodeExternalExample) { | ||
return 'value' in example ? example.value : example.externalValue; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { httpOperation } from '../../../__fixtures__/operations/put-todos'; | ||
import { initialParameterValues } from '../OperationParameters'; | ||
|
||
describe('Parameters', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I believe that - according to what we agreed on - this shouldn't really be tested. It should be tested if the component itself displays proper contents on initial render? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the test of that function, but I agree, there should also be a component test |
||
it('should fill initial parameters', () => { | ||
const operationParameters = { | ||
path: httpOperation.request?.path, | ||
query: httpOperation.request?.query, | ||
headers: httpOperation.request?.headers, | ||
}; | ||
|
||
const parameters = initialParameterValues(operationParameters); | ||
|
||
expect(parameters).toMatchObject({ | ||
limit: '0', | ||
type: 'something', | ||
value: '0', | ||
'account-id': 'example id', | ||
'message-id': 'example value', | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am just noticing, that we are defining a lot of stuff in
BasicSend
, but then it is only used inOperationParameters
really, with an exception of building the actual fetch request.Since operation parameters are actually a responsibility of... well...
OperationParameters
component, maybe all this logic -initialParameterValues
,operationParameters
etc. should be placed inOperationParameters
?This is a soft proposition though - not really sure if the code would become actually cleaner in reality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, we do. I guess the idea was to keep state in main component as there might be more components using that state. I'm not sure whether we should move it. We will eventually switch to a state library so then might be good time to clear it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hard one. This is what you end up with when you do the playbook React "lift the state up" exercise, and I'm not sure it's a bad thing. The more you lift the state up the more
OperationParameters
(the descendant) becomes a fully controlled presentational component. So while on one hand I think you are right on the other this is better separation of state logic vs visualization.I think it's fine to leave it as is, at least for now. Also it isn't something that I would bring in a state management lib for, I think what vanilla React offers here is perfectly good enough and doesn't justify the complexity but that's another discussion.