UNPKG

@atlaskit/editor-plugin-extension

Version:

editor-plugin-extension plugin for @atlaskit/editor-core

488 lines (482 loc) 18 kB
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;