-
Notifications
You must be signed in to change notification settings - Fork 215
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
Conversation
This is looking pretty good! One slight deviation from the plan is the example dropdown. The idea is that a string with an example is still just a string, there's simply a selector that allows you to pick an example to update that string value. Same for numbers, they're a input[type=number] which also allows you to select the example. It's not an input next to a selector, its an input with a selector. |
Right, I get it. Thing is that having regular input with selector is tricky if we want to stick to current mosaic components. There's a solution with @marcelltoth what do you think? If there's no way to handle it for now we could stick to initial approach and single input with first example populated. |
We can split work until later if it’s blocked but please make a new issue and mention it in the PR and team leader agreed so we can get it added to the backlog.
We can leave it now it is until mosaic is ready to help, or we can make mosaic support it, whichever you folks decide, so long as there’s an issue to finish it later.
…--
Phil Sturgeon
DevRel @ Stoplight.io
On Dec 11, 2020, at 13:41, Jakub Jankowski ***@***.***> wrote:
This is looking pretty good! One slight deviation from the plan is the example dropdown.
The idea is that a string with an example is still just a string, there's simply a selector that allows you to pick an example to update that string value. Same for numbers, they're a input[type=number] which also allows you to select the example.
It's not an input next to a selector, its an input with a selector.
Right, I get it. Thing is that having regular input with selector is tricky if we want to stick to current mosaic components.
This issue is marked as blocked by stoplightio/mosaic#44 which might change that (not sure as it's empty), but for now we might need to stick with what we have. Unless I'm missing some feature.
There's a solution with list prop and dataset, but that doesn't look good.
@marcelltoth what do you think? If there's no way to handle it for now we could stick to initial approach and single input with first example populated.
—
You are receiving this because your review was requested.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
96cd82e
to
69f950c
Compare
✔️ Deploy preview for stoplight-elements ready! 🔨 Explore the source changes: ec8e5a3 🔍 Inspect the deploy logs: https://app.netlify.com/sites/stoplight-elements/deploys/5fe1ca5206413600094bf066 😎 Browse the preview: https://deploy-preview-755--stoplight-elements.netlify.app |
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 comment
The 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 comment
The 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
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.
Enums work perfectly.
The workaround for Examples is perfect, too, IMO, just create a followup issue as @philsturgeon requested.
I left some comments in the code. It's a first round so I might have skipped something, will check again when these are resolved or dismissed.
}; | ||
} | ||
|
||
function parameterOptions(parameter: IHttpPathParam | IHttpQueryParam) { |
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.
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.
The parameter can be IHttpParam
which is the common denominator between the two inputs.
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.
Right, IHttpParam
does the job
function exampleOptions(parameter: IHttpPathParam | IHttpQueryParam) { | ||
return parameter.examples?.length && parameter.examples.length > 1 | ||
? parameter.examples.map(example => ({ label: example.key, value: exampleValue(example) })) | ||
: null; | ||
} |
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 one has a nice return type for example 👍
return 'value' in example | ||
? (example as INodeExample).value | ||
: 'externalValue' in example | ||
? (example as INodeExternalExample).externalValue | ||
: ''; |
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 simplifies to
return 'value' in example ? example.value : example.externalValue;
Fortunately in
is now a type-guard. (Which is a bit of a lie in TypeScript as there are no final-types but it's handy). If the types don't lie the third case - your ''
- should never appear.
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 great!
const onChange = React.useCallback( | ||
parameter => (e: React.FormEvent<HTMLSelectElement> | React.ChangeEvent<HTMLInputElement>) => { | ||
const newValue = e.currentTarget.value; | ||
onChangeValues({ ...values, [parameter.name]: newValue }); | ||
}, | ||
[onChangeValues, values], | ||
); |
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 useCallback
doesn't give you anything unfortunately. The function that will be memoized is the outer one that isn't used as a prop directly. I think you can simply lose the useCallback
and be fine, but if you really want to memoize it the only way I know of is something like:
const onChangeFunctions = React.useMemo(() => (
parameters.map(parameter => (e: React.FormEvent<HTMLSelectElement> | React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.currentTarget.value;
onChangeValues({ ...values, [parameter.name]: newValue });
})
), [onChangeValues, values]);
If I'm not mistaken this should now have the type of Array<(e: React.FormEvent<HTMLSelectElement> | React.ChangeEvent<HTMLInputElement>) => void>
then you can use it as onChange={onChangeFunctions[parameter]}
.
But probably not worth the effort TBH, unless we actually measure some performance bottleneck. (Which I don't expect as there shouldn't be much more than 10-20 parameters IMO)
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, not worth it in this case.
value={values[parameter.name] ?? ''} | ||
onChange={onChange(parameter)} | ||
/> | ||
{examples && <Select flexGrow options={examples} onChange={onChange(parameter)} />} |
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 input is partially-controlled which is usually not advised. If we provide onChange
we should provide value
as well. It will improve UX as well because we could provide value like:
//somewhere up as a constant
const selectExampleOption = {value: '', label: "Pick an example"};
// in the map
const examples = // this should include selectExampleOption
const selectedExample = examples.find(e => e.value === values[parameter.name]) ?? selectExampleOption;
<Select value={selectedExample.value} />
This will make the example selector react to manual changes as well: in case the user types something it'll reset to "Pick an example"
const examples = parameters | ||
.filter(p => Array.isArray(p.examples)) | ||
.reduce((params, p) => { | ||
if (p.examples?.length) { | ||
return { | ||
...params, | ||
[p.name]: exampleValue(p.examples[0]), | ||
}; | ||
} else { | ||
return { ...params }; | ||
} | ||
}, {}); |
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.
These reduce
s can be written in a more declarative way using Object.fromEntries
which is usually nicer IMO. Your call if you like it or not, also applies to the one above. This example:
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])]),
);
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 remember you told me about that earlier. I also prefer your way when I look at it, I'm just too used to reduce ;)
@@ -33,7 +33,9 @@ export const BasicSend: React.FC<BasicSendProps> = ({ httpOperation }) => { | |||
}; | |||
const allParameters = Object.values(operationParameters).flat(); | |||
|
|||
const [parameterValues, setParameterValues] = React.useState<Dictionary<string, string>>({}); | |||
const [parameterValues, setParameterValues] = React.useState<Dictionary<string, string>>( |
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 in OperationParameters
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 in OperationParameters
?
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.
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we could clean up this map
by sepearating those children into a separate component that simply accepts parameter
prop (and some other stuff if necessary)
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.
Good point - I moved it.
|
||
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); | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
this might be separated into getExampleForParameter
util or something.
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.
Not sure if it's worth it as parameterValueOptions
and examples
are used here anyway so that would be just replacing that one line. Is it?
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 comment
The 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 string
for sure?
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.
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 text
or number
in our case, definitely not string
, array
or boolean
</Flex> | ||
); | ||
})} | ||
</Panel.Content> | ||
</Panel> | ||
); | ||
}; | ||
|
||
function flattenParameters(parameters: OperationParameters) { |
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.
In Haskell etc. it is a common practice to put "util" functions alongside the defined interface.
So in this case, the interface OperationParameters
would land in it's own file and all associated functions would be placed in the same file as well.
Parhaps worth considering here?
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'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 comment
The 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.
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.
Lovely! 🚢 IT!
const completedField = screen.getByLabelText('completed'); | ||
expect(completedField).toHaveValue(''); |
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 love the getByLabelText
solution! 😍
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.
Amazing!
Fixes #645
Adds parameter validations:
Not Set
as with booleans)Not Set
(empty string) as defaultInitializes parameters with values from enums and examples.