@atlaskit/editor-plugin-media 12.1.5 → 12.2.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-plugin-media
2
2
 
3
+ ## 12.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`a0b1822615d7e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a0b1822615d7e) -
8
+ Refactor inline editor AI image generation loading state to use ProseMirror decorations:
9
+ - Add AI generating decoration plugin to editor-plugin-media for transient visual state tracking
10
+ via ProseMirror decorations instead of ADF schema attributes
11
+ - Remove \_\_isAIGenerating transient attribute from ADF media node schema
12
+ - Update editor-rovo-bridge to dispatch decoration meta instead of mutating node attributes
13
+ - Media NodeView reads decoration state and passes isAIGenerating prop to media-card
14
+ - AIBorder component with pulsing gradient border and translucent blanket during AI image
15
+ generation
16
+ - Internationalized AI generating progress bar aria label
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+
3
22
  ## 12.1.5
4
23
 
5
24
  ### Patch Changes
@@ -27,6 +27,7 @@ var _media = require("./nodeviews/toDOM-fixes/media");
27
27
  var _mediaGroup = require("./nodeviews/toDOM-fixes/mediaGroup");
28
28
  var _mediaInline = require("./nodeviews/toDOM-fixes/mediaInline");
29
29
  var _mediaSingle2 = require("./nodeviews/toDOM-fixes/mediaSingle");
30
+ var _aiGeneratingDecoration = require("./pm-plugins/ai-generating-decoration");
30
31
  var _altText = require("./pm-plugins/alt-text");
31
32
  var _keymap = _interopRequireDefault(require("./pm-plugins/alt-text/keymap"));
32
33
  var _commands = require("./pm-plugins/commands");
@@ -180,6 +181,8 @@ var mediaPlugin = exports.mediaPlugin = function mediaPlugin(_ref3) {
180
181
  showMediaViewer: _commands.showMediaViewer,
181
182
  hideMediaViewer: _commands.hideMediaViewer,
182
183
  trackMediaPaste: _commands.trackMediaPaste,
184
+ setAIGenerating: _commands.setAIGenerating,
185
+ clearAIGenerating: _commands.clearAIGenerating,
183
186
  insertMediaSingle: (0, _commands.insertMediaAsMediaSingleCommand)(api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions, options.allowPixelResizing)
184
187
  },
185
188
  nodes: function nodes() {
@@ -305,6 +308,12 @@ var mediaPlugin = exports.mediaPlugin = function mediaPlugin(_ref3) {
305
308
  plugin: _pixelResizing.createPlugin
306
309
  });
307
310
  }
311
+ pmPlugins.push({
312
+ name: 'mediaAIGeneratingDecoration',
313
+ plugin: function plugin() {
314
+ return (0, _aiGeneratingDecoration.createAIGeneratingDecorationPlugin)();
315
+ }
316
+ });
308
317
  pmPlugins.push({
309
318
  name: 'mediaSelectionHandler',
310
319
  plugin: function plugin() {
@@ -23,6 +23,7 @@ var _useSharedPluginStateSelector = require("@atlaskit/editor-common/use-shared-
23
23
  var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
24
24
  var _mediaClient = require("@atlaskit/media-client");
25
25
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
26
+ var _aiGeneratingDecoration = require("../../pm-plugins/ai-generating-decoration");
26
27
  var _helpers = require("../../pm-plugins/commands/helpers");
27
28
  var _mediaCommon = require("../../pm-plugins/utils/media-common");
28
29
  var _media = _interopRequireDefault(require("./media"));
@@ -62,6 +63,7 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
62
63
  }
63
64
  _this = _callSuper(this, MediaNodeView, [].concat(args));
64
65
  (0, _defineProperty2.default)(_this, "isSelected", false);
66
+ (0, _defineProperty2.default)(_this, "isAIGenerating", false);
65
67
  (0, _defineProperty2.default)(_this, "hasBeenResized", false);
66
68
  (0, _defineProperty2.default)(_this, "hasResizedListener", function () {
67
69
  if (!_this.hasBeenResized) {
@@ -178,7 +180,8 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
178
180
  mediaOptions: mediaOptions,
179
181
  onExternalImageLoaded: _this.onExternalImageLoaded,
180
182
  isViewOnly: ((_this$reactComponentP = _this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP === void 0 || (_this$reactComponentP = _this$reactComponentP.editorViewMode) === null || _this$reactComponentP === void 0 || (_this$reactComponentP = _this$reactComponentP.sharedState.currentState()) === null || _this$reactComponentP === void 0 ? void 0 : _this$reactComponentP.mode) === 'view',
181
- pluginInjectionApi: _this.reactComponentProps.pluginInjectionApi
183
+ pluginInjectionApi: _this.reactComponentProps.pluginInjectionApi,
184
+ isAIGenerating: _this.isAIGenerating
182
185
  });
183
186
  };
184
187
  });
@@ -268,6 +271,11 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
268
271
  this.isSelected = hasMediaNodeSelectedDecoration;
269
272
  return true;
270
273
  }
274
+ var aiGenerating = (0, _aiGeneratingDecoration.hasAIGeneratingDecoration)(decorations);
275
+ if (this.isAIGenerating !== aiGenerating) {
276
+ this.isAIGenerating = aiGenerating;
277
+ return true;
278
+ }
271
279
  if (this.node.attrs !== nextNode.attrs) {
272
280
  return true;
273
281
  }
@@ -181,7 +181,7 @@ var MediaNode = exports.MediaNode = /*#__PURE__*/function (_Component) {
181
181
  value: function shouldComponentUpdate(nextProps, nextState) {
182
182
  var hasNewViewMediaClientConfig = !this.state.viewMediaClientConfig && nextState.viewMediaClientConfig;
183
183
  var hasNewViewAndUploadMediaClientConfig = !this.state.viewAndUploadMediaClientConfig && nextState.viewAndUploadMediaClientConfig;
184
- if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || (0, _expValEquals.expValEquals)('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
184
+ if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.isAIGenerating !== nextProps.isAIGenerating || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || (0, _expValEquals.expValEquals)('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
185
185
  return true;
186
186
  }
187
187
  return false;
@@ -391,6 +391,7 @@ var MediaNode = exports.MediaNode = /*#__PURE__*/function (_Component) {
391
391
  videoControlsWrapperRef: this.videoControlsWrapperRef,
392
392
  ssr: ssr,
393
393
  mediaSettings: this.getMediaSettings(viewAndUploadMediaClientConfig),
394
+ isAIGenerating: !!this.props.isAIGenerating,
394
395
  onError: (0, _expValEquals.expValEquals)('platform_editor_media_error_analytics', 'isEnabled', true) ? this.onError : undefined
395
396
  })));
396
397
  }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.aiGeneratingDecorationPluginKey = void 0;
8
+ exports.clearAIGeneratingMeta = clearAIGeneratingMeta;
9
+ exports.createAIGeneratingDecorationPlugin = createAIGeneratingDecorationPlugin;
10
+ exports.hasAIGeneratingDecoration = hasAIGeneratingDecoration;
11
+ exports.setAIGeneratingMeta = setAIGeneratingMeta;
12
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
+ var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
14
+ var _state = require("@atlaskit/editor-prosemirror/state");
15
+ var _view = require("@atlaskit/editor-prosemirror/view");
16
+ var _featureGateJsClient = _interopRequireDefault(require("@atlaskit/feature-gate-js-client"));
17
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
18
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
19
+ /**
20
+ * ProseMirror plugin that manages AI-generating decorations on media nodes.
21
+ *
22
+ * Instead of storing transient `__isAIGenerating` attributes in the ADF schema
23
+ * (which pollutes the document model and undo history), this plugin uses
24
+ * ProseMirror decorations — a view-layer-only mechanism that never affects the
25
+ * document content, serialization, or undo/redo stack.
26
+ */
27
+
28
+ // ── Plugin Key ──────────────────────────────────────────────────────────────
29
+
30
+ var aiGeneratingDecorationPluginKey = exports.aiGeneratingDecorationPluginKey = new _state.PluginKey('aiGeneratingDecoration');
31
+
32
+ // ── Types ───────────────────────────────────────────────────────────────────
33
+
34
+ var AI_GENERATING_DECORATION_TYPE = 'ai-generating';
35
+
36
+ // ── Helpers ─────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Build a DecorationSet containing a Decoration.node for every media node
40
+ * whose `id` is in the given set.
41
+ */
42
+ function buildDecorationSet(doc, mediaIds) {
43
+ if (mediaIds.size === 0) {
44
+ return _view.DecorationSet.empty;
45
+ }
46
+ var decorations = [];
47
+ doc.descendants(function (node, pos) {
48
+ if (node.type.name === 'media' && mediaIds.has(node.attrs.id)) {
49
+ decorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {},
50
+ // no DOM attrs needed — the NodeView reads the decoration spec
51
+ {
52
+ type: AI_GENERATING_DECORATION_TYPE,
53
+ mediaId: node.attrs.id
54
+ }));
55
+ }
56
+ });
57
+ return _view.DecorationSet.create(doc, decorations);
58
+ }
59
+
60
+ // ── Public utilities ────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Returns `true` if the given decorations array contains an AI-generating
64
+ * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()`
65
+ * to determine whether to render the AI border.
66
+ */
67
+ function hasAIGeneratingDecoration(decorations) {
68
+ return decorations.some(function (d) {
69
+ return d.spec.type === AI_GENERATING_DECORATION_TYPE;
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Dispatch a transaction that sets the AI-generating decoration on a media
75
+ * node identified by `mediaId`.
76
+ *
77
+ * Usage from the editor bridge:
78
+ * ```
79
+ * editorAPI.core.actions.execute(({ tr }) =>
80
+ * setAIGeneratingMeta(tr, mediaId),
81
+ * );
82
+ * ```
83
+ */
84
+ function setAIGeneratingMeta(tr, mediaId) {
85
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
86
+ type: 'SET_GENERATING',
87
+ mediaId: mediaId
88
+ }).setMeta('addToHistory', false);
89
+ }
90
+
91
+ /**
92
+ * Dispatch a transaction that clears the AI-generating decoration for a
93
+ * specific media node.
94
+ */
95
+ function clearAIGeneratingMeta(tr, mediaId) {
96
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
97
+ type: 'CLEAR_GENERATING',
98
+ mediaId: mediaId
99
+ }).setMeta('addToHistory', false);
100
+ }
101
+
102
+ // ── Plugin ──────────────────────────────────────────────────────────────────
103
+
104
+ /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */
105
+ function createAIGeneratingDecorationPlugin() {
106
+ return new _safePlugin.SafePlugin({
107
+ key: aiGeneratingDecorationPluginKey,
108
+ state: {
109
+ init: function init() {
110
+ return {
111
+ generatingMediaIds: new Set(),
112
+ decorationSet: _view.DecorationSet.empty
113
+ };
114
+ },
115
+ apply: function apply(tr, pluginState, _oldState, newState) {
116
+ // Killswitch — if active, clear any existing decorations and stop
117
+ // eslint-disable-next-line @atlaskit/platform/use-recommended-utils -- dynamic config killswitch, not a standard feature gate
118
+ if (_featureGateJsClient.default.getExperimentValue('maui_ai_border_killswitch', 'value', false)) {
119
+ if (pluginState.generatingMediaIds.size > 0) {
120
+ return {
121
+ generatingMediaIds: new Set(),
122
+ decorationSet: _view.DecorationSet.empty
123
+ };
124
+ }
125
+ return pluginState;
126
+ }
127
+ var meta = tr.getMeta(aiGeneratingDecorationPluginKey);
128
+ if (meta) {
129
+ switch (meta.type) {
130
+ case 'SET_GENERATING':
131
+ {
132
+ var ids = new Set(pluginState.generatingMediaIds);
133
+ ids.add(meta.mediaId);
134
+ return {
135
+ generatingMediaIds: ids,
136
+ decorationSet: buildDecorationSet(newState.doc, ids)
137
+ };
138
+ }
139
+ case 'CLEAR_GENERATING':
140
+ {
141
+ var _ids = new Set(pluginState.generatingMediaIds);
142
+ _ids.delete(meta.mediaId);
143
+ return {
144
+ generatingMediaIds: _ids,
145
+ decorationSet: buildDecorationSet(newState.doc, _ids)
146
+ };
147
+ }
148
+ case 'CLEAR_ALL':
149
+ return {
150
+ generatingMediaIds: new Set(),
151
+ decorationSet: _view.DecorationSet.empty
152
+ };
153
+ }
154
+ }
155
+
156
+ // Map decorations through document changes so positions stay in sync
157
+ if (tr.docChanged && pluginState.decorationSet !== _view.DecorationSet.empty) {
158
+ try {
159
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
160
+ decorationSet: pluginState.decorationSet.map(tr.mapping, newState.doc)
161
+ });
162
+ } catch (_unused) {
163
+ // Collaborative editing edge case — reset
164
+ return {
165
+ generatingMediaIds: new Set(),
166
+ decorationSet: _view.DecorationSet.empty
167
+ };
168
+ }
169
+ }
170
+ return pluginState;
171
+ }
172
+ },
173
+ props: {
174
+ decorations: function decorations(state) {
175
+ var _aiGeneratingDecorati, _aiGeneratingDecorati2;
176
+ return (_aiGeneratingDecorati = (_aiGeneratingDecorati2 = aiGeneratingDecorationPluginKey.getState(state)) === null || _aiGeneratingDecorati2 === void 0 ? void 0 : _aiGeneratingDecorati2.decorationSet) !== null && _aiGeneratingDecorati !== void 0 ? _aiGeneratingDecorati : _view.DecorationSet.empty;
177
+ }
178
+ }
179
+ });
180
+ }
@@ -3,8 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.trackMediaPaste = exports.showMediaViewer = exports.insertMediaAsMediaSingleCommand = exports.hideMediaViewer = void 0;
6
+ exports.trackMediaPaste = exports.showMediaViewer = exports.setAIGenerating = exports.insertMediaAsMediaSingleCommand = exports.hideMediaViewer = exports.clearAIGenerating = void 0;
7
7
  var _actions = require("../pm-plugins/actions");
8
+ var _aiGeneratingDecoration = require("../pm-plugins/ai-generating-decoration");
8
9
  var _pluginKey = require("../pm-plugins/plugin-key");
9
10
  var _mediaCommon = require("../pm-plugins/utils/media-common");
10
11
  var _mediaSingle = require("./utils/media-single");
@@ -39,6 +40,31 @@ var trackMediaPaste = exports.trackMediaPaste = function trackMediaPaste(attrs)
39
40
  return tr;
40
41
  };
41
42
  };
43
+
44
+ /**
45
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
46
+ * The decoration triggers the AI border visual on the media's NodeView.
47
+ *
48
+ * Decorations live in the view layer only and never affect the document model
49
+ * or undo/redo history.
50
+ */
51
+ var setAIGenerating = exports.setAIGenerating = function setAIGenerating(mediaId) {
52
+ return function (_ref4) {
53
+ var tr = _ref4.tr;
54
+ return (0, _aiGeneratingDecoration.setAIGeneratingMeta)(tr, mediaId);
55
+ };
56
+ };
57
+
58
+ /**
59
+ * Clears the AI-generating decoration for a specific media node identified by
60
+ * `mediaId`. Removes the AI border visual from that media's NodeView.
61
+ */
62
+ var clearAIGenerating = exports.clearAIGenerating = function clearAIGenerating(mediaId) {
63
+ return function (_ref5) {
64
+ var tr = _ref5.tr;
65
+ return (0, _aiGeneratingDecoration.clearAIGeneratingMeta)(tr, mediaId);
66
+ };
67
+ };
42
68
  var insertMediaAsMediaSingleCommand = exports.insertMediaAsMediaSingleCommand = function insertMediaAsMediaSingleCommand(editorAnalyticsAPI, allowPixelResizing) {
43
69
  return function (mediaAttrs, inputMethod, insertMediaVia) {
44
70
  return (0, _mediaSingle.createInsertMediaAsMediaSingleCommand)(mediaAttrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing);
@@ -18,9 +18,10 @@ import { mediaSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/media';
18
18
  import { mediaGroupSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaGroup';
19
19
  import { mediaInlineSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaInline';
20
20
  import { mediaSingleSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaSingle';
21
+ import { createAIGeneratingDecorationPlugin } from './pm-plugins/ai-generating-decoration';
21
22
  import { createPlugin as createMediaAltTextPlugin } from './pm-plugins/alt-text';
22
23
  import keymapMediaAltTextPlugin from './pm-plugins/alt-text/keymap';
23
- import { hideMediaViewer, insertMediaAsMediaSingleCommand, showMediaViewer, trackMediaPaste } from './pm-plugins/commands';
24
+ import { clearAIGenerating, hideMediaViewer, insertMediaAsMediaSingleCommand, setAIGenerating, showMediaViewer, trackMediaPaste } from './pm-plugins/commands';
24
25
  import keymapPlugin from './pm-plugins/keymap';
25
26
  import keymapMediaSinglePlugin from './pm-plugins/keymap-media';
26
27
  import linkingPlugin from './pm-plugins/linking';
@@ -173,6 +174,8 @@ export const mediaPlugin = ({
173
174
  showMediaViewer,
174
175
  hideMediaViewer,
175
176
  trackMediaPaste,
177
+ setAIGenerating,
178
+ clearAIGenerating,
176
179
  insertMediaSingle: insertMediaAsMediaSingleCommand(api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions, options.allowPixelResizing)
177
180
  },
178
181
  nodes() {
@@ -296,6 +299,10 @@ export const mediaPlugin = ({
296
299
  plugin: createMediaPixelResizingPlugin
297
300
  });
298
301
  }
302
+ pmPlugins.push({
303
+ name: 'mediaAIGeneratingDecoration',
304
+ plugin: () => createAIGeneratingDecorationPlugin()
305
+ });
299
306
  pmPlugins.push({
300
307
  name: 'mediaSelectionHandler',
301
308
  plugin: () => {
@@ -9,6 +9,7 @@ import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared
9
9
  import { akEditorFullWidthLayoutWidth, akEditorDefaultLayoutWidth, akEditorCalculatedWideLayoutWidth } from '@atlaskit/editor-shared-styles';
10
10
  import { getAttrsFromUrl } from '@atlaskit/media-client';
11
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
+ import { hasAIGeneratingDecoration } from '../../pm-plugins/ai-generating-decoration';
12
13
  import { updateCurrentMediaNodeAttrs } from '../../pm-plugins/commands/helpers';
13
14
  import { isMediaBlobUrlFromAttrs } from '../../pm-plugins/utils/media-common';
14
15
  // Ignored via go/ees005
@@ -40,6 +41,7 @@ class MediaNodeView extends SelectionBasedNodeView {
40
41
  constructor(...args) {
41
42
  super(...args);
42
43
  _defineProperty(this, "isSelected", false);
44
+ _defineProperty(this, "isAIGenerating", false);
43
45
  _defineProperty(this, "hasBeenResized", false);
44
46
  _defineProperty(this, "hasResizedListener", () => {
45
47
  if (!this.hasBeenResized) {
@@ -167,7 +169,8 @@ class MediaNodeView extends SelectionBasedNodeView {
167
169
  mediaOptions: mediaOptions,
168
170
  onExternalImageLoaded: this.onExternalImageLoaded,
169
171
  isViewOnly: ((_this$reactComponentP = this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP === void 0 ? void 0 : (_this$reactComponentP2 = _this$reactComponentP.editorViewMode) === null || _this$reactComponentP2 === void 0 ? void 0 : (_this$reactComponentP3 = _this$reactComponentP2.sharedState.currentState()) === null || _this$reactComponentP3 === void 0 ? void 0 : _this$reactComponentP3.mode) === 'view',
170
- pluginInjectionApi: this.reactComponentProps.pluginInjectionApi
172
+ pluginInjectionApi: this.reactComponentProps.pluginInjectionApi,
173
+ isAIGenerating: this.isAIGenerating
171
174
  });
172
175
  };
173
176
  });
@@ -248,6 +251,11 @@ class MediaNodeView extends SelectionBasedNodeView {
248
251
  this.isSelected = hasMediaNodeSelectedDecoration;
249
252
  return true;
250
253
  }
254
+ const aiGenerating = hasAIGeneratingDecoration(decorations);
255
+ if (this.isAIGenerating !== aiGenerating) {
256
+ this.isAIGenerating = aiGenerating;
257
+ return true;
258
+ }
251
259
  if (this.node.attrs !== nextNode.attrs) {
252
260
  return true;
253
261
  }
@@ -150,7 +150,7 @@ export class MediaNode extends Component {
150
150
  shouldComponentUpdate(nextProps, nextState) {
151
151
  const hasNewViewMediaClientConfig = !this.state.viewMediaClientConfig && nextState.viewMediaClientConfig;
152
152
  const hasNewViewAndUploadMediaClientConfig = !this.state.viewAndUploadMediaClientConfig && nextState.viewAndUploadMediaClientConfig;
153
- if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
153
+ if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.isAIGenerating !== nextProps.isAIGenerating || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
154
154
  return true;
155
155
  }
156
156
  return false;
@@ -332,6 +332,7 @@ export class MediaNode extends Component {
332
332
  videoControlsWrapperRef: this.videoControlsWrapperRef,
333
333
  ssr: ssr,
334
334
  mediaSettings: this.getMediaSettings(viewAndUploadMediaClientConfig),
335
+ isAIGenerating: !!this.props.isAIGenerating,
335
336
  onError: expValEquals('platform_editor_media_error_analytics', 'isEnabled', true) ? this.onError : undefined
336
337
  })));
337
338
  }
@@ -0,0 +1,166 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
3
+ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
4
+ import FeatureGates from '@atlaskit/feature-gate-js-client';
5
+
6
+ /**
7
+ * ProseMirror plugin that manages AI-generating decorations on media nodes.
8
+ *
9
+ * Instead of storing transient `__isAIGenerating` attributes in the ADF schema
10
+ * (which pollutes the document model and undo history), this plugin uses
11
+ * ProseMirror decorations — a view-layer-only mechanism that never affects the
12
+ * document content, serialization, or undo/redo stack.
13
+ */
14
+
15
+ // ── Plugin Key ──────────────────────────────────────────────────────────────
16
+
17
+ export const aiGeneratingDecorationPluginKey = new PluginKey('aiGeneratingDecoration');
18
+
19
+ // ── Types ───────────────────────────────────────────────────────────────────
20
+
21
+ const AI_GENERATING_DECORATION_TYPE = 'ai-generating';
22
+
23
+ // ── Helpers ─────────────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Build a DecorationSet containing a Decoration.node for every media node
27
+ * whose `id` is in the given set.
28
+ */
29
+ function buildDecorationSet(doc, mediaIds) {
30
+ if (mediaIds.size === 0) {
31
+ return DecorationSet.empty;
32
+ }
33
+ const decorations = [];
34
+ doc.descendants((node, pos) => {
35
+ if (node.type.name === 'media' && mediaIds.has(node.attrs.id)) {
36
+ decorations.push(Decoration.node(pos, pos + node.nodeSize, {},
37
+ // no DOM attrs needed — the NodeView reads the decoration spec
38
+ {
39
+ type: AI_GENERATING_DECORATION_TYPE,
40
+ mediaId: node.attrs.id
41
+ }));
42
+ }
43
+ });
44
+ return DecorationSet.create(doc, decorations);
45
+ }
46
+
47
+ // ── Public utilities ────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Returns `true` if the given decorations array contains an AI-generating
51
+ * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()`
52
+ * to determine whether to render the AI border.
53
+ */
54
+ export function hasAIGeneratingDecoration(decorations) {
55
+ return decorations.some(d => d.spec.type === AI_GENERATING_DECORATION_TYPE);
56
+ }
57
+
58
+ /**
59
+ * Dispatch a transaction that sets the AI-generating decoration on a media
60
+ * node identified by `mediaId`.
61
+ *
62
+ * Usage from the editor bridge:
63
+ * ```
64
+ * editorAPI.core.actions.execute(({ tr }) =>
65
+ * setAIGeneratingMeta(tr, mediaId),
66
+ * );
67
+ * ```
68
+ */
69
+ export function setAIGeneratingMeta(tr, mediaId) {
70
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
71
+ type: 'SET_GENERATING',
72
+ mediaId
73
+ }).setMeta('addToHistory', false);
74
+ }
75
+
76
+ /**
77
+ * Dispatch a transaction that clears the AI-generating decoration for a
78
+ * specific media node.
79
+ */
80
+ export function clearAIGeneratingMeta(tr, mediaId) {
81
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
82
+ type: 'CLEAR_GENERATING',
83
+ mediaId
84
+ }).setMeta('addToHistory', false);
85
+ }
86
+
87
+ // ── Plugin ──────────────────────────────────────────────────────────────────
88
+
89
+ /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */
90
+ export function createAIGeneratingDecorationPlugin() {
91
+ return new SafePlugin({
92
+ key: aiGeneratingDecorationPluginKey,
93
+ state: {
94
+ init() {
95
+ return {
96
+ generatingMediaIds: new Set(),
97
+ decorationSet: DecorationSet.empty
98
+ };
99
+ },
100
+ apply(tr, pluginState, _oldState, newState) {
101
+ // Killswitch — if active, clear any existing decorations and stop
102
+ // eslint-disable-next-line @atlaskit/platform/use-recommended-utils -- dynamic config killswitch, not a standard feature gate
103
+ if (FeatureGates.getExperimentValue('maui_ai_border_killswitch', 'value', false)) {
104
+ if (pluginState.generatingMediaIds.size > 0) {
105
+ return {
106
+ generatingMediaIds: new Set(),
107
+ decorationSet: DecorationSet.empty
108
+ };
109
+ }
110
+ return pluginState;
111
+ }
112
+ const meta = tr.getMeta(aiGeneratingDecorationPluginKey);
113
+ if (meta) {
114
+ switch (meta.type) {
115
+ case 'SET_GENERATING':
116
+ {
117
+ const ids = new Set(pluginState.generatingMediaIds);
118
+ ids.add(meta.mediaId);
119
+ return {
120
+ generatingMediaIds: ids,
121
+ decorationSet: buildDecorationSet(newState.doc, ids)
122
+ };
123
+ }
124
+ case 'CLEAR_GENERATING':
125
+ {
126
+ const ids = new Set(pluginState.generatingMediaIds);
127
+ ids.delete(meta.mediaId);
128
+ return {
129
+ generatingMediaIds: ids,
130
+ decorationSet: buildDecorationSet(newState.doc, ids)
131
+ };
132
+ }
133
+ case 'CLEAR_ALL':
134
+ return {
135
+ generatingMediaIds: new Set(),
136
+ decorationSet: DecorationSet.empty
137
+ };
138
+ }
139
+ }
140
+
141
+ // Map decorations through document changes so positions stay in sync
142
+ if (tr.docChanged && pluginState.decorationSet !== DecorationSet.empty) {
143
+ try {
144
+ return {
145
+ ...pluginState,
146
+ decorationSet: pluginState.decorationSet.map(tr.mapping, newState.doc)
147
+ };
148
+ } catch {
149
+ // Collaborative editing edge case — reset
150
+ return {
151
+ generatingMediaIds: new Set(),
152
+ decorationSet: DecorationSet.empty
153
+ };
154
+ }
155
+ }
156
+ return pluginState;
157
+ }
158
+ },
159
+ props: {
160
+ decorations(state) {
161
+ var _aiGeneratingDecorati, _aiGeneratingDecorati2;
162
+ return (_aiGeneratingDecorati = (_aiGeneratingDecorati2 = aiGeneratingDecorationPluginKey.getState(state)) === null || _aiGeneratingDecorati2 === void 0 ? void 0 : _aiGeneratingDecorati2.decorationSet) !== null && _aiGeneratingDecorati !== void 0 ? _aiGeneratingDecorati : DecorationSet.empty;
163
+ }
164
+ }
165
+ });
166
+ }
@@ -1,4 +1,5 @@
1
1
  import { ACTIONS } from '../pm-plugins/actions';
2
+ import { setAIGeneratingMeta, clearAIGeneratingMeta } from '../pm-plugins/ai-generating-decoration';
2
3
  import { stateKey } from '../pm-plugins/plugin-key';
3
4
  import { getIdentifier } from '../pm-plugins/utils/media-common';
4
5
  import { createInsertMediaAsMediaSingleCommand } from './utils/media-single';
@@ -32,4 +33,23 @@ export const trackMediaPaste = attrs => ({
32
33
  });
33
34
  return tr;
34
35
  };
36
+
37
+ /**
38
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
39
+ * The decoration triggers the AI border visual on the media's NodeView.
40
+ *
41
+ * Decorations live in the view layer only and never affect the document model
42
+ * or undo/redo history.
43
+ */
44
+ export const setAIGenerating = mediaId => ({
45
+ tr
46
+ }) => setAIGeneratingMeta(tr, mediaId);
47
+
48
+ /**
49
+ * Clears the AI-generating decoration for a specific media node identified by
50
+ * `mediaId`. Removes the AI border visual from that media's NodeView.
51
+ */
52
+ export const clearAIGenerating = mediaId => ({
53
+ tr
54
+ }) => clearAIGeneratingMeta(tr, mediaId);
35
55
  export const insertMediaAsMediaSingleCommand = (editorAnalyticsAPI, allowPixelResizing) => (mediaAttrs, inputMethod, insertMediaVia) => createInsertMediaAsMediaSingleCommand(mediaAttrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing);
@@ -21,9 +21,10 @@ import { mediaSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/media';
21
21
  import { mediaGroupSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaGroup';
22
22
  import { mediaInlineSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaInline';
23
23
  import { mediaSingleSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaSingle';
24
+ import { createAIGeneratingDecorationPlugin } from './pm-plugins/ai-generating-decoration';
24
25
  import { createPlugin as createMediaAltTextPlugin } from './pm-plugins/alt-text';
25
26
  import keymapMediaAltTextPlugin from './pm-plugins/alt-text/keymap';
26
- import { hideMediaViewer, insertMediaAsMediaSingleCommand, showMediaViewer, trackMediaPaste } from './pm-plugins/commands';
27
+ import { clearAIGenerating, hideMediaViewer, insertMediaAsMediaSingleCommand, setAIGenerating, showMediaViewer, trackMediaPaste } from './pm-plugins/commands';
27
28
  import keymapPlugin from './pm-plugins/keymap';
28
29
  import keymapMediaSinglePlugin from './pm-plugins/keymap-media';
29
30
  import linkingPlugin from './pm-plugins/linking';
@@ -171,6 +172,8 @@ export var mediaPlugin = function mediaPlugin(_ref3) {
171
172
  showMediaViewer: showMediaViewer,
172
173
  hideMediaViewer: hideMediaViewer,
173
174
  trackMediaPaste: trackMediaPaste,
175
+ setAIGenerating: setAIGenerating,
176
+ clearAIGenerating: clearAIGenerating,
174
177
  insertMediaSingle: insertMediaAsMediaSingleCommand(api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions, options.allowPixelResizing)
175
178
  },
176
179
  nodes: function nodes() {
@@ -296,6 +299,12 @@ export var mediaPlugin = function mediaPlugin(_ref3) {
296
299
  plugin: createMediaPixelResizingPlugin
297
300
  });
298
301
  }
302
+ pmPlugins.push({
303
+ name: 'mediaAIGeneratingDecoration',
304
+ plugin: function plugin() {
305
+ return createAIGeneratingDecorationPlugin();
306
+ }
307
+ });
299
308
  pmPlugins.push({
300
309
  name: 'mediaSelectionHandler',
301
310
  plugin: function plugin() {
@@ -22,6 +22,7 @@ import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared
22
22
  import { akEditorFullWidthLayoutWidth, akEditorDefaultLayoutWidth, akEditorCalculatedWideLayoutWidth } from '@atlaskit/editor-shared-styles';
23
23
  import { getAttrsFromUrl } from '@atlaskit/media-client';
24
24
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
25
+ import { hasAIGeneratingDecoration } from '../../pm-plugins/ai-generating-decoration';
25
26
  import { updateCurrentMediaNodeAttrs } from '../../pm-plugins/commands/helpers';
26
27
  import { isMediaBlobUrlFromAttrs } from '../../pm-plugins/utils/media-common';
27
28
  // Ignored via go/ees005
@@ -56,6 +57,7 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
56
57
  }
57
58
  _this = _callSuper(this, MediaNodeView, [].concat(args));
58
59
  _defineProperty(_this, "isSelected", false);
60
+ _defineProperty(_this, "isAIGenerating", false);
59
61
  _defineProperty(_this, "hasBeenResized", false);
60
62
  _defineProperty(_this, "hasResizedListener", function () {
61
63
  if (!_this.hasBeenResized) {
@@ -172,7 +174,8 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
172
174
  mediaOptions: mediaOptions,
173
175
  onExternalImageLoaded: _this.onExternalImageLoaded,
174
176
  isViewOnly: ((_this$reactComponentP = _this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP === void 0 || (_this$reactComponentP = _this$reactComponentP.editorViewMode) === null || _this$reactComponentP === void 0 || (_this$reactComponentP = _this$reactComponentP.sharedState.currentState()) === null || _this$reactComponentP === void 0 ? void 0 : _this$reactComponentP.mode) === 'view',
175
- pluginInjectionApi: _this.reactComponentProps.pluginInjectionApi
177
+ pluginInjectionApi: _this.reactComponentProps.pluginInjectionApi,
178
+ isAIGenerating: _this.isAIGenerating
176
179
  });
177
180
  };
178
181
  });
@@ -262,6 +265,11 @@ var MediaNodeView = /*#__PURE__*/function (_SelectionBasedNodeVi) {
262
265
  this.isSelected = hasMediaNodeSelectedDecoration;
263
266
  return true;
264
267
  }
268
+ var aiGenerating = hasAIGeneratingDecoration(decorations);
269
+ if (this.isAIGenerating !== aiGenerating) {
270
+ this.isAIGenerating = aiGenerating;
271
+ return true;
272
+ }
265
273
  if (this.node.attrs !== nextNode.attrs) {
266
274
  return true;
267
275
  }
@@ -173,7 +173,7 @@ export var MediaNode = /*#__PURE__*/function (_Component) {
173
173
  value: function shouldComponentUpdate(nextProps, nextState) {
174
174
  var hasNewViewMediaClientConfig = !this.state.viewMediaClientConfig && nextState.viewMediaClientConfig;
175
175
  var hasNewViewAndUploadMediaClientConfig = !this.state.viewAndUploadMediaClientConfig && nextState.viewAndUploadMediaClientConfig;
176
- if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
176
+ if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.isAIGenerating !== nextProps.isAIGenerating || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
177
177
  return true;
178
178
  }
179
179
  return false;
@@ -383,6 +383,7 @@ export var MediaNode = /*#__PURE__*/function (_Component) {
383
383
  videoControlsWrapperRef: this.videoControlsWrapperRef,
384
384
  ssr: ssr,
385
385
  mediaSettings: this.getMediaSettings(viewAndUploadMediaClientConfig),
386
+ isAIGenerating: !!this.props.isAIGenerating,
386
387
  onError: expValEquals('platform_editor_media_error_analytics', 'isEnabled', true) ? this.onError : undefined
387
388
  })));
388
389
  }
@@ -0,0 +1,170 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
5
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
+ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
+ import FeatureGates from '@atlaskit/feature-gate-js-client';
8
+
9
+ /**
10
+ * ProseMirror plugin that manages AI-generating decorations on media nodes.
11
+ *
12
+ * Instead of storing transient `__isAIGenerating` attributes in the ADF schema
13
+ * (which pollutes the document model and undo history), this plugin uses
14
+ * ProseMirror decorations — a view-layer-only mechanism that never affects the
15
+ * document content, serialization, or undo/redo stack.
16
+ */
17
+
18
+ // ── Plugin Key ──────────────────────────────────────────────────────────────
19
+
20
+ export var aiGeneratingDecorationPluginKey = new PluginKey('aiGeneratingDecoration');
21
+
22
+ // ── Types ───────────────────────────────────────────────────────────────────
23
+
24
+ var AI_GENERATING_DECORATION_TYPE = 'ai-generating';
25
+
26
+ // ── Helpers ─────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Build a DecorationSet containing a Decoration.node for every media node
30
+ * whose `id` is in the given set.
31
+ */
32
+ function buildDecorationSet(doc, mediaIds) {
33
+ if (mediaIds.size === 0) {
34
+ return DecorationSet.empty;
35
+ }
36
+ var decorations = [];
37
+ doc.descendants(function (node, pos) {
38
+ if (node.type.name === 'media' && mediaIds.has(node.attrs.id)) {
39
+ decorations.push(Decoration.node(pos, pos + node.nodeSize, {},
40
+ // no DOM attrs needed — the NodeView reads the decoration spec
41
+ {
42
+ type: AI_GENERATING_DECORATION_TYPE,
43
+ mediaId: node.attrs.id
44
+ }));
45
+ }
46
+ });
47
+ return DecorationSet.create(doc, decorations);
48
+ }
49
+
50
+ // ── Public utilities ────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Returns `true` if the given decorations array contains an AI-generating
54
+ * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()`
55
+ * to determine whether to render the AI border.
56
+ */
57
+ export function hasAIGeneratingDecoration(decorations) {
58
+ return decorations.some(function (d) {
59
+ return d.spec.type === AI_GENERATING_DECORATION_TYPE;
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Dispatch a transaction that sets the AI-generating decoration on a media
65
+ * node identified by `mediaId`.
66
+ *
67
+ * Usage from the editor bridge:
68
+ * ```
69
+ * editorAPI.core.actions.execute(({ tr }) =>
70
+ * setAIGeneratingMeta(tr, mediaId),
71
+ * );
72
+ * ```
73
+ */
74
+ export function setAIGeneratingMeta(tr, mediaId) {
75
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
76
+ type: 'SET_GENERATING',
77
+ mediaId: mediaId
78
+ }).setMeta('addToHistory', false);
79
+ }
80
+
81
+ /**
82
+ * Dispatch a transaction that clears the AI-generating decoration for a
83
+ * specific media node.
84
+ */
85
+ export function clearAIGeneratingMeta(tr, mediaId) {
86
+ return tr.setMeta(aiGeneratingDecorationPluginKey, {
87
+ type: 'CLEAR_GENERATING',
88
+ mediaId: mediaId
89
+ }).setMeta('addToHistory', false);
90
+ }
91
+
92
+ // ── Plugin ──────────────────────────────────────────────────────────────────
93
+
94
+ /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */
95
+ export function createAIGeneratingDecorationPlugin() {
96
+ return new SafePlugin({
97
+ key: aiGeneratingDecorationPluginKey,
98
+ state: {
99
+ init: function init() {
100
+ return {
101
+ generatingMediaIds: new Set(),
102
+ decorationSet: DecorationSet.empty
103
+ };
104
+ },
105
+ apply: function apply(tr, pluginState, _oldState, newState) {
106
+ // Killswitch — if active, clear any existing decorations and stop
107
+ // eslint-disable-next-line @atlaskit/platform/use-recommended-utils -- dynamic config killswitch, not a standard feature gate
108
+ if (FeatureGates.getExperimentValue('maui_ai_border_killswitch', 'value', false)) {
109
+ if (pluginState.generatingMediaIds.size > 0) {
110
+ return {
111
+ generatingMediaIds: new Set(),
112
+ decorationSet: DecorationSet.empty
113
+ };
114
+ }
115
+ return pluginState;
116
+ }
117
+ var meta = tr.getMeta(aiGeneratingDecorationPluginKey);
118
+ if (meta) {
119
+ switch (meta.type) {
120
+ case 'SET_GENERATING':
121
+ {
122
+ var ids = new Set(pluginState.generatingMediaIds);
123
+ ids.add(meta.mediaId);
124
+ return {
125
+ generatingMediaIds: ids,
126
+ decorationSet: buildDecorationSet(newState.doc, ids)
127
+ };
128
+ }
129
+ case 'CLEAR_GENERATING':
130
+ {
131
+ var _ids = new Set(pluginState.generatingMediaIds);
132
+ _ids.delete(meta.mediaId);
133
+ return {
134
+ generatingMediaIds: _ids,
135
+ decorationSet: buildDecorationSet(newState.doc, _ids)
136
+ };
137
+ }
138
+ case 'CLEAR_ALL':
139
+ return {
140
+ generatingMediaIds: new Set(),
141
+ decorationSet: DecorationSet.empty
142
+ };
143
+ }
144
+ }
145
+
146
+ // Map decorations through document changes so positions stay in sync
147
+ if (tr.docChanged && pluginState.decorationSet !== DecorationSet.empty) {
148
+ try {
149
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
150
+ decorationSet: pluginState.decorationSet.map(tr.mapping, newState.doc)
151
+ });
152
+ } catch (_unused) {
153
+ // Collaborative editing edge case — reset
154
+ return {
155
+ generatingMediaIds: new Set(),
156
+ decorationSet: DecorationSet.empty
157
+ };
158
+ }
159
+ }
160
+ return pluginState;
161
+ }
162
+ },
163
+ props: {
164
+ decorations: function decorations(state) {
165
+ var _aiGeneratingDecorati, _aiGeneratingDecorati2;
166
+ return (_aiGeneratingDecorati = (_aiGeneratingDecorati2 = aiGeneratingDecorationPluginKey.getState(state)) === null || _aiGeneratingDecorati2 === void 0 ? void 0 : _aiGeneratingDecorati2.decorationSet) !== null && _aiGeneratingDecorati !== void 0 ? _aiGeneratingDecorati : DecorationSet.empty;
167
+ }
168
+ }
169
+ });
170
+ }
@@ -1,4 +1,5 @@
1
1
  import { ACTIONS } from '../pm-plugins/actions';
2
+ import { setAIGeneratingMeta, clearAIGeneratingMeta } from '../pm-plugins/ai-generating-decoration';
2
3
  import { stateKey } from '../pm-plugins/plugin-key';
3
4
  import { getIdentifier } from '../pm-plugins/utils/media-common';
4
5
  import { createInsertMediaAsMediaSingleCommand } from './utils/media-single';
@@ -33,6 +34,31 @@ export var trackMediaPaste = function trackMediaPaste(attrs) {
33
34
  return tr;
34
35
  };
35
36
  };
37
+
38
+ /**
39
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
40
+ * The decoration triggers the AI border visual on the media's NodeView.
41
+ *
42
+ * Decorations live in the view layer only and never affect the document model
43
+ * or undo/redo history.
44
+ */
45
+ export var setAIGenerating = function setAIGenerating(mediaId) {
46
+ return function (_ref4) {
47
+ var tr = _ref4.tr;
48
+ return setAIGeneratingMeta(tr, mediaId);
49
+ };
50
+ };
51
+
52
+ /**
53
+ * Clears the AI-generating decoration for a specific media node identified by
54
+ * `mediaId`. Removes the AI border visual from that media's NodeView.
55
+ */
56
+ export var clearAIGenerating = function clearAIGenerating(mediaId) {
57
+ return function (_ref5) {
58
+ var tr = _ref5.tr;
59
+ return clearAIGeneratingMeta(tr, mediaId);
60
+ };
61
+ };
36
62
  export var insertMediaAsMediaSingleCommand = function insertMediaAsMediaSingleCommand(editorAnalyticsAPI, allowPixelResizing) {
37
63
  return function (mediaAttrs, inputMethod, insertMediaVia) {
38
64
  return createInsertMediaAsMediaSingleCommand(mediaAttrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing);
@@ -69,6 +69,11 @@ export type MediaNextEditorPluginType = NextEditorPlugin<'media', {
69
69
  setProvider: (provider: Promise<MediaProvider>) => boolean;
70
70
  };
71
71
  commands: {
72
+ /**
73
+ * Clears the AI-generating decoration for a specific media node identified by
74
+ * `mediaId`. Removes the AI border from that media node.
75
+ */
76
+ clearAIGenerating: (mediaId: string) => EditorCommand;
72
77
  hideMediaViewer: EditorCommand;
73
78
  /**
74
79
  * Inserts a media node as a media single.
@@ -79,6 +84,14 @@ export type MediaNextEditorPluginType = NextEditorPlugin<'media', {
79
84
  * @param insertMediaVia - Optional parameter indicating how the media was inserted
80
85
  */
81
86
  insertMediaSingle: (attrs: MediaADFAttrs, inputMethod: InputMethodInsertMedia, insertMediaVia?: InsertMediaVia) => EditorCommand;
87
+ /**
88
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
89
+ * Renders an AI border around the media node while AI is generating/replacing it.
90
+ *
91
+ * Decorations live in the view layer only and never affect the document model
92
+ * or undo/redo history.
93
+ */
94
+ setAIGenerating: (mediaId: string) => EditorCommand;
82
95
  showMediaViewer: (media: MediaADFAttrs) => EditorCommand;
83
96
  trackMediaPaste: (attrs: MediaADFAttrs) => EditorCommand;
84
97
  };
@@ -17,6 +17,7 @@ interface MediaNodeWithPluginStateComponentProps {
17
17
  }
18
18
  declare class MediaNodeView extends SelectionBasedNodeView<MediaNodeViewProps> {
19
19
  private isSelected;
20
+ private isAIGenerating;
20
21
  private hasBeenResized;
21
22
  private resizeListenerBinding?;
22
23
  getMediaSingleNode(getPos: getPosHandlerNode): PMNode | null;
@@ -15,6 +15,7 @@ export interface MediaNodeProps extends ReactNodeProps, ImageLoaderProps {
15
15
  api?: ExtractInjectionAPI<MediaNextEditorPluginType>;
16
16
  contextIdentifierProvider?: Promise<ContextIdentifierProvider>;
17
17
  getPos: ProsemirrorGetPosHandler;
18
+ isAIGenerating?: boolean;
18
19
  isLoading?: boolean;
19
20
  isMediaSingle?: boolean;
20
21
  isViewOnly?: boolean;
@@ -0,0 +1,47 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
3
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
4
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
+ /**
6
+ * ProseMirror plugin that manages AI-generating decorations on media nodes.
7
+ *
8
+ * Instead of storing transient `__isAIGenerating` attributes in the ADF schema
9
+ * (which pollutes the document model and undo history), this plugin uses
10
+ * ProseMirror decorations — a view-layer-only mechanism that never affects the
11
+ * document content, serialization, or undo/redo stack.
12
+ */
13
+ export declare const aiGeneratingDecorationPluginKey: PluginKey;
14
+ export type AIGeneratingAction = {
15
+ mediaId: string;
16
+ type: 'SET_GENERATING';
17
+ } | {
18
+ mediaId: string;
19
+ type: 'CLEAR_GENERATING';
20
+ } | {
21
+ type: 'CLEAR_ALL';
22
+ };
23
+ /**
24
+ * Returns `true` if the given decorations array contains an AI-generating
25
+ * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()`
26
+ * to determine whether to render the AI border.
27
+ */
28
+ export declare function hasAIGeneratingDecoration(decorations: readonly Decoration[]): boolean;
29
+ /**
30
+ * Dispatch a transaction that sets the AI-generating decoration on a media
31
+ * node identified by `mediaId`.
32
+ *
33
+ * Usage from the editor bridge:
34
+ * ```
35
+ * editorAPI.core.actions.execute(({ tr }) =>
36
+ * setAIGeneratingMeta(tr, mediaId),
37
+ * );
38
+ * ```
39
+ */
40
+ export declare function setAIGeneratingMeta(tr: Transaction, mediaId: string): Transaction;
41
+ /**
42
+ * Dispatch a transaction that clears the AI-generating decoration for a
43
+ * specific media node.
44
+ */
45
+ export declare function clearAIGeneratingMeta(tr: Transaction, mediaId: string): Transaction;
46
+ /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */
47
+ export declare function createAIGeneratingDecorationPlugin(): SafePlugin;
@@ -4,4 +4,17 @@ import type { EditorCommand } from '@atlaskit/editor-common/types';
4
4
  export declare const showMediaViewer: (media: MediaADFAttrs) => EditorCommand;
5
5
  export declare const hideMediaViewer: EditorCommand;
6
6
  export declare const trackMediaPaste: (attrs: MediaADFAttrs) => EditorCommand;
7
+ /**
8
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
9
+ * The decoration triggers the AI border visual on the media's NodeView.
10
+ *
11
+ * Decorations live in the view layer only and never affect the document model
12
+ * or undo/redo history.
13
+ */
14
+ export declare const setAIGenerating: (mediaId: string) => EditorCommand;
15
+ /**
16
+ * Clears the AI-generating decoration for a specific media node identified by
17
+ * `mediaId`. Removes the AI border visual from that media's NodeView.
18
+ */
19
+ export declare const clearAIGenerating: (mediaId: string) => EditorCommand;
7
20
  export declare const insertMediaAsMediaSingleCommand: (editorAnalyticsAPI?: EditorAnalyticsAPI, allowPixelResizing?: boolean) => (mediaAttrs: MediaADFAttrs, inputMethod: InputMethodInsertMedia, insertMediaVia?: InsertMediaVia) => EditorCommand;
@@ -69,6 +69,11 @@ export type MediaNextEditorPluginType = NextEditorPlugin<'media', {
69
69
  setProvider: (provider: Promise<MediaProvider>) => boolean;
70
70
  };
71
71
  commands: {
72
+ /**
73
+ * Clears the AI-generating decoration for a specific media node identified by
74
+ * `mediaId`. Removes the AI border from that media node.
75
+ */
76
+ clearAIGenerating: (mediaId: string) => EditorCommand;
72
77
  hideMediaViewer: EditorCommand;
73
78
  /**
74
79
  * Inserts a media node as a media single.
@@ -79,6 +84,14 @@ export type MediaNextEditorPluginType = NextEditorPlugin<'media', {
79
84
  * @param insertMediaVia - Optional parameter indicating how the media was inserted
80
85
  */
81
86
  insertMediaSingle: (attrs: MediaADFAttrs, inputMethod: InputMethodInsertMedia, insertMediaVia?: InsertMediaVia) => EditorCommand;
87
+ /**
88
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
89
+ * Renders an AI border around the media node while AI is generating/replacing it.
90
+ *
91
+ * Decorations live in the view layer only and never affect the document model
92
+ * or undo/redo history.
93
+ */
94
+ setAIGenerating: (mediaId: string) => EditorCommand;
82
95
  showMediaViewer: (media: MediaADFAttrs) => EditorCommand;
83
96
  trackMediaPaste: (attrs: MediaADFAttrs) => EditorCommand;
84
97
  };
@@ -17,6 +17,7 @@ interface MediaNodeWithPluginStateComponentProps {
17
17
  }
18
18
  declare class MediaNodeView extends SelectionBasedNodeView<MediaNodeViewProps> {
19
19
  private isSelected;
20
+ private isAIGenerating;
20
21
  private hasBeenResized;
21
22
  private resizeListenerBinding?;
22
23
  getMediaSingleNode(getPos: getPosHandlerNode): PMNode | null;
@@ -15,6 +15,7 @@ export interface MediaNodeProps extends ReactNodeProps, ImageLoaderProps {
15
15
  api?: ExtractInjectionAPI<MediaNextEditorPluginType>;
16
16
  contextIdentifierProvider?: Promise<ContextIdentifierProvider>;
17
17
  getPos: ProsemirrorGetPosHandler;
18
+ isAIGenerating?: boolean;
18
19
  isLoading?: boolean;
19
20
  isMediaSingle?: boolean;
20
21
  isViewOnly?: boolean;
@@ -0,0 +1,47 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
3
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
4
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
+ /**
6
+ * ProseMirror plugin that manages AI-generating decorations on media nodes.
7
+ *
8
+ * Instead of storing transient `__isAIGenerating` attributes in the ADF schema
9
+ * (which pollutes the document model and undo history), this plugin uses
10
+ * ProseMirror decorations — a view-layer-only mechanism that never affects the
11
+ * document content, serialization, or undo/redo stack.
12
+ */
13
+ export declare const aiGeneratingDecorationPluginKey: PluginKey;
14
+ export type AIGeneratingAction = {
15
+ mediaId: string;
16
+ type: 'SET_GENERATING';
17
+ } | {
18
+ mediaId: string;
19
+ type: 'CLEAR_GENERATING';
20
+ } | {
21
+ type: 'CLEAR_ALL';
22
+ };
23
+ /**
24
+ * Returns `true` if the given decorations array contains an AI-generating
25
+ * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()`
26
+ * to determine whether to render the AI border.
27
+ */
28
+ export declare function hasAIGeneratingDecoration(decorations: readonly Decoration[]): boolean;
29
+ /**
30
+ * Dispatch a transaction that sets the AI-generating decoration on a media
31
+ * node identified by `mediaId`.
32
+ *
33
+ * Usage from the editor bridge:
34
+ * ```
35
+ * editorAPI.core.actions.execute(({ tr }) =>
36
+ * setAIGeneratingMeta(tr, mediaId),
37
+ * );
38
+ * ```
39
+ */
40
+ export declare function setAIGeneratingMeta(tr: Transaction, mediaId: string): Transaction;
41
+ /**
42
+ * Dispatch a transaction that clears the AI-generating decoration for a
43
+ * specific media node.
44
+ */
45
+ export declare function clearAIGeneratingMeta(tr: Transaction, mediaId: string): Transaction;
46
+ /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */
47
+ export declare function createAIGeneratingDecorationPlugin(): SafePlugin;
@@ -4,4 +4,17 @@ import type { EditorCommand } from '@atlaskit/editor-common/types';
4
4
  export declare const showMediaViewer: (media: MediaADFAttrs) => EditorCommand;
5
5
  export declare const hideMediaViewer: EditorCommand;
6
6
  export declare const trackMediaPaste: (attrs: MediaADFAttrs) => EditorCommand;
7
+ /**
8
+ * Sets the AI-generating decoration on a media node identified by `mediaId`.
9
+ * The decoration triggers the AI border visual on the media's NodeView.
10
+ *
11
+ * Decorations live in the view layer only and never affect the document model
12
+ * or undo/redo history.
13
+ */
14
+ export declare const setAIGenerating: (mediaId: string) => EditorCommand;
15
+ /**
16
+ * Clears the AI-generating decoration for a specific media node identified by
17
+ * `mediaId`. Removes the AI border visual from that media's NodeView.
18
+ */
19
+ export declare const clearAIGenerating: (mediaId: string) => EditorCommand;
7
20
  export declare const insertMediaAsMediaSingleCommand: (editorAnalyticsAPI?: EditorAnalyticsAPI, allowPixelResizing?: boolean) => (mediaAttrs: MediaADFAttrs, inputMethod: InputMethodInsertMedia, insertMediaVia?: InsertMediaVia) => EditorCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-media",
3
- "version": "12.1.5",
3
+ "version": "12.2.0",
4
4
  "description": "Media plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "atlaskit:src": "src/index.ts",
31
31
  "dependencies": {
32
- "@atlaskit/adf-schema": "^52.5.0",
32
+ "@atlaskit/adf-schema": "^52.6.0",
33
33
  "@atlaskit/analytics-namespaced-context": "^7.2.0",
34
34
  "@atlaskit/analytics-next": "^11.2.0",
35
35
  "@atlaskit/button": "^23.11.0",
@@ -51,10 +51,11 @@
51
51
  "@atlaskit/editor-prosemirror": "^7.3.0",
52
52
  "@atlaskit/editor-shared-styles": "^3.10.0",
53
53
  "@atlaskit/editor-tables": "^2.9.0",
54
+ "@atlaskit/feature-gate-js-client": "5.5.11",
54
55
  "@atlaskit/form": "^15.5.0",
55
56
  "@atlaskit/icon": "^34.3.0",
56
57
  "@atlaskit/icon-lab": "^6.6.0",
57
- "@atlaskit/media-card": "^80.1.0",
58
+ "@atlaskit/media-card": "^80.2.0",
58
59
  "@atlaskit/media-client": "^36.0.0",
59
60
  "@atlaskit/media-client-react": "^5.0.0",
60
61
  "@atlaskit/media-common": "^13.0.0",
@@ -65,7 +66,7 @@
65
66
  "@atlaskit/platform-feature-flags": "^1.1.0",
66
67
  "@atlaskit/primitives": "^19.0.0",
67
68
  "@atlaskit/textfield": "^8.3.0",
68
- "@atlaskit/tmp-editor-statsig": "^67.0.0",
69
+ "@atlaskit/tmp-editor-statsig": "^68.0.0",
69
70
  "@atlaskit/tokens": "^13.0.0",
70
71
  "@atlaskit/tooltip": "^21.2.0",
71
72
  "@babel/runtime": "^7.0.0",
@@ -77,7 +78,7 @@
77
78
  "uuid": "^3.1.0"
78
79
  },
79
80
  "peerDependencies": {
80
- "@atlaskit/editor-common": "^114.8.0",
81
+ "@atlaskit/editor-common": "^114.9.0",
81
82
  "@atlaskit/media-core": "^37.0.0",
82
83
  "react": "^18.2.0",
83
84
  "react-dom": "^18.2.0",