@atlaskit/editor-plugin-extension
Version:
editor-plugin-extension plugin for @atlaskit/editor-core
488 lines (482 loc) • 18 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React, { useCallback, useEffect, useRef } from 'react';
import { bind } from 'bind-event-listener';
import _isEqual from 'lodash/isEqual';
import _mergeRecursive from 'lodash/merge';
import memoizeOne from 'memoize-one';
import { injectIntl } from 'react-intl';
import { withAnalyticsContext, withAnalyticsEvents } from '@atlaskit/analytics-next';
import { getDocument } from '@atlaskit/browser-apis';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/new';
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, fireAnalyticsEvent } from '@atlaskit/editor-common/analytics';
import { isTabGroup, configPanelMessages as messages } from '@atlaskit/editor-common/extensions';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import Form, { FormFooter } from '@atlaskit/form';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { ALLOWED_LOGGED_MACRO_PARAMS } from './constants';
import { DescriptionSummary } from './DescriptionSummary';
import ErrorMessage from './ErrorMessage';
import FormContent from './FormContent';
import { FormErrorBoundary } from './FormErrorBoundary';
import Header from './Header';
import LoadingState from './LoadingState';
import { deserialize, findDuplicateFields, serialize } from './transformers';
import { getLoggedParameters } from './utils';
function ConfigForm({
canSave,
errorMessage,
extensionManifest,
fields,
firstVisibleFieldName,
hasParsedParameters,
intl,
isLoading,
onCancel,
onFieldChange,
parameters,
submitting,
contextIdentifierProvider,
featureFlags,
disableFields
}) {
useEffect(() => {
if (fields) {
const firstDuplicateField = findDuplicateFields(fields);
if (firstDuplicateField) {
throw new Error(`Possible duplicate field name: \`${firstDuplicateField.name}\`.`);
}
}
}, [fields]);
if (isLoading || !hasParsedParameters && errorMessage === null) {
return /*#__PURE__*/React.createElement(LoadingState, null);
}
if (errorMessage || !fields) {
return /*#__PURE__*/React.createElement(ErrorMessage, {
errorMessage: errorMessage || ''
});
}
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(FormContent, {
fields: fields,
parameters: parameters,
extensionManifest: extensionManifest,
onFieldChange: onFieldChange,
firstVisibleFieldName: firstVisibleFieldName,
contextIdentifierProvider: contextIdentifierProvider,
featureFlags: featureFlags,
isDisabled: disableFields
}), /*#__PURE__*/React.createElement("div", {
style: canSave ? {} : {
display: 'none'
}
}, /*#__PURE__*/React.createElement(FormFooter, {
align: "start"
}, /*#__PURE__*/React.createElement(ButtonGroup, null, /*#__PURE__*/React.createElement(Button, {
type: "submit",
appearance: "primary"
}, intl.formatMessage(messages.submit)), /*#__PURE__*/React.createElement(Button, {
appearance: "default",
isDisabled: submitting,
onClick: onCancel
}, intl.formatMessage(messages.cancel))))));
}
const ConfigFormIntl = injectIntl(ConfigForm);
const WithOnFieldChange = ({
getState,
autoSave,
handleSubmit,
children
}) => {
const getStateRef = useRef(getState);
useEffect(() => {
getStateRef.current = getState;
}, [getState]);
const handleFieldChange = useCallback((name, isDirty) => {
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 = {};
for (const key of Object.keys(values)) {
if (!errors[key]) {
// not has error
validValues[key] = values[key];
}
}
handleSubmit(validValues);
}, [autoSave, handleSubmit]);
return children(handleFieldChange);
};
// eslint-disable-next-line @repo/internal/react/no-class-components
class ConfigPanel extends React.Component {
constructor(props) {
super(props);
_defineProperty(this, "handleKeyDown", e => {
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.
_defineProperty(this, "backfillTabFormData", (fields, formData, currentParameters) => {
const getRelevantData = (field, formParams, currentParams, backfill) => {
if (field.hasGroupedValues && !(field.name in backfill)) {
backfill[field.name] = {};
}
const actualFormParams = field.hasGroupedValues ? formParams[field.name] || {} : formParams;
const actualCurrentParams = field.hasGroupedValues ? currentParams[field.name] || {} : currentParams;
const actualBackfillParams = field.hasGroupedValues ? backfill[field.name] : backfill;
return {
formParams: actualFormParams,
currentParams: actualCurrentParams,
backfillParams: actualBackfillParams
};
};
// Traverse any tab structures and backfill field values on tabs
// which aren't shown. This filter should be ok because tabs are
// currently only allowed on top level
const mergedTabGroups = fields.filter(isTabGroup).reduce((missingBackfill, tabGroup) => {
const {
formParams: tabGroupFormData,
currentParams: tabGroupCurrentData,
backfillParams: tabGroupParams
} = getRelevantData(tabGroup, formData, currentParameters, missingBackfill);
// Loop through tabs and see what fields are missing from current data
tabGroup.fields.forEach(tabField => {
const {
formParams: tabFormData,
currentParams: tabCurrentData,
backfillParams: tabParams
} = getRelevantData(tabField, tabGroupFormData, tabGroupCurrentData, tabGroupParams);
tabField.fields.forEach(field => {
if (field.name in tabFormData || !(field.name in tabCurrentData)) {
return;
}
tabParams[field.name] = tabCurrentData[field.name];
});
});
return missingBackfill;
}, {});
return _mergeRecursive({}, mergedTabGroups, formData);
});
_defineProperty(this, "handleSubmit", async formData => {
const {
fields,
extensionManifest,
onChange,
autoSaveReject
} = this.props;
if (!extensionManifest || !fields) {
if (!extensionManifest) {
autoSaveReject === null || autoSaveReject === void 0 ? void 0 : autoSaveReject(new Error('Extension manifest not loaded'));
} else if (!fields) {
autoSaveReject === null || autoSaveReject === void 0 ? void 0 : autoSaveReject(new Error('Config fields not loaded'));
}
return;
}
try {
const serializedData = await serialize(extensionManifest, this.backfillTabFormData(fields, formData, this.state.currentParameters), fields);
if (editorExperiment('platform_editor_offline_editing_web', true, {
exposure: true
})) {
await onChange(serializedData);
} else {
onChange(serializedData);
}
} catch (error) {
autoSaveReject === null || autoSaveReject === void 0 ? void 0 : autoSaveReject(error);
// eslint-disable-next-line no-console
console.error(`Error serializing parameters`, error);
}
});
_defineProperty(this, "parseParameters", async (fields, 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
});
});
/**
* Remove renderHeader when when cleaning platform_editor_ai_object_sidebar_injection FG
* Because header will br rendered separately outside of ConfigPanel.
* ConfigPanel will be body component of ContextPanel.
*/
// memoized to prevent rerender on new parameters
_defineProperty(this, "renderHeader", memoizeOne(extensionManifest => {
// Remove below line when cleaning platform_editor_ai_object_sidebar_injection FG
if (this.props.usingObjectSidebarPanel) {
return null;
}
const {
onCancel,
showHeader
} = this.props;
// Use a temporary allowlist of top 3 macros to test out a new "Documentation" CTA ("Need help?")
// This will be removed when Top 5 Modernized Macros updates are rolled out
const modernizedMacrosList = ['children', 'recently-updated', 'excerpt'];
const enableHelpCTA = modernizedMacrosList.includes(extensionManifest.key);
if (!showHeader) {
return null;
}
return /*#__PURE__*/React.createElement(Header, {
icon: extensionManifest.icons['48'],
title: extensionManifest.title,
description: extensionManifest.description,
deprecation: extensionManifest.deprecation,
summary: extensionManifest.summary,
documentationUrl: extensionManifest.documentationUrl,
onClose: onCancel,
enableHelpCTA: enableHelpCTA
});
}));
_defineProperty(this, "getFirstVisibleFieldName", memoizeOne(fields => {
function nonHidden(field) {
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;
}));
_defineProperty(this, "setFirstVisibleFieldName", fields => {
const newFirstVisibleFieldName = this.getFirstVisibleFieldName(fields);
if (newFirstVisibleFieldName !== this.state.firstVisibleFieldName) {
this.setState({
firstVisibleFieldName: newFirstVisibleFieldName
});
}
});
this.state = {
hasParsedParameters: false,
currentParameters: {},
firstVisibleFieldName: props.fields ? this.getFirstVisibleFieldName(props.fields) : undefined
};
this.onFieldChange = null;
this.unbindKeyDownHandler = null;
}
componentDidMount() {
const {
fields,
parameters
} = this.props;
this.parseParameters(fields, parameters);
if (expValEquals('platform_editor_a11y_eslint_fix', 'isEnabled', true)) {
const doc = getDocument();
if (doc) {
this.unbindKeyDownHandler = bind(doc, {
type: 'keydown',
listener: this.handleKeyDown
});
}
}
}
componentWillUnmount() {
var _this$unbindKeyDownHa;
const {
createAnalyticsEvent,
extensionManifest,
fields
} = this.props;
const {
currentParameters
} = this.state;
(_this$unbindKeyDownHa = this.unbindKeyDownHandler) === null || _this$unbindKeyDownHa === void 0 ? void 0 : _this$unbindKeyDownHa.call(this);
fireAnalyticsEvent(createAnalyticsEvent)({
payload: {
action: ACTION.CLOSED,
actionSubject: ACTION_SUBJECT.CONFIG_PANEL,
eventType: EVENT_TYPE.UI,
attributes: {
extensionKey: extensionManifest === null || extensionManifest === void 0 ? void 0 : extensionManifest.key,
extensionType: extensionManifest === null || extensionManifest === void 0 ? void 0 : extensionManifest.type,
...(extensionManifest !== null && extensionManifest !== void 0 && extensionManifest.key && ALLOWED_LOGGED_MACRO_PARAMS[extensionManifest.key] ? {
parameters: getLoggedParameters(extensionManifest.key, currentParameters, fields)
} : {})
}
}
});
}
componentDidUpdate(prevProps) {
const {
parameters,
fields,
autoSaveTrigger,
extensionManifest
} = 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);
}
}
if (prevProps.extensionManifest === undefined && prevProps.extensionManifest !== extensionManifest) {
// This will only be fired once when extensionManifest is loaded initially
// Can't do this in componentDidMount because extensionManifest is still undefined at that point
fireAnalyticsEvent(this.props.createAnalyticsEvent)({
payload: {
action: ACTION.OPENED,
actionSubject: ACTION_SUBJECT.CONFIG_PANEL,
eventType: EVENT_TYPE.UI,
attributes: {
extensionKey: extensionManifest === null || extensionManifest === void 0 ? void 0 : extensionManifest.key,
extensionType: extensionManifest === null || extensionManifest === void 0 ? void 0 : extensionManifest.type
}
}
});
}
}
render() {
const {
extensionManifest,
featureFlags
} = this.props;
if (!extensionManifest) {
return /*#__PURE__*/React.createElement(LoadingState, null);
}
const {
errorMessage,
fields,
isLoading,
onCancel,
api
} = this.props;
const {
currentParameters,
hasParsedParameters,
firstVisibleFieldName
} = this.state;
const {
handleSubmit,
handleKeyDown
} = this;
return /*#__PURE__*/React.createElement(Form, {
onSubmit: handleSubmit
}, ({
formProps,
getState,
submitting
}) => {
return /*#__PURE__*/React.createElement(WithOnFieldChange, {
autoSave: true,
getState: getState,
handleSubmit: handleSubmit
}, onFieldChange => {
this.onFieldChange = onFieldChange;
return /*#__PURE__*/React.createElement("form", _extends({}, formProps, {
noValidate: true,
onKeyDown: expValEquals('platform_editor_a11y_eslint_fix', 'isEnabled', true) ? undefined : handleKeyDown,
"data-testid": "extension-config-panel"
}), this.renderHeader(extensionManifest), (!fg('platform_editor_conditionally_add_sidebar_summary') || this.props.usingObjectSidebarPanel) && fg('platform_editor_ai_object_sidebar_injection') && /*#__PURE__*/React.createElement(DescriptionSummary, {
extensionManifest: extensionManifest
}), /*#__PURE__*/React.createElement(ConfigFormIntlWithBoundary, {
api: api,
canSave: false,
errorMessage: errorMessage,
extensionManifest: extensionManifest,
fields: fields !== null && fields !== void 0 ? fields : [],
firstVisibleFieldName: firstVisibleFieldName,
hasParsedParameters: hasParsedParameters,
isLoading: isLoading || false,
onCancel: onCancel,
onFieldChange: onFieldChange,
parameters: currentParameters,
submitting: submitting,
featureFlags: featureFlags,
disableFields: this.props.disableFields
}));
});
});
}
}
const selector = states => {
var _states$contextIdenti;
return {
contextIdentifierProvider: (_states$contextIdenti = states.contextIdentifierState) === null || _states$contextIdenti === void 0 ? void 0 : _states$contextIdenti.contextIdentifierProvider
};
};
function ConfigFormIntlWithBoundary({
api,
fields,
submitting,
parameters,
featureFlags,
canSave,
extensionManifest,
onFieldChange,
onCancel,
isLoading,
hasParsedParameters,
firstVisibleFieldName,
errorMessage,
disableFields
}) {
const {
contextIdentifierProvider
} = useSharedPluginStateWithSelector(api, ['contextIdentifier'], selector);
return /*#__PURE__*/React.createElement(FormErrorBoundary, {
contextIdentifierProvider: contextIdentifierProvider,
extensionKey: extensionManifest.key,
fields: fields
}, /*#__PURE__*/React.createElement(ConfigFormIntl, {
canSave: canSave,
errorMessage: errorMessage,
extensionManifest: extensionManifest,
fields: fields,
firstVisibleFieldName: firstVisibleFieldName,
hasParsedParameters: hasParsedParameters,
isLoading: isLoading || false,
onCancel: onCancel,
onFieldChange: onFieldChange,
parameters: parameters,
submitting: submitting,
contextIdentifierProvider: contextIdentifierProvider,
featureFlags: featureFlags,
disableFields: disableFields
}));
}
const result = withAnalyticsContext({
source: 'ConfigPanel'
})(withAnalyticsEvents()(ConfigPanel));
export default result;