(
<>
>
)}
/>
);
}
const ConfigFormIntl = injectIntl(ConfigForm);
const WithOnFieldChange: FunctionComponent<{
getState: () => { values: Parameters; errors: Record };
autoSave: boolean;
handleSubmit: (parameters: Parameters) => void;
children: (onFieldChange: OnFieldChange) => ReactElement;
}> = ({ getState, autoSave, handleSubmit, children }) => {
const getStateRef = useRef<
() => { values: Parameters; errors: ValidationErrors }
>(getState);
useEffect(() => {
getStateRef.current = getState;
}, [getState]);
const handleFieldChange = useCallback(
(name: string, isDirty: boolean) => {
if (!autoSave) {
return;
}
// Don't trigger submit if nothing actually changed
if (!isDirty) {
return;
}
const { errors, values } = getStateRef.current();
// Get only values that does not contain errors
const validValues: Parameters = {};
for (const key of Object.keys(values)) {
if (!errors[key]) {
// not has error
validValues[key] = values[key];
}
}
handleSubmit(validValues);
},
[autoSave, handleSubmit],
);
return children(handleFieldChange);
};
type Props = {
extensionManifest?: ExtensionManifest;
fields?: FieldDefinition[];
parameters?: Parameters;
autoSave?: boolean;
autoSaveTrigger?: unknown;
showHeader?: boolean;
closeOnEsc?: boolean;
onChange: OnSaveCallback;
onCancel: () => void;
errorMessage: string | null;
isLoading?: boolean;
} & WithAnalyticsEventsProps;
type State = {
hasParsedParameters: boolean;
currentParameters: Parameters;
firstVisibleFieldName?: string;
};
class ConfigPanel extends React.Component {
onFieldChange: OnFieldChange | null;
constructor(props: Props) {
super(props);
this.state = {
hasParsedParameters: false,
currentParameters: {},
firstVisibleFieldName: props.fields
? this.getFirstVisibleFieldName(props.fields)
: undefined,
};
this.onFieldChange = null;
}
componentDidMount() {
const { fields, parameters, createAnalyticsEvent } = this.props;
this.parseParameters(fields, parameters);
fireAnalyticsEvent(createAnalyticsEvent)({
payload: {
action: ACTION.OPENED,
actionSubject: ACTION_SUBJECT.CONFIG_PANEL,
eventType: EVENT_TYPE.UI,
attributes: {},
},
});
}
componentWillUnmount() {
fireAnalyticsEvent(this.props.createAnalyticsEvent)({
payload: {
action: ACTION.CLOSED,
actionSubject: ACTION_SUBJECT.CONFIG_PANEL,
eventType: EVENT_TYPE.UI,
attributes: {},
},
});
}
componentDidUpdate(prevProps: Props) {
const { parameters, fields, autoSaveTrigger } = this.props;
if (
(parameters && parameters !== prevProps.parameters) ||
(fields && (!prevProps.fields || !_isEqual(fields, prevProps.fields)))
) {
this.parseParameters(fields, parameters);
}
if (fields && (!prevProps.fields || !_isEqual(fields, prevProps.fields))) {
this.setFirstVisibleFieldName(fields);
}
if (prevProps.autoSaveTrigger !== autoSaveTrigger) {
if (this.onFieldChange) {
this.onFieldChange('', true);
}
}
}
handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.key === 'Esc' || e.key === 'Escape') && this.props.closeOnEsc) {
this.props.onCancel();
}
};
// https://product-fabric.atlassian.net/browse/DST-2697
// workaround for DST-2697, remove this function once fix.
backfillTabFormData = (
fields: FieldDefinition[],
formData: Parameters,
currentParameters: Parameters,
): Parameters => {
const mergedTabGroups = fields.filter(isTabGroup).reduce(
(acc, field) => ({
...acc,
[field.name]: {
...(currentParameters[field.name] || {}),
...(formData[field.name] || {}),
},
}),
{},
);
return { ...formData, ...mergedTabGroups };
};
handleSubmit = async (formData: Parameters) => {
const { fields, extensionManifest, onChange } = this.props;
if (!extensionManifest || !fields) {
return;
}
try {
const serializedData = await serialize(
extensionManifest,
this.backfillTabFormData(
fields,
formData,
this.state.currentParameters,
),
fields,
);
onChange(serializedData);
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error serializing parameters`, error);
}
};
parseParameters = async (
fields?: FieldDefinition[],
parameters?: Parameters,
) => {
const { extensionManifest } = this.props;
if (!extensionManifest || !fields || fields.length === 0) {
// do not parse while fields are not returned
return;
}
if (typeof parameters === 'undefined') {
this.setState({ currentParameters: {}, hasParsedParameters: true });
return;
}
const currentParameters = await deserialize(
extensionManifest,
parameters,
fields,
);
this.setState({ currentParameters, hasParsedParameters: true });
};
// memoized to prevent rerender on new parameters
renderHeader = memoizeOne((extensionManifest: ExtensionManifest) => {
const { onCancel, showHeader } = this.props;
if (!showHeader) {
return null;
}
return (
);
});
getFirstVisibleFieldName = memoizeOne((fields: FieldDefinition[]) => {
function nonHidden(field: FieldDefinition) {
if ('isHidden' in field) {
return !field.isHidden;
}
return true;
}
// finds the first visible field, true for FieldSets too
const firstVisibleField = fields.find(nonHidden);
let newFirstVisibleFieldName;
if (firstVisibleField) {
// if it was a fieldset, go deeper trying to locate the field
if (firstVisibleField.type === 'fieldset') {
const firstVisibleFieldWithinFieldset = firstVisibleField.fields.find(
nonHidden,
);
newFirstVisibleFieldName =
firstVisibleFieldWithinFieldset &&
firstVisibleFieldWithinFieldset.name;
} else {
newFirstVisibleFieldName = firstVisibleField.name;
}
}
return newFirstVisibleFieldName;
});
setFirstVisibleFieldName = (fields: FieldDefinition[]) => {
const newFirstVisibleFieldName = this.getFirstVisibleFieldName(fields);
if (newFirstVisibleFieldName !== this.state.firstVisibleFieldName) {
this.setState({
firstVisibleFieldName: newFirstVisibleFieldName,
});
}
};
render() {
const { extensionManifest } = this.props;
if (!extensionManifest) {
return ;
}
const { autoSave, errorMessage, fields, isLoading, onCancel } = this.props;
const {
currentParameters,
hasParsedParameters,
firstVisibleFieldName,
} = this.state;
const { handleSubmit, handleKeyDown } = this;
return (
);
}
}
export default withAnalyticsContext({ source: 'ConfigPanel' })(
withAnalyticsEvents()(ConfigPanel),
);