@atlaskit/editor-core 219.5.1 → 219.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/editor-core
2
2
 
3
+ ## 219.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`80e6f41ddfce4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/80e6f41ddfce4) -
8
+ Reconcile editor plugin set on reconfigureState across preset switches: drop plugins whose schema
9
+ nodes/marks aren't in the current schema, and evict plugins from the injection API that aren't
10
+ part of the new preset. Behind feature gate `platform_editor_reconfigure_filter_plugins`.
11
+
12
+ Adds the `NamedReactHookFactory` type to `@atlaskit/editor-common/types` and annotates each
13
+ plugin's `usePluginHook` with its plugin name in `processPluginsList` so `MountPluginHooks` can
14
+ key React fibers stably per plugin. The shape of `EditorConfig.pluginHooks` is unchanged
15
+ (`ReactHookFactory[]`); the annotation is a non-breaking additive property on the function value
16
+ that falls back to the array index when absent.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+
3
22
  ## 219.5.1
4
23
 
5
24
  ### Patch Changes
@@ -49,6 +49,7 @@ var _consts = require("./consts");
49
49
  var _createEditor = require("./create-editor");
50
50
  var _createPluginsList = _interopRequireDefault(require("./create-plugins-list"));
51
51
  var _createSchema = require("./create-schema");
52
+ var _filterPluginsForReconfigure = require("./filter-plugins-for-reconfigure");
52
53
  var _messages = require("./messages");
53
54
  var _focusEditorElement = require("./ReactEditorView/focusEditorElement");
54
55
  var _getUAPrefix = require("./ReactEditorView/getUAPrefix");
@@ -362,6 +363,10 @@ function ReactEditorView(props) {
362
363
  var _useState = (0, _react.useState)(0),
363
364
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
364
365
  bumpConfigVersion = _useState2[1];
366
+
367
+ // Preset reference last processed by the schema/API reconciliation below.
368
+ // Used to skip that work when reconfigure is called with the same preset.
369
+ var lastFilteredPresetRef = (0, _react.useRef)(null);
365
370
  var reconfigureState = (0, _react.useCallback)(function (props) {
366
371
  if (!viewRef.current) {
367
372
  return;
@@ -371,7 +376,47 @@ function ReactEditorView(props) {
371
376
  // so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
372
377
  // nodes that haven't been re-rendered to the document yet.
373
378
  blur();
379
+
380
+ // Snapshot plugin names registered before createPluginsList runs, so
381
+ // we can tell which plugins are newly added by the new preset vs.
382
+ // which ones already coexisted with the current schema.
383
+ var previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
374
384
  var editorPlugins = (0, _createPluginsList.default)(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
385
+
386
+ // `state.reconfigure` keeps the original schema, so switching presets
387
+ // can leave the editor inconsistent in two ways:
388
+ // 1. The new preset may add plugins that reference schema nodes or
389
+ // marks the original schema doesn't have.
390
+ // 2. Plugins registered by a previous preset can linger in the
391
+ // injection API even when the new preset doesn't re-register
392
+ // them, so listeners still fire against a state that no longer
393
+ // has their pmPlugin.
394
+ if (lastFilteredPresetRef.current !== props.preset && (0, _platformFeatureFlags.fg)('platform_editor_reconfigure_filter_plugins')) {
395
+ var _filterPluginsForReco = (0, _filterPluginsForReconfigure.filterPluginsForReconfigure)(editorPlugins, viewRef.current.state.schema, previousPluginNames),
396
+ kept = _filterPluginsForReco.kept,
397
+ dropped = _filterPluginsForReco.dropped;
398
+ editorPlugins = kept;
399
+
400
+ // Reconcile the injection API with the post-filter plugin set.
401
+ // This evicts both the plugins we just dropped above (re-registered
402
+ // by createPluginsList but no longer in editorPlugins) AND any
403
+ // plugin from a previous preset that the new preset doesn't
404
+ // re-register.
405
+ var keptPluginNames = new Set(editorPlugins.map(function (p) {
406
+ return p === null || p === void 0 ? void 0 : p.name;
407
+ }).filter(function (n) {
408
+ return Boolean(n);
409
+ }));
410
+ var evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
411
+ if (dropped.length > 0 || evictedFromApi.length > 0) {
412
+ // eslint-disable-next-line no-console
413
+ console.warn('[reconfigureState] Cleanup summary:', {
414
+ dropped: dropped,
415
+ evictedFromApi: evictedFromApi
416
+ });
417
+ }
418
+ lastFilteredPresetRef.current = props.preset;
419
+ }
375
420
  config.current = (0, _createEditor.processPluginsList)(editorPlugins);
376
421
  if ((0, _expValEquals.expValEquals)('platform_editor_appearance_shared_state', 'isEnabled', true)) {
377
422
  var _config$current$pmPlu2;
@@ -72,7 +72,12 @@ function processPluginsList(plugins) {
72
72
  acc.contentComponents.push(plugin.contentComponent);
73
73
  }
74
74
  if (plugin.usePluginHook) {
75
- acc.pluginHooks.push(plugin.usePluginHook);
75
+ // Wrap with .bind(null) so we can annotate the function with the
76
+ // plugin name without mutating the plugin's original hook reference.
77
+ // MountPluginHooks reads `pluginName` to derive a stable React key.
78
+ var named = plugin.usePluginHook.bind(null);
79
+ named.pluginName = plugin.name;
80
+ acc.pluginHooks.push(named);
76
81
  }
77
82
  if (plugin.primaryToolbarComponent) {
78
83
  acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.filterPluginsForReconfigure = filterPluginsForReconfigure;
7
+ /**
8
+ * Used by `reconfigureState` to drop plugins from a freshly-built preset that
9
+ * declare schema nodes/marks the current schema doesn't have. Plugins already
10
+ * present before this reconfigure are kept untouched: they have demonstrably
11
+ * coexisted with the schema (e.g. tests that mock a stripped-down schema), so
12
+ * removing them now would be a regression.
13
+ */
14
+ function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
15
+ var availableNodeNames = new Set(Object.keys(schema.nodes));
16
+ var availableMarkNames = new Set(Object.keys(schema.marks));
17
+ var dropped = [];
18
+ var kept = editorPlugins.filter(function (plugin) {
19
+ if (!plugin) {
20
+ return false;
21
+ }
22
+ if (previousPluginNames.has(plugin.name)) {
23
+ return true;
24
+ }
25
+ var missingNodes = plugin.nodes ? plugin.nodes().map(function (n) {
26
+ return n.name;
27
+ }).filter(function (n) {
28
+ return !availableNodeNames.has(n);
29
+ }) : [];
30
+ var missingMarks = plugin.marks ? plugin.marks().map(function (m) {
31
+ return m.name;
32
+ }).filter(function (m) {
33
+ return !availableMarkNames.has(m);
34
+ }) : [];
35
+ if (missingNodes.length > 0 || missingMarks.length > 0) {
36
+ dropped.push({
37
+ name: plugin.name,
38
+ missingNodes: missingNodes,
39
+ missingMarks: missingMarks
40
+ });
41
+ return false;
42
+ }
43
+ return true;
44
+ });
45
+ return {
46
+ kept: kept,
47
+ dropped: dropped
48
+ };
49
+ }
@@ -56,7 +56,9 @@ function lightProcessPluginsList(editorPlugins) {
56
56
  acc.contentComponents.push(editorPlugin.contentComponent);
57
57
  }
58
58
  if (editorPlugin.usePluginHook) {
59
- acc.pluginHooks.push(editorPlugin.usePluginHook);
59
+ var named = editorPlugin.usePluginHook.bind(null);
60
+ named.pluginName = editorPlugin.name;
61
+ acc.pluginHooks.push(named);
60
62
  }
61
63
  if (editorPlugin.onEditorViewStateUpdated) {
62
64
  acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
@@ -23,12 +23,19 @@ function MountPluginHooks(_ref2) {
23
23
  if (!editorView) {
24
24
  return null;
25
25
  }
26
- return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, key) {
26
+
27
+ // Key each fiber by the plugin name carried on the hook function (set by
28
+ // `processPluginsList`). This keeps fibers stable across `reconfigureState`
29
+ // calls that change the plugin set — keying by array index would let React
30
+ // reuse the same fiber for a different `usePluginHook`, which calls a
31
+ // different sequence of hooks (Rules of Hooks violation).
32
+ // Falls back to the array index for any hook that wasn't annotated.
33
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, index) {
34
+ var pluginName = usePluginHook.pluginName;
27
35
  return /*#__PURE__*/_react.default.createElement(MountPluginHook
28
- // Ignored via go/ees005
29
36
  // eslint-disable-next-line react/no-array-index-key
30
37
  , {
31
- key: key,
38
+ key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
32
39
  usePluginHook: usePluginHook,
33
40
  editorView: editorView,
34
41
  containerElement: containerElement
@@ -5,4 +5,4 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.version = exports.name = void 0;
7
7
  var name = exports.name = "@atlaskit/editor-core";
8
- var version = exports.version = "219.5.0";
8
+ var version = exports.version = "219.5.1";
@@ -38,6 +38,7 @@ import { PROSEMIRROR_RENDERED_DEGRADED_SEVERITY_THRESHOLD, PROSEMIRROR_RENDERED_
38
38
  import { createErrorReporter, createPMPlugins, processPluginsList } from './create-editor';
39
39
  import createPluginsList from './create-plugins-list';
40
40
  import { createSchema } from './create-schema';
41
+ import { filterPluginsForReconfigure } from './filter-plugins-for-reconfigure';
41
42
  import { editorMessages } from './messages';
42
43
  import { focusEditorElement } from './ReactEditorView/focusEditorElement';
43
44
  import { getUAPrefix } from './ReactEditorView/getUAPrefix';
@@ -336,6 +337,10 @@ export function ReactEditorView(props) {
336
337
  // in-place-mutated `config.current` (contentComponents / toolbar
337
338
  // components from the rebuilt preset).
338
339
  const [, bumpConfigVersion] = useState(0);
340
+
341
+ // Preset reference last processed by the schema/API reconciliation below.
342
+ // Used to skip that work when reconfigure is called with the same preset.
343
+ const lastFilteredPresetRef = useRef(null);
339
344
  const reconfigureState = useCallback(props => {
340
345
  if (!viewRef.current) {
341
346
  return;
@@ -345,7 +350,44 @@ export function ReactEditorView(props) {
345
350
  // so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
346
351
  // nodes that haven't been re-rendered to the document yet.
347
352
  blur();
348
- const editorPlugins = createPluginsList(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
353
+
354
+ // Snapshot plugin names registered before createPluginsList runs, so
355
+ // we can tell which plugins are newly added by the new preset vs.
356
+ // which ones already coexisted with the current schema.
357
+ const previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
358
+ let editorPlugins = createPluginsList(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
359
+
360
+ // `state.reconfigure` keeps the original schema, so switching presets
361
+ // can leave the editor inconsistent in two ways:
362
+ // 1. The new preset may add plugins that reference schema nodes or
363
+ // marks the original schema doesn't have.
364
+ // 2. Plugins registered by a previous preset can linger in the
365
+ // injection API even when the new preset doesn't re-register
366
+ // them, so listeners still fire against a state that no longer
367
+ // has their pmPlugin.
368
+ if (lastFilteredPresetRef.current !== props.preset && fg('platform_editor_reconfigure_filter_plugins')) {
369
+ const {
370
+ kept,
371
+ dropped
372
+ } = filterPluginsForReconfigure(editorPlugins, viewRef.current.state.schema, previousPluginNames);
373
+ editorPlugins = kept;
374
+
375
+ // Reconcile the injection API with the post-filter plugin set.
376
+ // This evicts both the plugins we just dropped above (re-registered
377
+ // by createPluginsList but no longer in editorPlugins) AND any
378
+ // plugin from a previous preset that the new preset doesn't
379
+ // re-register.
380
+ const keptPluginNames = new Set(editorPlugins.map(p => p === null || p === void 0 ? void 0 : p.name).filter(n => Boolean(n)));
381
+ const evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
382
+ if (dropped.length > 0 || evictedFromApi.length > 0) {
383
+ // eslint-disable-next-line no-console
384
+ console.warn('[reconfigureState] Cleanup summary:', {
385
+ dropped,
386
+ evictedFromApi
387
+ });
388
+ }
389
+ lastFilteredPresetRef.current = props.preset;
390
+ }
349
391
  config.current = processPluginsList(editorPlugins);
350
392
  if (expValEquals('platform_editor_appearance_shared_state', 'isEnabled', true)) {
351
393
  config.current.pmPlugins.push(...pluginInjectionAPI.current.getInternalPMPlugins());
@@ -53,7 +53,12 @@ export function processPluginsList(plugins) {
53
53
  acc.contentComponents.push(plugin.contentComponent);
54
54
  }
55
55
  if (plugin.usePluginHook) {
56
- acc.pluginHooks.push(plugin.usePluginHook);
56
+ // Wrap with .bind(null) so we can annotate the function with the
57
+ // plugin name without mutating the plugin's original hook reference.
58
+ // MountPluginHooks reads `pluginName` to derive a stable React key.
59
+ const named = plugin.usePluginHook.bind(null);
60
+ named.pluginName = plugin.name;
61
+ acc.pluginHooks.push(named);
57
62
  }
58
63
  if (plugin.primaryToolbarComponent) {
59
64
  acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Used by `reconfigureState` to drop plugins from a freshly-built preset that
3
+ * declare schema nodes/marks the current schema doesn't have. Plugins already
4
+ * present before this reconfigure are kept untouched: they have demonstrably
5
+ * coexisted with the schema (e.g. tests that mock a stripped-down schema), so
6
+ * removing them now would be a regression.
7
+ */
8
+ export function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
9
+ const availableNodeNames = new Set(Object.keys(schema.nodes));
10
+ const availableMarkNames = new Set(Object.keys(schema.marks));
11
+ const dropped = [];
12
+ const kept = editorPlugins.filter(plugin => {
13
+ if (!plugin) {
14
+ return false;
15
+ }
16
+ if (previousPluginNames.has(plugin.name)) {
17
+ return true;
18
+ }
19
+ const missingNodes = plugin.nodes ? plugin.nodes().map(n => n.name).filter(n => !availableNodeNames.has(n)) : [];
20
+ const missingMarks = plugin.marks ? plugin.marks().map(m => m.name).filter(m => !availableMarkNames.has(m)) : [];
21
+ if (missingNodes.length > 0 || missingMarks.length > 0) {
22
+ dropped.push({
23
+ name: plugin.name,
24
+ missingNodes,
25
+ missingMarks
26
+ });
27
+ return false;
28
+ }
29
+ return true;
30
+ });
31
+ return {
32
+ kept,
33
+ dropped
34
+ };
35
+ }
@@ -43,7 +43,9 @@ function lightProcessPluginsList(editorPlugins) {
43
43
  acc.contentComponents.push(editorPlugin.contentComponent);
44
44
  }
45
45
  if (editorPlugin.usePluginHook) {
46
- acc.pluginHooks.push(editorPlugin.usePluginHook);
46
+ const named = editorPlugin.usePluginHook.bind(null);
47
+ named.pluginName = editorPlugin.name;
48
+ acc.pluginHooks.push(named);
47
49
  }
48
50
  if (editorPlugin.onEditorViewStateUpdated) {
49
51
  acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
@@ -18,13 +18,22 @@ export function MountPluginHooks({
18
18
  if (!editorView) {
19
19
  return null;
20
20
  }
21
- return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map((usePluginHook, key) => /*#__PURE__*/React.createElement(MountPluginHook
22
- // Ignored via go/ees005
23
- // eslint-disable-next-line react/no-array-index-key
24
- , {
25
- key: key,
26
- usePluginHook: usePluginHook,
27
- editorView: editorView,
28
- containerElement: containerElement
29
- })));
21
+
22
+ // Key each fiber by the plugin name carried on the hook function (set by
23
+ // `processPluginsList`). This keeps fibers stable across `reconfigureState`
24
+ // calls that change the plugin set — keying by array index would let React
25
+ // reuse the same fiber for a different `usePluginHook`, which calls a
26
+ // different sequence of hooks (Rules of Hooks violation).
27
+ // Falls back to the array index for any hook that wasn't annotated.
28
+ return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map((usePluginHook, index) => {
29
+ const pluginName = usePluginHook.pluginName;
30
+ return /*#__PURE__*/React.createElement(MountPluginHook
31
+ // eslint-disable-next-line react/no-array-index-key
32
+ , {
33
+ key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
34
+ usePluginHook: usePluginHook,
35
+ editorView: editorView,
36
+ containerElement: containerElement
37
+ });
38
+ }));
30
39
  }
@@ -1,2 +1,2 @@
1
1
  export const name = "@atlaskit/editor-core";
2
- export const version = "219.5.0";
2
+ export const version = "219.5.1";
@@ -46,6 +46,7 @@ import { PROSEMIRROR_RENDERED_DEGRADED_SEVERITY_THRESHOLD, PROSEMIRROR_RENDERED_
46
46
  import { createErrorReporter, createPMPlugins, processPluginsList } from './create-editor';
47
47
  import createPluginsList from './create-plugins-list';
48
48
  import { createSchema } from './create-schema';
49
+ import { filterPluginsForReconfigure } from './filter-plugins-for-reconfigure';
49
50
  import { editorMessages } from './messages';
50
51
  import { focusEditorElement } from './ReactEditorView/focusEditorElement';
51
52
  import { getUAPrefix } from './ReactEditorView/getUAPrefix';
@@ -353,6 +354,10 @@ export function ReactEditorView(props) {
353
354
  var _useState = useState(0),
354
355
  _useState2 = _slicedToArray(_useState, 2),
355
356
  bumpConfigVersion = _useState2[1];
357
+
358
+ // Preset reference last processed by the schema/API reconciliation below.
359
+ // Used to skip that work when reconfigure is called with the same preset.
360
+ var lastFilteredPresetRef = useRef(null);
356
361
  var reconfigureState = useCallback(function (props) {
357
362
  if (!viewRef.current) {
358
363
  return;
@@ -362,7 +367,47 @@ export function ReactEditorView(props) {
362
367
  // so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
363
368
  // nodes that haven't been re-rendered to the document yet.
364
369
  blur();
370
+
371
+ // Snapshot plugin names registered before createPluginsList runs, so
372
+ // we can tell which plugins are newly added by the new preset vs.
373
+ // which ones already coexisted with the current schema.
374
+ var previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
365
375
  var editorPlugins = createPluginsList(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
376
+
377
+ // `state.reconfigure` keeps the original schema, so switching presets
378
+ // can leave the editor inconsistent in two ways:
379
+ // 1. The new preset may add plugins that reference schema nodes or
380
+ // marks the original schema doesn't have.
381
+ // 2. Plugins registered by a previous preset can linger in the
382
+ // injection API even when the new preset doesn't re-register
383
+ // them, so listeners still fire against a state that no longer
384
+ // has their pmPlugin.
385
+ if (lastFilteredPresetRef.current !== props.preset && fg('platform_editor_reconfigure_filter_plugins')) {
386
+ var _filterPluginsForReco = filterPluginsForReconfigure(editorPlugins, viewRef.current.state.schema, previousPluginNames),
387
+ kept = _filterPluginsForReco.kept,
388
+ dropped = _filterPluginsForReco.dropped;
389
+ editorPlugins = kept;
390
+
391
+ // Reconcile the injection API with the post-filter plugin set.
392
+ // This evicts both the plugins we just dropped above (re-registered
393
+ // by createPluginsList but no longer in editorPlugins) AND any
394
+ // plugin from a previous preset that the new preset doesn't
395
+ // re-register.
396
+ var keptPluginNames = new Set(editorPlugins.map(function (p) {
397
+ return p === null || p === void 0 ? void 0 : p.name;
398
+ }).filter(function (n) {
399
+ return Boolean(n);
400
+ }));
401
+ var evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
402
+ if (dropped.length > 0 || evictedFromApi.length > 0) {
403
+ // eslint-disable-next-line no-console
404
+ console.warn('[reconfigureState] Cleanup summary:', {
405
+ dropped: dropped,
406
+ evictedFromApi: evictedFromApi
407
+ });
408
+ }
409
+ lastFilteredPresetRef.current = props.preset;
410
+ }
366
411
  config.current = processPluginsList(editorPlugins);
367
412
  if (expValEquals('platform_editor_appearance_shared_state', 'isEnabled', true)) {
368
413
  var _config$current$pmPlu2;
@@ -61,7 +61,12 @@ export function processPluginsList(plugins) {
61
61
  acc.contentComponents.push(plugin.contentComponent);
62
62
  }
63
63
  if (plugin.usePluginHook) {
64
- acc.pluginHooks.push(plugin.usePluginHook);
64
+ // Wrap with .bind(null) so we can annotate the function with the
65
+ // plugin name without mutating the plugin's original hook reference.
66
+ // MountPluginHooks reads `pluginName` to derive a stable React key.
67
+ var named = plugin.usePluginHook.bind(null);
68
+ named.pluginName = plugin.name;
69
+ acc.pluginHooks.push(named);
65
70
  }
66
71
  if (plugin.primaryToolbarComponent) {
67
72
  acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Used by `reconfigureState` to drop plugins from a freshly-built preset that
3
+ * declare schema nodes/marks the current schema doesn't have. Plugins already
4
+ * present before this reconfigure are kept untouched: they have demonstrably
5
+ * coexisted with the schema (e.g. tests that mock a stripped-down schema), so
6
+ * removing them now would be a regression.
7
+ */
8
+ export function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
9
+ var availableNodeNames = new Set(Object.keys(schema.nodes));
10
+ var availableMarkNames = new Set(Object.keys(schema.marks));
11
+ var dropped = [];
12
+ var kept = editorPlugins.filter(function (plugin) {
13
+ if (!plugin) {
14
+ return false;
15
+ }
16
+ if (previousPluginNames.has(plugin.name)) {
17
+ return true;
18
+ }
19
+ var missingNodes = plugin.nodes ? plugin.nodes().map(function (n) {
20
+ return n.name;
21
+ }).filter(function (n) {
22
+ return !availableNodeNames.has(n);
23
+ }) : [];
24
+ var missingMarks = plugin.marks ? plugin.marks().map(function (m) {
25
+ return m.name;
26
+ }).filter(function (m) {
27
+ return !availableMarkNames.has(m);
28
+ }) : [];
29
+ if (missingNodes.length > 0 || missingMarks.length > 0) {
30
+ dropped.push({
31
+ name: plugin.name,
32
+ missingNodes: missingNodes,
33
+ missingMarks: missingMarks
34
+ });
35
+ return false;
36
+ }
37
+ return true;
38
+ });
39
+ return {
40
+ kept: kept,
41
+ dropped: dropped
42
+ };
43
+ }
@@ -50,7 +50,9 @@ function lightProcessPluginsList(editorPlugins) {
50
50
  acc.contentComponents.push(editorPlugin.contentComponent);
51
51
  }
52
52
  if (editorPlugin.usePluginHook) {
53
- acc.pluginHooks.push(editorPlugin.usePluginHook);
53
+ var named = editorPlugin.usePluginHook.bind(null);
54
+ named.pluginName = editorPlugin.name;
55
+ acc.pluginHooks.push(named);
54
56
  }
55
57
  if (editorPlugin.onEditorViewStateUpdated) {
56
58
  acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
@@ -16,12 +16,19 @@ export function MountPluginHooks(_ref2) {
16
16
  if (!editorView) {
17
17
  return null;
18
18
  }
19
- return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, key) {
19
+
20
+ // Key each fiber by the plugin name carried on the hook function (set by
21
+ // `processPluginsList`). This keeps fibers stable across `reconfigureState`
22
+ // calls that change the plugin set — keying by array index would let React
23
+ // reuse the same fiber for a different `usePluginHook`, which calls a
24
+ // different sequence of hooks (Rules of Hooks violation).
25
+ // Falls back to the array index for any hook that wasn't annotated.
26
+ return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, index) {
27
+ var pluginName = usePluginHook.pluginName;
20
28
  return /*#__PURE__*/React.createElement(MountPluginHook
21
- // Ignored via go/ees005
22
29
  // eslint-disable-next-line react/no-array-index-key
23
30
  , {
24
- key: key,
31
+ key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
25
32
  usePluginHook: usePluginHook,
26
33
  editorView: editorView,
27
34
  containerElement: containerElement
@@ -1,2 +1,2 @@
1
1
  export var name = "@atlaskit/editor-core";
2
- export var version = "219.5.0";
2
+ export var version = "219.5.1";
@@ -0,0 +1,19 @@
1
+ import type { EditorPlugin } from '@atlaskit/editor-common/types';
2
+ import type { Schema } from '@atlaskit/editor-prosemirror/model';
3
+ export interface DroppedPlugin {
4
+ missingMarks: string[];
5
+ missingNodes: string[];
6
+ name: string;
7
+ }
8
+ export interface FilterPluginsForReconfigureResult {
9
+ dropped: DroppedPlugin[];
10
+ kept: EditorPlugin[];
11
+ }
12
+ /**
13
+ * Used by `reconfigureState` to drop plugins from a freshly-built preset that
14
+ * declare schema nodes/marks the current schema doesn't have. Plugins already
15
+ * present before this reconfigure are kept untouched: they have demonstrably
16
+ * coexisted with the schema (e.g. tests that mock a stripped-down schema), so
17
+ * removing them now would be a regression.
18
+ */
19
+ export declare function filterPluginsForReconfigure(editorPlugins: Array<EditorPlugin | undefined | null>, schema: Schema, previousPluginNames: ReadonlySet<string>): FilterPluginsForReconfigureResult;
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import type { ReactHookFactory } from '@atlaskit/editor-common/types';
2
+ import type { NamedReactHookFactory } from '@atlaskit/editor-common/types';
3
3
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
4
4
  interface MountPluginHooksProps {
5
5
  containerElement: HTMLElement | null;
6
6
  editorView: EditorView | undefined;
7
- pluginHooks: ReactHookFactory[] | undefined;
7
+ pluginHooks: NamedReactHookFactory[] | undefined;
8
8
  }
9
9
  export declare function MountPluginHooks({ pluginHooks, editorView, containerElement, }: MountPluginHooksProps): React.JSX.Element | null;
10
10
  export {};
@@ -0,0 +1,19 @@
1
+ import type { EditorPlugin } from '@atlaskit/editor-common/types';
2
+ import type { Schema } from '@atlaskit/editor-prosemirror/model';
3
+ export interface DroppedPlugin {
4
+ missingMarks: string[];
5
+ missingNodes: string[];
6
+ name: string;
7
+ }
8
+ export interface FilterPluginsForReconfigureResult {
9
+ dropped: DroppedPlugin[];
10
+ kept: EditorPlugin[];
11
+ }
12
+ /**
13
+ * Used by `reconfigureState` to drop plugins from a freshly-built preset that
14
+ * declare schema nodes/marks the current schema doesn't have. Plugins already
15
+ * present before this reconfigure are kept untouched: they have demonstrably
16
+ * coexisted with the schema (e.g. tests that mock a stripped-down schema), so
17
+ * removing them now would be a regression.
18
+ */
19
+ export declare function filterPluginsForReconfigure(editorPlugins: Array<EditorPlugin | undefined | null>, schema: Schema, previousPluginNames: ReadonlySet<string>): FilterPluginsForReconfigureResult;
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import type { ReactHookFactory } from '@atlaskit/editor-common/types';
2
+ import type { NamedReactHookFactory } from '@atlaskit/editor-common/types';
3
3
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
4
4
  interface MountPluginHooksProps {
5
5
  containerElement: HTMLElement | null;
6
6
  editorView: EditorView | undefined;
7
- pluginHooks: ReactHookFactory[] | undefined;
7
+ pluginHooks: NamedReactHookFactory[] | undefined;
8
8
  }
9
9
  export declare function MountPluginHooks({ pluginHooks, editorView, containerElement, }: MountPluginHooksProps): React.JSX.Element | null;
10
10
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-core",
3
- "version": "219.5.1",
3
+ "version": "219.6.0",
4
4
  "description": "A package contains Atlassian editor core functionality",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -64,7 +64,7 @@
64
64
  "@atlaskit/editor-ssr-renderer": "^5.1.0",
65
65
  "@atlaskit/editor-toolbar": "^1.0.0",
66
66
  "@atlaskit/editor-toolbar-model": "^0.4.0",
67
- "@atlaskit/emoji": "^70.4.0",
67
+ "@atlaskit/emoji": "^70.5.0",
68
68
  "@atlaskit/feature-gate-js-client": "^5.5.0",
69
69
  "@atlaskit/icon": "^34.5.0",
70
70
  "@atlaskit/link": "^3.4.0",
@@ -92,7 +92,7 @@
92
92
  "uuid": "^3.1.0"
93
93
  },
94
94
  "peerDependencies": {
95
- "@atlaskit/editor-common": "^114.26.0",
95
+ "@atlaskit/editor-common": "^114.27.0",
96
96
  "@atlaskit/link-provider": "^4.4.0",
97
97
  "@atlaskit/media-core": "^37.0.0",
98
98
  "react": "^18.2.0",
@@ -106,7 +106,7 @@
106
106
  "@atlaskit/adf-utils": "^19.29.0",
107
107
  "@atlaskit/analytics-listeners": "^10.0.0",
108
108
  "@atlaskit/collab-provider": "^18.2.0",
109
- "@atlaskit/editor-plugin-annotation": "^10.2.0",
109
+ "@atlaskit/editor-plugin-annotation": "^10.3.0",
110
110
  "@atlaskit/editor-plugin-card": "^16.5.0",
111
111
  "@atlaskit/editor-plugin-list": "^12.0.0",
112
112
  "@atlaskit/editor-plugin-paste": "^11.1.0",
@@ -167,6 +167,9 @@
167
167
  }
168
168
  },
169
169
  "platform-feature-flags": {
170
+ "platform_editor_reconfigure_filter_plugins": {
171
+ "type": "boolean"
172
+ },
170
173
  "linking_platform_datasource_assets_objects": {
171
174
  "type": "boolean",
172
175
  "referenceOnly": "true"