-
Notifications
You must be signed in to change notification settings - Fork 2
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: Allow lazy-loaded SelectFields to change their options. #962
Conversation
src/inputs/Autocomplete.stories.tsx
Outdated
@@ -46,7 +45,7 @@ function Template() { | |||
{o.label} | |||
</div> | |||
)} | |||
onSelect={action("onSelect")} | |||
onSelect={(o) => setValue(o.label)} |
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.
Er, I was playing with Autocomplete a few days ago and this snuck in...
I.e. if a variable in the GQL query changed, after we've done the initial load.
cdfd827
to
9919970
Compare
@@ -37,7 +37,7 @@ class SingleFilter<O, V extends Key> extends BaseFilter<V, SingleFilterProps<O, | |||
|
|||
const options = Array.isArray(maybeOptions) | |||
? [allOption as O, ...maybeOptions] | |||
: { ...maybeOptions, initial: [allOption as O, ...maybeOptions.initial] }; | |||
: { ...maybeOptions, initial: maybeOptions.initial }; |
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.
thought: I think this could cause bugs if anyone is using the SingleFilter
with lazily loaded options. The All option will no longer appear. 🤔 Though, I think this is already a bug. The allOption
probably isn't being returned in the load
callback, so 🤷. I don't know if we can really get around this anyways... nevermind.
@@ -218,15 +214,8 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>) | |||
async function maybeInitLoad() { |
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.
question: maybeInitLoad
is only being called once due to the firstOpen.current
check in onOpenChange
. Did you want to allow await maybeOptions.load()
to be called multiple times?
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.
No, after the 1st call, I'm anticipating the page/caller will just pipe options
directly into the component, without it having to be interacted with/triggered by the onOpenChange
:
export function BidItemSelectField(props: BidItemSelectFieldProps) {
const { filter, value, onSelect, readOnly, label, ...others } = props;
const { refetch, data } = useBidItemSelectQuery({
skip: true,
variables: { filter },
nextFetchPolicy: "cache-first",
});
return (
<SelectField
{...others}
label={label ?? "Bid Item"}
readOnly={readOnly}
getOptionLabel={(o) => o.displayName}
getOptionValue={(o) => o.id}
unsetLabel={emptyCellDash}
onSelect={onSelect}
options={{
initial: value ? { ...value, products: [] } : undefined,
load: refetch,
options: data?.potentialBidItems ?? [],
}}
value={value?.id}
/>
);
}
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.
If I understand this correctly, then we are skip
-ing the useQuery
call so that we only call it when we need to via refetch
. But we're only going to be calling refetch
once due to options.load
only being triggered once. Does skip
no longer apply if refetch
was triggered once 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.
Right, kinda surprising, but once enabled by refetch, changes to variables will update the query. Kinda makes sense...but had to confirm it.
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.
Fwiw I've also been moving callers over to the "useLazyQuery", where it's more intuitive that, after load is called, the query is kept up to date
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.
Something about this felt a little odd to me, and I think finally see it. If we have a options.options
property, maybe we use that for both the "initial" and "loaded" options and get rid of options.initial
?
options={{
load: refetch,
options: data?.potentialBidItems ?? (value ? [{ ...value, products: [] }] : []),
}}
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.
Here is a current impl:
export function ProductSelectField(props: ProductSelectFieldProps) {
const { value, onSelect, disabled, label = "Product", disableOptions, filter, ...others } = props;
const [load, { data }] = useProductSelectLazyQuery({ variables: { filter: { ...filter }, first: 500 } });
return (
<SelectField<ProductSelect_ProductFragment, string>
label={label}
getOptionValue={(o) => o.id}
getOptionLabel={optionDisplayName}
disabledOptions={disableOptions}
onSelect={(id, value) => onSelect(value)}
value={value?.id}
options={{ initial: value ?? undefined, load, options: data?.products }}
disabled={disabled}
{...others}
/>
);
}
Given your prompt, I had two thoughts:
a) Why keep options as a hash at all, i.e. we could do:
const [load, { data } = useLazyQuery();
<SelectFields options={data?.products ?? [value]} loadOptions={load} />
Which might be nice b/c it'd move options back to just an O[]
b) I'm wondering if we really want data?.products ?? [value]
, i.e. in the current select fields, I'm using a value
prop to set both initial (pre-interaction) and "currently selected value" (post-interaction):
<SelectField<ProductSelect_ProductFragment, string>
label={label}
getOptionValue={(o) => o.id}
getOptionLabel={optionDisplayName}
disabledOptions={disableOptions}
onSelect={(id, value) => onSelect(value)}
value={value?.id}
options={{ initial: value ?? undefined, load, options: data?.products }}
disabled={disabled}
{...others}
/>
So "initial" is no longer "initial", it's really the current value, which makes me wonder (I believe we'd discussed this before), what happens if the current value is something that's not in the list of options the GQL query returned?
Maybe we should be looking for value
/ initial
inside of options, and then appending it, if it's not there.
Which would be easier to do if initial
+ options
were separate props, b/c SelectField
could do the stitching internally...
Granted, I've not observed this "initial is not in options" happen in production yet, but seems like its just a matter of time...
So, dunno, wydt of renaming initial
-> current
and using the "ensure current is really within options" as a rationale for keeping options
overloaded as { current, load, options }
?
B/c if we don't want to do that (detect current is not in options), then maybe per "a" we should go back to options: O[]
, with the implicit contract (which I guess I don't like the implicit-ness of it) that "you should add current here", and an extra loadOptions
prop.
...dunno, currently liking current + load + options
, but lmk...
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 also need to ship the "just add a product" field I'm working on, so I'll also probably offer to do the "merge current into options if missing" as a follow up.
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.
Okay. Yea, I like the idea of current
. I 100% agree with:
Granted, I've not observed this "initial is not in options" happen in production yet, but seems like its just a matter of time...
Feels like we are on our way to this, especially with your use case of a separate component changing the set of options for this SelectField.
Regarding option a, we could have the caller do the merging of the "current into options" with some helper.
<SelectFields options={ensureHasOption(data?.products, value) ?? [value]} loadOptions={load} />
But then, maybe its just easier for Beam to handle that so that nuance isn't overlooked by the developer.
I'm on board with initial -> current
and ensuring current
is always in the list of options.
## [2.320.0](v2.319.1...v2.320.0) (2023-10-10) ### Features * Allow lazy-loaded SelectFields to change their options. ([#962](#962)) ([e919851](e919851))
🎉 This PR is included in version 2.320.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
I.e. if a variable in the GQL query changed, after we've done the initial load.
Previously our API for lazy SelectField was:
However this API only worked with the assumption that
load()
would only need to be called once, when the SelectField was 1st interacted with.Turns out some lazy-selects will some options based on other data (like the value of a sibling select field, i.e. changing bid item needs to change the products), and so need to re-load/change their options even after the initial interaction, i.e. after a GQL query to load the option has its variable change.
So we've changed the
options
API for lazy selects to be:current
(not just initial) value from either the model or apollo cacheload
a function that just triggers a load, but doesn't return the optionsoptions
the latest options, initially undefined pre-load
-being-called, but then can change repeatedly afterload
has been called, i.e. as GQL queries fire: