Skip to content
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

Merged
merged 5 commits into from
Dec 22, 2020
Merged

feat: parameters inputs #755

merged 5 commits into from
Dec 22, 2020

Conversation

mallachari
Copy link
Contributor

@mallachari mallachari commented Dec 11, 2020

Fixes #645

Adds parameter validations:

  • enums as dropdown - first element as default (we could add Not Set as with booleans)
  • booleans as dropdown - Not Set (empty string) as default
  • examples - first item as populated value, if more than one show both - text field and dropdown
  • default as placeholder

Initializes parameters with values from enums and examples.

Peek 2020-12-11 09-55

@mallachari mallachari requested a review from a team December 11, 2020 08:58
@mallachari mallachari self-assigned this Dec 11, 2020
@philsturgeon
Copy link
Contributor

This is looking pretty good! One slight deviation from the plan is the example dropdown.

image

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.

@mallachari
Copy link
Contributor Author

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 https://github.com/stoplightio/mosaic/issues/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.

Base automatically changed from feat/try-it-set-parameters to v7 December 11, 2020 17:23
@philsturgeon
Copy link
Contributor

philsturgeon commented Dec 11, 2020 via email

@netlify
Copy link

netlify bot commented Dec 11, 2020

✔️ 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', () => {
Copy link
Contributor

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? 🤔

Copy link
Contributor Author

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

Copy link
Contributor

@marcelltoth marcelltoth left a 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands, this function has a weird return type:
image
It would be nice to normalize it to one or the other.

You could change the nested ternaries into ifs, it might be longer but easier to read.

Copy link
Contributor

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.

Copy link
Contributor Author

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

Comment on lines 135 to 139
function exampleOptions(parameter: IHttpPathParam | IHttpQueryParam) {
return parameter.examples?.length && parameter.examples.length > 1
? parameter.examples.map(example => ({ label: example.key, value: exampleValue(example) }))
: null;
}
Copy link
Contributor

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 👍

Comment on lines 142 to 146
return 'value' in example
? (example as INodeExample).value
: 'externalValue' in example
? (example as INodeExternalExample).externalValue
: '';
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!

Comment on lines 38 to 44
const onChange = React.useCallback(
parameter => (e: React.FormEvent<HTMLSelectElement> | React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.currentTarget.value;
onChangeValues({ ...values, [parameter.name]: newValue });
},
[onChangeValues, values],
);
Copy link
Contributor

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)

Copy link
Contributor Author

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)} />}
Copy link
Contributor

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"

Comment on lines 105 to 116
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 };
}
}, {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These reduces 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])]),
  );

Copy link
Contributor Author

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>>(
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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);
Copy link
Contributor

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)

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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}
Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@marcelltoth marcelltoth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely! 🚢 IT!

Comment on lines +90 to +91
const completedField = screen.getByLabelText('completed');
expect(completedField).toHaveValue('');
Copy link
Contributor

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! 😍

Copy link
Contributor

@philsturgeon philsturgeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!

@mallachari mallachari merged commit fec2d79 into v7 Dec 22, 2020
@mallachari mallachari deleted the feat/parameters-inputs branch December 22, 2020 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Try It: Allow selecting parameter enums & examples
4 participants