Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/alienvault/alienvault.app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
console.log(Object.keys(this.$auth));
},
},
};
};
2 changes: 1 addition & 1 deletion components/beyond_presence/beyond_presence.app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
console.log(Object.keys(this.$auth));
},
},
};
};
2 changes: 1 addition & 1 deletion components/callhippo/callhippo.app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
console.log(Object.keys(this.$auth));
},
},
};
};
11 changes: 10 additions & 1 deletion packages/connect-react/src/components/ControlSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,16 @@ export function ControlSelect<T extends PropOptionValue>({
}
} else if (rawValue && typeof rawValue === "object" && "__lv" in (rawValue as Record<string, unknown>)) {
// Extract the actual option from __lv wrapper and sanitize to LV
return sanitizeOption(((rawValue as Record<string, unknown>).__lv) as T);
// Handle both single objects and arrays wrapped in __lv
const lvContent = (rawValue as Record<string, unknown>).__lv;
if (!lvContent) {
console.warn("Invalid __lv content:", rawValue);
return null;
}
if (Array.isArray(lvContent)) {
return lvContent.map((item) => sanitizeOption(item as T));
}
return sanitizeOption(lvContent as T);
} else if (!isOptionWithLabel(rawValue)) {
const lvOptions = selectOptions?.[0] && isOptionWithLabel(selectOptions[0]);
if (lvOptions) {
Expand Down
79 changes: 69 additions & 10 deletions packages/connect-react/src/hooks/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
}, [
component.key,
]);

// XXX pass this down? (in case we make it hash or set backed, but then also provide {add,remove} instead of set)
const optionalPropIsEnabled = (prop: ConfigurableProp) => enabledOptionalProps[prop.name];

Expand Down Expand Up @@ -275,6 +276,33 @@ export const FormContextProvider = <T extends ConfigurableProps>({
reloadPropIdx,
]);

// Auto-enable optional props that have values in configuredProps
// This ensures optional fields with saved values are shown when mounting with pre-configured props
useEffect(() => {
const propsToEnable: Record<string, boolean> = {};

for (const prop of configurableProps) {
if (prop.optional && !enabledOptionalProps[prop.name]) {
const value = configuredProps[prop.name as keyof ConfiguredProps<T>];
if (value !== undefined) {
propsToEnable[prop.name] = true;
}
}
}

if (Object.keys(propsToEnable).length > 0) {
setEnabledOptionalProps((prev) => ({
...prev,
...propsToEnable,
}));
}
}, [
component.key,
configurableProps,
configuredProps,
enabledOptionalProps,
]);

// these validations are necessary because they might override PropInput for number case for instance
// so can't rely on that base control form validation
const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
Expand Down Expand Up @@ -355,12 +383,25 @@ export const FormContextProvider = <T extends ConfigurableProps>({
};

useEffect(() => {
// Initialize queryDisabledIdx on load so that we don't force users
// to reconfigure a prop they've already configured whenever the page
// or component is reloaded
updateConfiguredPropsQueryDisabledIdx(_configuredProps)
// Initialize queryDisabledIdx using actual configuredProps (includes parent-passed values in controlled mode)
// instead of _configuredProps which starts empty. This ensures that when mounting with pre-configured
// values, remote options queries are not incorrectly blocked.
updateConfiguredPropsQueryDisabledIdx(configuredProps)
}, [
_configuredProps,
component.key,
configurableProps,
enabledOptionalProps,
]);

// Update queryDisabledIdx reactively when configuredProps changes.
// This prevents race conditions where queryDisabledIdx updates synchronously before
// configuredProps completes its state update, causing duplicate API calls with stale data.
useEffect(() => {
updateConfiguredPropsQueryDisabledIdx(configuredProps);
}, [
configuredProps,
configurableProps,
enabledOptionalProps,
]);

useEffect(() => {
Expand All @@ -386,8 +427,13 @@ export const FormContextProvider = <T extends ConfigurableProps>({
if (skippablePropTypes.includes(prop.type)) {
continue;
}
// if prop.optional and not shown, we skip and do on un-collapse
// if prop.optional and not shown, we still preserve the value if it exists
// This prevents losing saved values for optional props that haven't been enabled yet
if (prop.optional && !optionalPropIsEnabled(prop)) {
const value = configuredProps[prop.name as keyof ConfiguredProps<T>];
if (value !== undefined) {
newConfiguredProps[prop.name as keyof ConfiguredProps<T>] = value;
}
continue;
}
const value = configuredProps[prop.name as keyof ConfiguredProps<T>];
Expand All @@ -398,7 +444,21 @@ export const FormContextProvider = <T extends ConfigurableProps>({
}
} else {
if (prop.type === "integer" && typeof value !== "number") {
delete newConfiguredProps[prop.name as keyof ConfiguredProps<T>];
// Preserve label-value format from remote options dropdowns
// Remote options store values as {__lv: {label: "...", value: ...}}
// For multi-select fields, this will be an array of __lv objects
// IMPORTANT: Integer props with remote options (like IDs) can be stored in __lv format
// to preserve the display label. We only delete the value if it's NOT in __lv format
// AND not a number, which indicates invalid/corrupted data.
const isLabelValue = value && typeof value === "object" && "__lv" in value;
const isArrayOfLabelValues = Array.isArray(value) && value.length > 0 &&
value.every((item) => item && typeof item === "object" && "__lv" in item);

if (!(isLabelValue || isArrayOfLabelValues)) {
delete newConfiguredProps[prop.name as keyof ConfiguredProps<T>];
} else {
newConfiguredProps[prop.name as keyof ConfiguredProps<T>] = value;
}
} else {
newConfiguredProps[prop.name as keyof ConfiguredProps<T>] = value;
}
Expand All @@ -409,6 +469,8 @@ export const FormContextProvider = <T extends ConfigurableProps>({
}
}, [
configurableProps,
enabledOptionalProps,
configuredProps,
]);

// clear all props on user change
Expand Down Expand Up @@ -440,9 +502,6 @@ export const FormContextProvider = <T extends ConfigurableProps>({
if (prop.reloadProps) {
setReloadPropIdx(idx);
}
if (prop.type === "app" || prop.remoteOptions) {
updateConfiguredPropsQueryDisabledIdx(newConfiguredProps);
}
const errs = propErrors(prop, value);
const newErrors = {
...errors,
Expand Down
Loading