@atlaskit/editor-plugin-show-diff 16.0.12 → 16.0.14

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +30 -4
  3. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +52 -6
  4. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  5. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +10 -8
  6. package/dist/cjs/pm-plugins/utils/isEmptyParagraphSlice.js +11 -0
  7. package/dist/cjs/ui/IndicatorBar/IndicatorBar.js +1 -1
  8. package/dist/cjs/ui/IndicatorBar/IndicatorBarContentComponent.js +1 -1
  9. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +28 -4
  10. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +52 -7
  11. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  12. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +10 -8
  13. package/dist/es2019/pm-plugins/utils/isEmptyParagraphSlice.js +5 -0
  14. package/dist/es2019/ui/IndicatorBar/IndicatorBar.js +1 -1
  15. package/dist/es2019/ui/IndicatorBar/IndicatorBarContentComponent.js +1 -1
  16. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +30 -4
  17. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +53 -7
  18. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  19. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +10 -8
  20. package/dist/esm/pm-plugins/utils/isEmptyParagraphSlice.js +5 -0
  21. package/dist/esm/ui/IndicatorBar/IndicatorBar.js +1 -1
  22. package/dist/esm/ui/IndicatorBar/IndicatorBarContentComponent.js +1 -1
  23. package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +2 -1
  24. package/dist/types/pm-plugins/utils/isEmptyParagraphSlice.d.ts +3 -0
  25. package/package.json +2 -2
  26. package/dist/cjs/pm-plugins/utils/hasVisibleContent.js +0 -37
  27. package/dist/es2019/pm-plugins/utils/hasVisibleContent.js +0 -28
  28. package/dist/esm/pm-plugins/utils/hasVisibleContent.js +0 -32
  29. package/dist/types/pm-plugins/utils/hasVisibleContent.d.ts +0 -13
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 16.0.14
4
+
5
+ ### Patch Changes
6
+
7
+ - [`6667b6ae8e77c`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6667b6ae8e77c) -
8
+ Ensure contributor tags are rendered for shown attributed diff content, including whitespace-only
9
+ changes and deletion widgets, without tagging structurally empty paragraphs.
10
+
11
+ ## 16.0.13
12
+
13
+ ### Patch Changes
14
+
15
+ - [`7d1a41f1575e5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/7d1a41f1575e5) -
16
+ Reserve red for deletions in show-diff attribution: an actor hashing to a red palette slot now
17
+ takes the next colour instead. Gated on confluence_ncs_step_diffing_version_history and
18
+ platform_editor_show_diff_color_scheme_refactor.
19
+ - [`19e336d422584`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/19e336d422584) -
20
+ Fix whole-row table replacements rendering no diff for the new content.
21
+ `createChangedRowDecorationWidgets` only handled row deletions, so a `replaceNode` on a `tableRow`
22
+ (as AI suggested edits emits) left the reviewer with just the removed side — or an empty row in
23
+ the clean view.
24
+
25
+ Also give the changed-row widget an indicator anchor, so the indicator bar spans both the proposed
26
+ and replaced rows instead of only the row being changed. This affects row deletions too, whose
27
+ deleted-row bar previously had no anchor to resolve against.
28
+
29
+ Both behind `platform_editor_ai_show_diff_patch_1`.
30
+
31
+ - Updated dependencies
32
+
3
33
  ## 16.0.12
4
34
 
5
35
  ### Patch Changes
@@ -25,15 +25,40 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
25
25
 
26
26
  /** Maps the shared 18-slot telepointer palette onto the ten bolder diff colours. */
27
27
  var HASHED_PARTICIPANT_COLOR_SCHEMES = [].concat((0, _toConsumableArray2.default)(_types.PARTICIPANT_COLOR_SCHEMES), ['blue', 'red', 'orange', 'yellow', 'green', 'teal', 'purple', 'magenta']);
28
+
29
+ /**
30
+ * Red means "deleted" in both public schemes, so it never identifies an actor. Reserving it here
31
+ * covers every path into an attribution colour; `createAttributionColorMap` is the only caller, and
32
+ * it is gated on `confluence_ncs_step_diffing_version_history` plus
33
+ * `platform_editor_show_diff_color_scheme_refactor`.
34
+ */
35
+ var RESERVED_ATTRIBUTION_COLORS = new Set(['red']);
36
+ var isAssignable = function isAssignable(color) {
37
+ return !RESERVED_ATTRIBUTION_COLORS.has(color);
38
+ };
39
+
40
+ /** Projects a telepointer slot onto its diff hue, advancing past a hue reserved for deletion. */
41
+ var getSlotColor = function getSlotColor(index) {
42
+ for (var offset = 0; offset < HASHED_PARTICIPANT_COLOR_SCHEMES.length; offset++) {
43
+ var color = HASHED_PARTICIPANT_COLOR_SCHEMES[(index + offset) % HASHED_PARTICIPANT_COLOR_SCHEMES.length];
44
+ if (color && isAssignable(color)) {
45
+ return color;
46
+ }
47
+ }
48
+ return undefined;
49
+ };
28
50
  var getAvailableColor = function getAvailableColor(hashedColor, allocatedColors) {
51
+ var _PARTICIPANT_COLOR_SC;
29
52
  var startIndex = _types.PARTICIPANT_COLOR_SCHEMES.indexOf(hashedColor);
30
53
  for (var offset = 0; offset < _types.PARTICIPANT_COLOR_SCHEMES.length; offset++) {
31
54
  var color = _types.PARTICIPANT_COLOR_SCHEMES[(startIndex + offset) % _types.PARTICIPANT_COLOR_SCHEMES.length];
32
- if (color && !allocatedColors.has(color)) {
55
+ if (color && isAssignable(color) && !allocatedColors.has(color)) {
33
56
  return color;
34
57
  }
35
58
  }
36
- return hashedColor;
59
+
60
+ // Every assignable hue is taken, so a duplicate is unavoidable — but never the reserved hue.
61
+ return isAssignable(hashedColor) ? hashedColor : (_PARTICIPANT_COLOR_SC = _types.PARTICIPANT_COLOR_SCHEMES.find(isAssignable)) !== null && _PARTICIPANT_COLOR_SC !== void 0 ? _PARTICIPANT_COLOR_SC : hashedColor;
37
62
  };
38
63
 
39
64
  /**
@@ -133,7 +158,8 @@ var createAttributionColorMap = exports.createAttributionColorMap = function cre
133
158
  var colors = new Map();
134
159
  var allocatedColors = new Set();
135
160
 
136
- // Reserve shared brand colours before hashing other actors, regardless of step order.
161
+ // Reserve shared brand colours before hashing other actors, regardless of step order. A brand
162
+ // slot is pinned deliberately, so it is taken verbatim rather than through `getSlotColor`.
137
163
  var _iterator2 = _createForOfIteratorHelper(stepAttributions),
138
164
  _step2;
139
165
  try {
@@ -168,7 +194,7 @@ var createAttributionColorMap = exports.createAttributionColorMap = function cre
168
194
  }
169
195
  var _getParticipantColor2 = (0, _utils.getParticipantColor)(_identity.colorSeed, _attribution === null || _attribution === void 0 ? void 0 : _attribution.agentType),
170
196
  _index = _getParticipantColor2.index;
171
- var hashedColor = HASHED_PARTICIPANT_COLOR_SCHEMES[_index];
197
+ var hashedColor = getSlotColor(_index);
172
198
  if (!hashedColor) {
173
199
  continue;
174
200
  }
@@ -12,10 +12,12 @@ var _utils = require("@atlaskit/editor-prosemirror/utils");
12
12
  var _view = require("@atlaskit/editor-prosemirror/view");
13
13
  var _tableMap = require("@atlaskit/editor-tables/table-map");
14
14
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
15
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
15
16
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
16
17
  var _isExtendedEnabled = require("../isExtendedEnabled");
17
18
  var _factory = require("./colorSchemes/factory");
18
19
  var _schemes = require("./colorSchemes/schemes");
20
+ var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
19
21
  var _createChangedRowDecorationWidgetsStyles = require("./createChangedRowDecorationWidgets.styles.legacy");
20
22
  var _decorationKeys = require("./decorationKeys");
21
23
  var _findSafeInsertPos = require("./utils/findSafeInsertPos");
@@ -54,8 +56,14 @@ var extractChangedRows = function extractChangedRows(_ref) {
54
56
  return changedRows;
55
57
  }
56
58
  var newTableMap = _tableMap.TableMap.get(tableNew.node);
59
+ // A row DELETION makes the table shorter. An in-place whole-row REPLACEMENT (`replaceNode` on a
60
+ // `tableRow`, which AI suggested edits emits) leaves the row count untouched — and this helper
61
+ // was written for deletions only, so it used to discard every replacement and the changed row's
62
+ // content was never rendered anywhere. The reviewer was left with just the removed side.
63
+ var isRowDeletion = oldTableMap.height > newTableMap.height;
64
+ var handleRowReplacement = oldTableMap.height === newTableMap.height && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1');
57
65
  // If no rows were changed, return empty
58
- if (oldTableMap.height <= newTableMap.height ||
66
+ if (!(isRowDeletion || handleRowReplacement) ||
59
67
  // For now ignore if there are column deletions as well
60
68
  oldTableMap.width !== newTableMap.width) {
61
69
  return changedRows;
@@ -99,6 +107,15 @@ var extractChangedRows = function extractChangedRows(_ref) {
99
107
 
100
108
  // Filter changes that never truly got deleted
101
109
  return changedRows.filter(function (changedRow) {
110
+ // A replacement leaves every sibling row in place, so asking "does this row still exist
111
+ // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a
112
+ // sibling. Compare it against the row at the same index instead. Deletions must keep the
113
+ // whole-table `some()`: their indices shift, so an index comparison would let every
114
+ // surviving row through and draw a widget for each one.
115
+ if (handleRowReplacement) {
116
+ var counterpart = tableNew.node.maybeChild(changedRow.rowIndex);
117
+ return !counterpart || !(0, _document.areNodesEqualIgnoreAttrs)(counterpart, changedRow.rowNode);
118
+ }
102
119
  return !tableNew.node.children.some(function (newRow) {
103
120
  return (0, _document.areNodesEqualIgnoreAttrs)(newRow, changedRow.rowNode);
104
121
  });
@@ -142,7 +159,10 @@ var createChangedRowDOM = function createChangedRowDOM(rowNode, cellEdgeAttrs, n
142
159
  if (!(0, _isExtendedEnabled.isExtendedEnabled)(diffType) || !isInserted) {
143
160
  tr.setAttribute('style', (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_color_scheme_refactor') ? (0, _factory.buildDeletedRowStyle)(colors) : (0, _createChangedRowDecorationWidgetsStyles.resolveDeletedRowStyleLegacy)((0, _schemes.getLegacyColorScheme)(colorScheme)));
144
161
  }
145
- tr.setAttribute('data-testid', 'show-diff-deleted-row');
162
+ // Mirrors the strikethrough condition above: under the extended experience an `isInserted` row
163
+ // is ADDED content, so it must not claim the "deleted" testid — page models match that as
164
+ // removed content (same reasoning as `createTableCellContentWidgets`).
165
+ tr.setAttribute('data-testid', (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && isInserted && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1') ? 'show-diff-changed-row' : 'show-diff-deleted-row');
146
166
 
147
167
  // Serialize each cell in the row
148
168
  var cellIndex = 0;
@@ -222,7 +242,9 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
222
242
  colorScheme = _ref3.colorScheme,
223
243
  _ref3$isInserted = _ref3.isInserted,
224
244
  isInserted = _ref3$isInserted === void 0 ? false : _ref3$isInserted,
225
- diffType = _ref3.diffType;
245
+ diffType = _ref3.diffType,
246
+ _ref3$showIndicators = _ref3.showIndicators,
247
+ showIndicators = _ref3$showIndicators === void 0 ? false : _ref3$showIndicators;
226
248
  // First, expand the changes to include complete deleted rows
227
249
  var changedRows = expandDiffForChangedRows({
228
250
  changes: changes.filter(function (change) {
@@ -232,7 +254,7 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
232
254
  newDoc: newDoc,
233
255
  diffType: diffType
234
256
  });
235
- return changedRows.map(function (changedRow) {
257
+ return changedRows.flatMap(function (changedRow) {
236
258
  var rowDOM = createChangedRowDOM(changedRow.rowNode, changedRow.cellEdgeAttrs, nodeViewSerializer, colorScheme, isInserted, diffType);
237
259
 
238
260
  // Find safe insertion position for the deleted row
@@ -240,13 +262,37 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
240
262
  // -1 to find the first safe position from the table
241
263
  originalDoc.slice(changedRow.fromA, changedRow.toA));
242
264
  var diffId = crypto.randomUUID();
243
- return _view.Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({
265
+ var decorations = [];
266
+
267
+ // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its
268
+ // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar
269
+ // is still created but can never resolve a position, so it silently does not render and the
270
+ // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same
271
+ // property on its own widget DOM for non-table content; this path returns before reaching it.
272
+ if (showIndicators && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) {
273
+ rowDOM.style.setProperty('anchor-name', "--".concat((0, _decorationKeys.buildAnchorDecorationKey)({
274
+ diffId: diffId
275
+ })));
276
+
277
+ // A table's content can extend past the doc margin, so the bar also needs a left anchor
278
+ // measured against the table itself or it is clipped once the table is resized.
279
+ var leftAnchor = (0, _createAnchorDecorationWidgets.createLeftAnchorWidget)({
280
+ doc: newDoc,
281
+ from: safeInsertPos,
282
+ diffId: diffId
283
+ });
284
+ if (leftAnchor) {
285
+ decorations.push(leftAnchor);
286
+ }
287
+ }
288
+ decorations.push(_view.Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({
244
289
  colorScheme: colorScheme,
245
290
  decorationType: 'widget',
246
291
  diffId: diffId,
247
292
  isActive: false,
248
293
  isInserted: isInserted,
249
294
  diffType: diffType
250
- })));
295
+ }))));
296
+ return decorations;
251
297
  });
252
298
  };
@@ -12,7 +12,7 @@ var _view = require("@atlaskit/editor-prosemirror/view");
12
12
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
13
13
  var _buildContributorTagDom = require("../../ui/ContributorTag/buildContributorTagDom");
14
14
  var _isExtendedEnabled = require("../isExtendedEnabled");
15
- var _hasVisibleContent = require("../utils/hasVisibleContent");
15
+ var _isEmptyParagraphSlice = require("../utils/isEmptyParagraphSlice");
16
16
  var _factory = require("./colorSchemes/factory");
17
17
  var _schemes = require("./colorSchemes/schemes");
18
18
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
@@ -163,11 +163,9 @@ var createInlineChangedDecoration = exports.createInlineChangedDecoration = func
163
163
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
164
164
  var diffId = showContributorTags ? "inline-".concat(change.fromB, "-").concat(change.toB) : crypto.randomUUID();
165
165
 
166
- // Whether this highlight hosts a tag. A range of nothing but whitespace has nothing to caption,
167
- // so it stays unattributed: the spec is the only route attribution takes to
168
- // `extractContributorTags`, so dropping the key there is what keeps a tag from being resolved for
169
- // a host that is never emitted (EDITOR-8855).
170
- var canTagChange = showContributorTags && !!doc && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() && (0, _hasVisibleContent.hasVisibleContent)(doc.slice(change.fromB, change.toB).content);
166
+ // Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
167
+ // content with no tag host.
168
+ var canTagChange = showContributorTags && !!doc && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() && !(0, _isEmptyParagraphSlice.isEmptyParagraphSlice)(doc.slice(change.fromB, change.toB));
171
169
  if (shouldHideDeleted) {
172
170
  return [_view.Decoration.inline(change.fromB, change.toB, {
173
171
  style: displayNoneStyle
@@ -9,7 +9,7 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
9
9
  var _view = require("@atlaskit/editor-prosemirror/view");
10
10
  var _fg = require("@atlaskit/platform-feature-flags/fg");
11
11
  var _isExtendedEnabled = require("../isExtendedEnabled");
12
- var _hasVisibleContent = require("../utils/hasVisibleContent");
12
+ var _isEmptyParagraphSlice = require("../utils/isEmptyParagraphSlice");
13
13
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
14
14
  var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets");
15
15
  var _createContributorTagWidget = require("./createContributorTagWidget");
@@ -101,7 +101,7 @@ var createTableCellContentWidgets = function createTableCellContentWidgets(_ref)
101
101
  * that is not in the current document.
102
102
  */
103
103
  var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) {
104
- var _slice$content, _slice$content2, _slice$content3, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
104
+ var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
105
105
  var attributionKey = _ref2.attributionKey,
106
106
  change = _ref2.change,
107
107
  doc = _ref2.doc,
@@ -124,7 +124,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
124
124
  reveal = _ref2.reveal,
125
125
  tagMountContext = _ref2.tagMountContext;
126
126
  var slice = doc.slice(change.fromA, change.toA);
127
- var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 || (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 || (_slice$content2 = slice.content) === null || _slice$content2 === void 0 || (_slice$content2 = _slice$content2.firstChild) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.type.name) === 'paragraph' && (slice === null || slice === void 0 || (_slice$content3 = slice.content) === null || _slice$content3 === void 0 || (_slice$content3 = _slice$content3.firstChild) === null || _slice$content3 === void 0 ? void 0 : _slice$content3.content.size) === 0;
127
+ var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (0, _isEmptyParagraphSlice.isEmptyParagraphSlice)(slice);
128
128
  // Widget decoration used for deletions as the content is not in the document
129
129
  // and we want to display the deleted content with a style.
130
130
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -172,7 +172,10 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
172
172
  nodeViewSerializer: nodeViewSerializer,
173
173
  colorScheme: colorScheme,
174
174
  isInserted: isInserted,
175
- diffType: diffType
175
+ diffType: diffType,
176
+ // Needed for the row's own indicator anchor; this path returns before the
177
+ // `anchor-name` assignment further down.
178
+ showIndicators: showIndicators
176
179
  });
177
180
  }
178
181
  var serializer = nodeViewSerializer;
@@ -223,10 +226,9 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
223
226
  // Derived from the deleted range so it survives a recalculation — see the same reasoning in
224
227
  // `createInlineChangedDecoration`.
225
228
  var diffId = showContributorTags ? "widget-".concat(change.fromA, "-").concat(change.toA) : crypto.randomUUID();
226
- // Whether this widget hosts a tag. Removing nothing but whitespace leaves no visible deleted
227
- // content to caption, so the change stays unattributed — matching the inline half's rule in
228
- // `createInlineChangedDecoration` (EDITOR-8855).
229
- var canTagWidget = showContributorTags && (0, _hasVisibleContent.hasVisibleContent)(slice.content);
229
+ // Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
230
+ // text in a rendered widget remains eligible.
231
+ var canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
230
232
  var decorations = [];
231
233
  var replacementNode = newDoc.nodeAt(change.fromB);
232
234
  var firstReplacedNode = slice.content.firstChild;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isEmptyParagraphSlice = void 0;
7
+ /** Whether a slice contains exactly one structurally empty paragraph. */
8
+ var isEmptyParagraphSlice = exports.isEmptyParagraphSlice = function isEmptyParagraphSlice(slice) {
9
+ var _slice$content$firstC;
10
+ return (slice === null || slice === void 0 ? void 0 : slice.content.childCount) === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'paragraph' && slice.content.firstChild.content.size === 0;
11
+ };
@@ -1,4 +1,4 @@
1
- /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  "use strict";
3
3
 
4
4
  var _typeof = require("@babel/runtime/helpers/typeof");
@@ -1,4 +1,4 @@
1
- /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  "use strict";
3
3
 
4
4
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
@@ -9,15 +9,38 @@ import { PARTICIPANT_COLOR_SCHEMES } from './types';
9
9
 
10
10
  /** Maps the shared 18-slot telepointer palette onto the ten bolder diff colours. */
11
11
  const HASHED_PARTICIPANT_COLOR_SCHEMES = [...PARTICIPANT_COLOR_SCHEMES, 'blue', 'red', 'orange', 'yellow', 'green', 'teal', 'purple', 'magenta'];
12
+
13
+ /**
14
+ * Red means "deleted" in both public schemes, so it never identifies an actor. Reserving it here
15
+ * covers every path into an attribution colour; `createAttributionColorMap` is the only caller, and
16
+ * it is gated on `confluence_ncs_step_diffing_version_history` plus
17
+ * `platform_editor_show_diff_color_scheme_refactor`.
18
+ */
19
+ const RESERVED_ATTRIBUTION_COLORS = new Set(['red']);
20
+ const isAssignable = color => !RESERVED_ATTRIBUTION_COLORS.has(color);
21
+
22
+ /** Projects a telepointer slot onto its diff hue, advancing past a hue reserved for deletion. */
23
+ const getSlotColor = index => {
24
+ for (let offset = 0; offset < HASHED_PARTICIPANT_COLOR_SCHEMES.length; offset++) {
25
+ const color = HASHED_PARTICIPANT_COLOR_SCHEMES[(index + offset) % HASHED_PARTICIPANT_COLOR_SCHEMES.length];
26
+ if (color && isAssignable(color)) {
27
+ return color;
28
+ }
29
+ }
30
+ return undefined;
31
+ };
12
32
  const getAvailableColor = (hashedColor, allocatedColors) => {
33
+ var _PARTICIPANT_COLOR_SC;
13
34
  const startIndex = PARTICIPANT_COLOR_SCHEMES.indexOf(hashedColor);
14
35
  for (let offset = 0; offset < PARTICIPANT_COLOR_SCHEMES.length; offset++) {
15
36
  const color = PARTICIPANT_COLOR_SCHEMES[(startIndex + offset) % PARTICIPANT_COLOR_SCHEMES.length];
16
- if (color && !allocatedColors.has(color)) {
37
+ if (color && isAssignable(color) && !allocatedColors.has(color)) {
17
38
  return color;
18
39
  }
19
40
  }
20
- return hashedColor;
41
+
42
+ // Every assignable hue is taken, so a duplicate is unavoidable — but never the reserved hue.
43
+ return isAssignable(hashedColor) ? hashedColor : (_PARTICIPANT_COLOR_SC = PARTICIPANT_COLOR_SCHEMES.find(isAssignable)) !== null && _PARTICIPANT_COLOR_SC !== void 0 ? _PARTICIPANT_COLOR_SC : hashedColor;
21
44
  };
22
45
 
23
46
  /**
@@ -104,7 +127,8 @@ export const createAttributionColorMap = stepAttributions => {
104
127
  const colors = new Map();
105
128
  const allocatedColors = new Set();
106
129
 
107
- // Reserve shared brand colours before hashing other actors, regardless of step order.
130
+ // Reserve shared brand colours before hashing other actors, regardless of step order. A brand
131
+ // slot is pinned deliberately, so it is taken verbatim rather than through `getSlotColor`.
108
132
  for (const attribution of stepAttributions) {
109
133
  const identity = getAttributionIdentity(attribution);
110
134
  if (!identity) {
@@ -128,7 +152,7 @@ export const createAttributionColorMap = stepAttributions => {
128
152
  const {
129
153
  index
130
154
  } = getParticipantColor(identity.colorSeed, attribution === null || attribution === void 0 ? void 0 : attribution.agentType);
131
- const hashedColor = HASHED_PARTICIPANT_COLOR_SCHEMES[index];
155
+ const hashedColor = getSlotColor(index);
132
156
  if (!hashedColor) {
133
157
  continue;
134
158
  }
@@ -3,12 +3,14 @@ import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
3
3
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
4
4
  import { TableMap } from '@atlaskit/editor-tables/table-map';
5
5
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
6
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
6
7
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
7
8
  import { isExtendedEnabled } from '../isExtendedEnabled';
8
9
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildDeletedRowStyle } from './colorSchemes/factory';
9
10
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
11
+ import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
10
12
  import { resolveCellOverlayStyleLegacy, resolveDeletedRowStyleLegacy } from './createChangedRowDecorationWidgets.styles.legacy';
11
- import { buildDiffDecorationSpec } from './decorationKeys';
13
+ import { buildAnchorDecorationKey, buildDiffDecorationSpec } from './decorationKeys';
12
14
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
13
15
  import { applyCellEdgeAttrs, getRowCellEdgeAttrs, getTableTopAndBottomCellEdgeAttrs } from './utils/tableCellEdgeAttrs';
14
16
  /**
@@ -37,8 +39,14 @@ const extractChangedRows = ({
37
39
  return changedRows;
38
40
  }
39
41
  const newTableMap = TableMap.get(tableNew.node);
42
+ // A row DELETION makes the table shorter. An in-place whole-row REPLACEMENT (`replaceNode` on a
43
+ // `tableRow`, which AI suggested edits emits) leaves the row count untouched — and this helper
44
+ // was written for deletions only, so it used to discard every replacement and the changed row's
45
+ // content was never rendered anywhere. The reviewer was left with just the removed side.
46
+ const isRowDeletion = oldTableMap.height > newTableMap.height;
47
+ const handleRowReplacement = oldTableMap.height === newTableMap.height && fg('platform_editor_ai_show_diff_patch_1');
40
48
  // If no rows were changed, return empty
41
- if (oldTableMap.height <= newTableMap.height ||
49
+ if (!(isRowDeletion || handleRowReplacement) ||
42
50
  // For now ignore if there are column deletions as well
43
51
  oldTableMap.width !== newTableMap.width) {
44
52
  return changedRows;
@@ -80,6 +88,15 @@ const extractChangedRows = ({
80
88
 
81
89
  // Filter changes that never truly got deleted
82
90
  return changedRows.filter(changedRow => {
91
+ // A replacement leaves every sibling row in place, so asking "does this row still exist
92
+ // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a
93
+ // sibling. Compare it against the row at the same index instead. Deletions must keep the
94
+ // whole-table `some()`: their indices shift, so an index comparison would let every
95
+ // surviving row through and draw a widget for each one.
96
+ if (handleRowReplacement) {
97
+ const counterpart = tableNew.node.maybeChild(changedRow.rowIndex);
98
+ return !counterpart || !areNodesEqualIgnoreAttrs(counterpart, changedRow.rowNode);
99
+ }
83
100
  return !tableNew.node.children.some(newRow => areNodesEqualIgnoreAttrs(newRow, changedRow.rowNode));
84
101
  });
85
102
  };
@@ -121,7 +138,10 @@ const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorSc
121
138
  if (!isExtendedEnabled(diffType) || !isInserted) {
122
139
  tr.setAttribute('style', isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme)));
123
140
  }
124
- tr.setAttribute('data-testid', 'show-diff-deleted-row');
141
+ // Mirrors the strikethrough condition above: under the extended experience an `isInserted` row
142
+ // is ADDED content, so it must not claim the "deleted" testid — page models match that as
143
+ // removed content (same reasoning as `createTableCellContentWidgets`).
144
+ tr.setAttribute('data-testid', isExtendedEnabled(diffType) && isInserted && fg('platform_editor_ai_show_diff_patch_1') ? 'show-diff-changed-row' : 'show-diff-deleted-row');
125
145
 
126
146
  // Serialize each cell in the row
127
147
  let cellIndex = 0;
@@ -192,7 +212,8 @@ export const createChangedRowDecorationWidgets = ({
192
212
  nodeViewSerializer,
193
213
  colorScheme,
194
214
  isInserted = false,
195
- diffType
215
+ diffType,
216
+ showIndicators = false
196
217
  }) => {
197
218
  // First, expand the changes to include complete deleted rows
198
219
  const changedRows = expandDiffForChangedRows({
@@ -201,7 +222,7 @@ export const createChangedRowDecorationWidgets = ({
201
222
  newDoc,
202
223
  diffType
203
224
  });
204
- return changedRows.map(changedRow => {
225
+ return changedRows.flatMap(changedRow => {
205
226
  const rowDOM = createChangedRowDOM(changedRow.rowNode, changedRow.cellEdgeAttrs, nodeViewSerializer, colorScheme, isInserted, diffType);
206
227
 
207
228
  // Find safe insertion position for the deleted row
@@ -209,7 +230,30 @@ export const createChangedRowDecorationWidgets = ({
209
230
  // -1 to find the first safe position from the table
210
231
  originalDoc.slice(changedRow.fromA, changedRow.toA));
211
232
  const diffId = crypto.randomUUID();
212
- return Decoration.widget(safeInsertPos, rowDOM, {
233
+ const decorations = [];
234
+
235
+ // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its
236
+ // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar
237
+ // is still created but can never resolve a position, so it silently does not render and the
238
+ // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same
239
+ // property on its own widget DOM for non-table content; this path returns before reaching it.
240
+ if (showIndicators && isExtendedEnabled(diffType) && fg('platform_editor_ai_show_diff_patch_1')) {
241
+ rowDOM.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
242
+ diffId
243
+ })}`);
244
+
245
+ // A table's content can extend past the doc margin, so the bar also needs a left anchor
246
+ // measured against the table itself or it is clipped once the table is resized.
247
+ const leftAnchor = createLeftAnchorWidget({
248
+ doc: newDoc,
249
+ from: safeInsertPos,
250
+ diffId
251
+ });
252
+ if (leftAnchor) {
253
+ decorations.push(leftAnchor);
254
+ }
255
+ }
256
+ decorations.push(Decoration.widget(safeInsertPos, rowDOM, {
213
257
  ...buildDiffDecorationSpec({
214
258
  colorScheme,
215
259
  decorationType: 'widget',
@@ -218,6 +262,7 @@ export const createChangedRowDecorationWidgets = ({
218
262
  isInserted,
219
263
  diffType
220
264
  })
221
- });
265
+ }));
266
+ return decorations;
222
267
  });
223
268
  };
@@ -3,7 +3,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
3
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
4
  import { CONTRIBUTOR_TAG_Z_INDEX } from '../../ui/ContributorTag/buildContributorTagDom';
5
5
  import { isExtendedEnabled } from '../isExtendedEnabled';
6
- import { hasVisibleContent } from '../utils/hasVisibleContent';
6
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
7
7
  import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
8
8
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
9
9
  import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
@@ -138,11 +138,9 @@ export const createInlineChangedDecoration = ({
138
138
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
139
139
  const diffId = showContributorTags ? `inline-${change.fromB}-${change.toB}` : crypto.randomUUID();
140
140
 
141
- // Whether this highlight hosts a tag. A range of nothing but whitespace has nothing to caption,
142
- // so it stays unattributed: the spec is the only route attribution takes to
143
- // `extractContributorTags`, so dropping the key there is what keeps a tag from being resolved for
144
- // a host that is never emitted (EDITOR-8855).
145
- const canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && hasVisibleContent(doc.slice(change.fromB, change.toB).content);
141
+ // Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
142
+ // content with no tag host.
143
+ const canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && !isEmptyParagraphSlice(doc.slice(change.fromB, change.toB));
146
144
  if (shouldHideDeleted) {
147
145
  return [Decoration.inline(change.fromB, change.toB, {
148
146
  style: displayNoneStyle
@@ -1,7 +1,7 @@
1
1
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { isExtendedEnabled } from '../isExtendedEnabled';
4
- import { hasVisibleContent } from '../utils/hasVisibleContent';
4
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
5
5
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
6
6
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
7
7
  import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
@@ -114,9 +114,9 @@ export const createNodeChangedDecorationWidget = ({
114
114
  reveal,
115
115
  tagMountContext
116
116
  }) => {
117
- var _slice$content, _slice$content2, _slice$content2$first, _slice$content3, _slice$content3$first, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
117
+ var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
118
118
  const slice = doc.slice(change.fromA, change.toA);
119
- const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 ? void 0 : (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 ? void 0 : (_slice$content2 = slice.content) === null || _slice$content2 === void 0 ? void 0 : (_slice$content2$first = _slice$content2.firstChild) === null || _slice$content2$first === void 0 ? void 0 : _slice$content2$first.type.name) === 'paragraph' && (slice === null || slice === void 0 ? void 0 : (_slice$content3 = slice.content) === null || _slice$content3 === void 0 ? void 0 : (_slice$content3$first = _slice$content3.firstChild) === null || _slice$content3$first === void 0 ? void 0 : _slice$content3$first.content.size) === 0;
119
+ const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
120
120
  // Widget decoration used for deletions as the content is not in the document
121
121
  // and we want to display the deleted content with a style.
122
122
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -156,7 +156,10 @@ export const createNodeChangedDecorationWidget = ({
156
156
  nodeViewSerializer,
157
157
  colorScheme,
158
158
  isInserted,
159
- diffType
159
+ diffType,
160
+ // Needed for the row's own indicator anchor; this path returns before the
161
+ // `anchor-name` assignment further down.
162
+ showIndicators
160
163
  });
161
164
  }
162
165
  const serializer = nodeViewSerializer;
@@ -207,10 +210,9 @@ export const createNodeChangedDecorationWidget = ({
207
210
  // Derived from the deleted range so it survives a recalculation — see the same reasoning in
208
211
  // `createInlineChangedDecoration`.
209
212
  const diffId = showContributorTags ? `widget-${change.fromA}-${change.toA}` : crypto.randomUUID();
210
- // Whether this widget hosts a tag. Removing nothing but whitespace leaves no visible deleted
211
- // content to caption, so the change stays unattributed — matching the inline half's rule in
212
- // `createInlineChangedDecoration` (EDITOR-8855).
213
- const canTagWidget = showContributorTags && hasVisibleContent(slice.content);
213
+ // Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
214
+ // text in a rendered widget remains eligible.
215
+ const canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
214
216
  const decorations = [];
215
217
  const replacementNode = newDoc.nodeAt(change.fromB);
216
218
  const firstReplacedNode = slice.content.firstChild;
@@ -0,0 +1,5 @@
1
+ /** Whether a slice contains exactly one structurally empty paragraph. */
2
+ export const isEmptyParagraphSlice = slice => {
3
+ var _slice$content$firstC;
4
+ return (slice === null || slice === void 0 ? void 0 : slice.content.childCount) === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'paragraph' && slice.content.firstChild.content.size === 0;
5
+ };
@@ -1,4 +1,4 @@
1
- /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  import "./IndicatorBar.compiled.css";
3
3
  import * as React from 'react';
4
4
  import { ax, ix } from "@compiled/react/runtime";
@@ -1,4 +1,4 @@
1
- /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  import { ax, ix } from "@compiled/react/runtime";
3
3
  import React from 'react';
4
4
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
@@ -19,15 +19,40 @@ import { PARTICIPANT_COLOR_SCHEMES } from './types';
19
19
 
20
20
  /** Maps the shared 18-slot telepointer palette onto the ten bolder diff colours. */
21
21
  var HASHED_PARTICIPANT_COLOR_SCHEMES = [].concat(_toConsumableArray(PARTICIPANT_COLOR_SCHEMES), ['blue', 'red', 'orange', 'yellow', 'green', 'teal', 'purple', 'magenta']);
22
+
23
+ /**
24
+ * Red means "deleted" in both public schemes, so it never identifies an actor. Reserving it here
25
+ * covers every path into an attribution colour; `createAttributionColorMap` is the only caller, and
26
+ * it is gated on `confluence_ncs_step_diffing_version_history` plus
27
+ * `platform_editor_show_diff_color_scheme_refactor`.
28
+ */
29
+ var RESERVED_ATTRIBUTION_COLORS = new Set(['red']);
30
+ var isAssignable = function isAssignable(color) {
31
+ return !RESERVED_ATTRIBUTION_COLORS.has(color);
32
+ };
33
+
34
+ /** Projects a telepointer slot onto its diff hue, advancing past a hue reserved for deletion. */
35
+ var getSlotColor = function getSlotColor(index) {
36
+ for (var offset = 0; offset < HASHED_PARTICIPANT_COLOR_SCHEMES.length; offset++) {
37
+ var color = HASHED_PARTICIPANT_COLOR_SCHEMES[(index + offset) % HASHED_PARTICIPANT_COLOR_SCHEMES.length];
38
+ if (color && isAssignable(color)) {
39
+ return color;
40
+ }
41
+ }
42
+ return undefined;
43
+ };
22
44
  var getAvailableColor = function getAvailableColor(hashedColor, allocatedColors) {
45
+ var _PARTICIPANT_COLOR_SC;
23
46
  var startIndex = PARTICIPANT_COLOR_SCHEMES.indexOf(hashedColor);
24
47
  for (var offset = 0; offset < PARTICIPANT_COLOR_SCHEMES.length; offset++) {
25
48
  var color = PARTICIPANT_COLOR_SCHEMES[(startIndex + offset) % PARTICIPANT_COLOR_SCHEMES.length];
26
- if (color && !allocatedColors.has(color)) {
49
+ if (color && isAssignable(color) && !allocatedColors.has(color)) {
27
50
  return color;
28
51
  }
29
52
  }
30
- return hashedColor;
53
+
54
+ // Every assignable hue is taken, so a duplicate is unavoidable — but never the reserved hue.
55
+ return isAssignable(hashedColor) ? hashedColor : (_PARTICIPANT_COLOR_SC = PARTICIPANT_COLOR_SCHEMES.find(isAssignable)) !== null && _PARTICIPANT_COLOR_SC !== void 0 ? _PARTICIPANT_COLOR_SC : hashedColor;
31
56
  };
32
57
 
33
58
  /**
@@ -127,7 +152,8 @@ export var createAttributionColorMap = function createAttributionColorMap(stepAt
127
152
  var colors = new Map();
128
153
  var allocatedColors = new Set();
129
154
 
130
- // Reserve shared brand colours before hashing other actors, regardless of step order.
155
+ // Reserve shared brand colours before hashing other actors, regardless of step order. A brand
156
+ // slot is pinned deliberately, so it is taken verbatim rather than through `getSlotColor`.
131
157
  var _iterator2 = _createForOfIteratorHelper(stepAttributions),
132
158
  _step2;
133
159
  try {
@@ -162,7 +188,7 @@ export var createAttributionColorMap = function createAttributionColorMap(stepAt
162
188
  }
163
189
  var _getParticipantColor2 = getParticipantColor(_identity.colorSeed, _attribution === null || _attribution === void 0 ? void 0 : _attribution.agentType),
164
190
  _index = _getParticipantColor2.index;
165
- var hashedColor = HASHED_PARTICIPANT_COLOR_SCHEMES[_index];
191
+ var hashedColor = getSlotColor(_index);
166
192
  if (!hashedColor) {
167
193
  continue;
168
194
  }
@@ -10,12 +10,14 @@ import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
10
10
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
11
11
  import { TableMap } from '@atlaskit/editor-tables/table-map';
12
12
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
13
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
13
14
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
14
15
  import { isExtendedEnabled } from '../isExtendedEnabled';
15
16
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildDeletedRowStyle } from './colorSchemes/factory';
16
17
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
18
+ import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
17
19
  import { resolveCellOverlayStyleLegacy, resolveDeletedRowStyleLegacy } from './createChangedRowDecorationWidgets.styles.legacy';
18
- import { buildDiffDecorationSpec } from './decorationKeys';
20
+ import { buildAnchorDecorationKey, buildDiffDecorationSpec } from './decorationKeys';
19
21
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
20
22
  import { applyCellEdgeAttrs, getRowCellEdgeAttrs, getTableTopAndBottomCellEdgeAttrs } from './utils/tableCellEdgeAttrs';
21
23
  /**
@@ -47,8 +49,14 @@ var extractChangedRows = function extractChangedRows(_ref) {
47
49
  return changedRows;
48
50
  }
49
51
  var newTableMap = TableMap.get(tableNew.node);
52
+ // A row DELETION makes the table shorter. An in-place whole-row REPLACEMENT (`replaceNode` on a
53
+ // `tableRow`, which AI suggested edits emits) leaves the row count untouched — and this helper
54
+ // was written for deletions only, so it used to discard every replacement and the changed row's
55
+ // content was never rendered anywhere. The reviewer was left with just the removed side.
56
+ var isRowDeletion = oldTableMap.height > newTableMap.height;
57
+ var handleRowReplacement = oldTableMap.height === newTableMap.height && fg('platform_editor_ai_show_diff_patch_1');
50
58
  // If no rows were changed, return empty
51
- if (oldTableMap.height <= newTableMap.height ||
59
+ if (!(isRowDeletion || handleRowReplacement) ||
52
60
  // For now ignore if there are column deletions as well
53
61
  oldTableMap.width !== newTableMap.width) {
54
62
  return changedRows;
@@ -92,6 +100,15 @@ var extractChangedRows = function extractChangedRows(_ref) {
92
100
 
93
101
  // Filter changes that never truly got deleted
94
102
  return changedRows.filter(function (changedRow) {
103
+ // A replacement leaves every sibling row in place, so asking "does this row still exist
104
+ // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a
105
+ // sibling. Compare it against the row at the same index instead. Deletions must keep the
106
+ // whole-table `some()`: their indices shift, so an index comparison would let every
107
+ // surviving row through and draw a widget for each one.
108
+ if (handleRowReplacement) {
109
+ var counterpart = tableNew.node.maybeChild(changedRow.rowIndex);
110
+ return !counterpart || !areNodesEqualIgnoreAttrs(counterpart, changedRow.rowNode);
111
+ }
95
112
  return !tableNew.node.children.some(function (newRow) {
96
113
  return areNodesEqualIgnoreAttrs(newRow, changedRow.rowNode);
97
114
  });
@@ -135,7 +152,10 @@ var createChangedRowDOM = function createChangedRowDOM(rowNode, cellEdgeAttrs, n
135
152
  if (!isExtendedEnabled(diffType) || !isInserted) {
136
153
  tr.setAttribute('style', isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme)));
137
154
  }
138
- tr.setAttribute('data-testid', 'show-diff-deleted-row');
155
+ // Mirrors the strikethrough condition above: under the extended experience an `isInserted` row
156
+ // is ADDED content, so it must not claim the "deleted" testid — page models match that as
157
+ // removed content (same reasoning as `createTableCellContentWidgets`).
158
+ tr.setAttribute('data-testid', isExtendedEnabled(diffType) && isInserted && fg('platform_editor_ai_show_diff_patch_1') ? 'show-diff-changed-row' : 'show-diff-deleted-row');
139
159
 
140
160
  // Serialize each cell in the row
141
161
  var cellIndex = 0;
@@ -215,7 +235,9 @@ export var createChangedRowDecorationWidgets = function createChangedRowDecorati
215
235
  colorScheme = _ref3.colorScheme,
216
236
  _ref3$isInserted = _ref3.isInserted,
217
237
  isInserted = _ref3$isInserted === void 0 ? false : _ref3$isInserted,
218
- diffType = _ref3.diffType;
238
+ diffType = _ref3.diffType,
239
+ _ref3$showIndicators = _ref3.showIndicators,
240
+ showIndicators = _ref3$showIndicators === void 0 ? false : _ref3$showIndicators;
219
241
  // First, expand the changes to include complete deleted rows
220
242
  var changedRows = expandDiffForChangedRows({
221
243
  changes: changes.filter(function (change) {
@@ -225,7 +247,7 @@ export var createChangedRowDecorationWidgets = function createChangedRowDecorati
225
247
  newDoc: newDoc,
226
248
  diffType: diffType
227
249
  });
228
- return changedRows.map(function (changedRow) {
250
+ return changedRows.flatMap(function (changedRow) {
229
251
  var rowDOM = createChangedRowDOM(changedRow.rowNode, changedRow.cellEdgeAttrs, nodeViewSerializer, colorScheme, isInserted, diffType);
230
252
 
231
253
  // Find safe insertion position for the deleted row
@@ -233,13 +255,37 @@ export var createChangedRowDecorationWidgets = function createChangedRowDecorati
233
255
  // -1 to find the first safe position from the table
234
256
  originalDoc.slice(changedRow.fromA, changedRow.toA));
235
257
  var diffId = crypto.randomUUID();
236
- return Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, buildDiffDecorationSpec({
258
+ var decorations = [];
259
+
260
+ // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its
261
+ // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar
262
+ // is still created but can never resolve a position, so it silently does not render and the
263
+ // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same
264
+ // property on its own widget DOM for non-table content; this path returns before reaching it.
265
+ if (showIndicators && isExtendedEnabled(diffType) && fg('platform_editor_ai_show_diff_patch_1')) {
266
+ rowDOM.style.setProperty('anchor-name', "--".concat(buildAnchorDecorationKey({
267
+ diffId: diffId
268
+ })));
269
+
270
+ // A table's content can extend past the doc margin, so the bar also needs a left anchor
271
+ // measured against the table itself or it is clipped once the table is resized.
272
+ var leftAnchor = createLeftAnchorWidget({
273
+ doc: newDoc,
274
+ from: safeInsertPos,
275
+ diffId: diffId
276
+ });
277
+ if (leftAnchor) {
278
+ decorations.push(leftAnchor);
279
+ }
280
+ }
281
+ decorations.push(Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, buildDiffDecorationSpec({
237
282
  colorScheme: colorScheme,
238
283
  decorationType: 'widget',
239
284
  diffId: diffId,
240
285
  isActive: false,
241
286
  isInserted: isInserted,
242
287
  diffType: diffType
243
- })));
288
+ }))));
289
+ return decorations;
244
290
  });
245
291
  };
@@ -7,7 +7,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
7
7
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
8
8
  import { CONTRIBUTOR_TAG_Z_INDEX } from '../../ui/ContributorTag/buildContributorTagDom';
9
9
  import { isExtendedEnabled } from '../isExtendedEnabled';
10
- import { hasVisibleContent } from '../utils/hasVisibleContent';
10
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
11
11
  import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
12
12
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
13
13
  import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
@@ -156,11 +156,9 @@ export var createInlineChangedDecoration = function createInlineChangedDecoratio
156
156
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
157
157
  var diffId = showContributorTags ? "inline-".concat(change.fromB, "-").concat(change.toB) : crypto.randomUUID();
158
158
 
159
- // Whether this highlight hosts a tag. A range of nothing but whitespace has nothing to caption,
160
- // so it stays unattributed: the spec is the only route attribution takes to
161
- // `extractContributorTags`, so dropping the key there is what keeps a tag from being resolved for
162
- // a host that is never emitted (EDITOR-8855).
163
- var canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && hasVisibleContent(doc.slice(change.fromB, change.toB).content);
159
+ // Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
160
+ // content with no tag host.
161
+ var canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && !isEmptyParagraphSlice(doc.slice(change.fromB, change.toB));
164
162
  if (shouldHideDeleted) {
165
163
  return [Decoration.inline(change.fromB, change.toB, {
166
164
  style: displayNoneStyle
@@ -4,7 +4,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
5
  import { fg } from '@atlaskit/platform-feature-flags/fg';
6
6
  import { isExtendedEnabled } from '../isExtendedEnabled';
7
- import { hasVisibleContent } from '../utils/hasVisibleContent';
7
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
8
8
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
9
9
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
10
10
  import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
@@ -94,7 +94,7 @@ var createTableCellContentWidgets = function createTableCellContentWidgets(_ref)
94
94
  * that is not in the current document.
95
95
  */
96
96
  export var createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) {
97
- var _slice$content, _slice$content2, _slice$content3, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
97
+ var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
98
98
  var attributionKey = _ref2.attributionKey,
99
99
  change = _ref2.change,
100
100
  doc = _ref2.doc,
@@ -117,7 +117,7 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
117
117
  reveal = _ref2.reveal,
118
118
  tagMountContext = _ref2.tagMountContext;
119
119
  var slice = doc.slice(change.fromA, change.toA);
120
- var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 || (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 || (_slice$content2 = slice.content) === null || _slice$content2 === void 0 || (_slice$content2 = _slice$content2.firstChild) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.type.name) === 'paragraph' && (slice === null || slice === void 0 || (_slice$content3 = slice.content) === null || _slice$content3 === void 0 || (_slice$content3 = _slice$content3.firstChild) === null || _slice$content3 === void 0 ? void 0 : _slice$content3.content.size) === 0;
120
+ var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
121
121
  // Widget decoration used for deletions as the content is not in the document
122
122
  // and we want to display the deleted content with a style.
123
123
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -165,7 +165,10 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
165
165
  nodeViewSerializer: nodeViewSerializer,
166
166
  colorScheme: colorScheme,
167
167
  isInserted: isInserted,
168
- diffType: diffType
168
+ diffType: diffType,
169
+ // Needed for the row's own indicator anchor; this path returns before the
170
+ // `anchor-name` assignment further down.
171
+ showIndicators: showIndicators
169
172
  });
170
173
  }
171
174
  var serializer = nodeViewSerializer;
@@ -216,10 +219,9 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
216
219
  // Derived from the deleted range so it survives a recalculation — see the same reasoning in
217
220
  // `createInlineChangedDecoration`.
218
221
  var diffId = showContributorTags ? "widget-".concat(change.fromA, "-").concat(change.toA) : crypto.randomUUID();
219
- // Whether this widget hosts a tag. Removing nothing but whitespace leaves no visible deleted
220
- // content to caption, so the change stays unattributed — matching the inline half's rule in
221
- // `createInlineChangedDecoration` (EDITOR-8855).
222
- var canTagWidget = showContributorTags && hasVisibleContent(slice.content);
222
+ // Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
223
+ // text in a rendered widget remains eligible.
224
+ var canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
223
225
  var decorations = [];
224
226
  var replacementNode = newDoc.nodeAt(change.fromB);
225
227
  var firstReplacedNode = slice.content.firstChild;
@@ -0,0 +1,5 @@
1
+ /** Whether a slice contains exactly one structurally empty paragraph. */
2
+ export var isEmptyParagraphSlice = function isEmptyParagraphSlice(slice) {
3
+ var _slice$content$firstC;
4
+ return (slice === null || slice === void 0 ? void 0 : slice.content.childCount) === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'paragraph' && slice.content.firstChild.content.size === 0;
5
+ };
@@ -1,4 +1,4 @@
1
- /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBar.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  import "./IndicatorBar.compiled.css";
3
3
  import * as React from 'react';
4
4
  import { ax, ix } from "@compiled/react/runtime";
@@ -1,4 +1,4 @@
1
- /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.2 */
1
+ /* IndicatorBarContentComponent.tsx generated by @compiled/babel-plugin v3.0.1 */
2
2
  import { ax, ix } from "@compiled/react/runtime";
3
3
  import React from 'react';
4
4
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
@@ -8,7 +8,7 @@ type SimpleChange = Pick<Change, 'fromA' | 'toA' | 'fromB' | 'deleted'>;
8
8
  /**
9
9
  * Main function to handle deleted rows - computes diff and creates decorations
10
10
  */
11
- export declare const createChangedRowDecorationWidgets: ({ changes, originalDoc, newDoc, nodeViewSerializer, colorScheme, isInserted, diffType, }: {
11
+ export declare const createChangedRowDecorationWidgets: ({ changes, originalDoc, newDoc, nodeViewSerializer, colorScheme, isInserted, diffType, showIndicators, }: {
12
12
  changes: SimpleChange[];
13
13
  colorScheme?: ColorScheme;
14
14
  diffType?: DiffType;
@@ -16,5 +16,6 @@ export declare const createChangedRowDecorationWidgets: ({ changes, originalDoc,
16
16
  newDoc: PMNode;
17
17
  nodeViewSerializer: NodeViewSerializer;
18
18
  originalDoc: PMNode;
19
+ showIndicators?: boolean;
19
20
  }) => Decoration[];
20
21
  export {};
@@ -0,0 +1,3 @@
1
+ import type { Slice } from '@atlaskit/editor-prosemirror/model';
2
+ /** Whether a slice contains exactly one structurally empty paragraph. */
3
+ export declare const isEmptyParagraphSlice: (slice: Slice | undefined) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "16.0.12",
3
+ "version": "16.0.14",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -64,7 +64,7 @@
64
64
  "react-intl": "^7.0.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^122.7.0",
67
+ "@atlaskit/editor-common": "^122.8.0",
68
68
  "react": "^18.2.0 || ^19.2.0",
69
69
  "react-dom": "^18.2.0 || ^19.2.0",
70
70
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
@@ -1,37 +0,0 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
- Object.defineProperty(exports, "__esModule", {
5
- value: true
6
- });
7
- exports.hasVisibleContent = void 0;
8
- var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
9
- var _charsByOffset = require("./charsByOffset");
10
- /**
11
- * Whether `fragment` holds anything a reader can see.
12
- *
13
- * Text counts once it carries a single non-whitespace character; a leaf node (mention, emoji, date,
14
- * media, rule, hardBreak, …) counts in its own right, exactly as `resolveTagAnchorPos` treats one.
15
- * Everything else is descended into, so a paragraph is visible when its content is.
16
- *
17
- * A change whose content is nothing but whitespace — a space typed between two words, a run of
18
- * trailing spaces, an added blank paragraph — has no visible content for a contributor tag to
19
- * caption, so it carries no tag (EDITOR-8855).
20
- */
21
- var _hasVisibleContent = exports.hasVisibleContent = function hasVisibleContent(fragment) {
22
- var visible = false;
23
- fragment.forEach(function (node) {
24
- if (visible) {
25
- return;
26
- }
27
- if (node.isText) {
28
- var _node$text;
29
- visible = (0, _toConsumableArray2.default)((_node$text = node.text) !== null && _node$text !== void 0 ? _node$text : '').some(function (char) {
30
- return !(0, _charsByOffset.isWhitespaceChar)(char);
31
- });
32
- return;
33
- }
34
- visible = node.isLeaf || _hasVisibleContent(node.content);
35
- });
36
- return visible;
37
- };
@@ -1,28 +0,0 @@
1
- import { isWhitespaceChar } from './charsByOffset';
2
-
3
- /**
4
- * Whether `fragment` holds anything a reader can see.
5
- *
6
- * Text counts once it carries a single non-whitespace character; a leaf node (mention, emoji, date,
7
- * media, rule, hardBreak, …) counts in its own right, exactly as `resolveTagAnchorPos` treats one.
8
- * Everything else is descended into, so a paragraph is visible when its content is.
9
- *
10
- * A change whose content is nothing but whitespace — a space typed between two words, a run of
11
- * trailing spaces, an added blank paragraph — has no visible content for a contributor tag to
12
- * caption, so it carries no tag (EDITOR-8855).
13
- */
14
- export const hasVisibleContent = fragment => {
15
- let visible = false;
16
- fragment.forEach(node => {
17
- if (visible) {
18
- return;
19
- }
20
- if (node.isText) {
21
- var _node$text;
22
- visible = [...((_node$text = node.text) !== null && _node$text !== void 0 ? _node$text : '')].some(char => !isWhitespaceChar(char));
23
- return;
24
- }
25
- visible = node.isLeaf || hasVisibleContent(node.content);
26
- });
27
- return visible;
28
- };
@@ -1,32 +0,0 @@
1
- import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
- import { isWhitespaceChar } from './charsByOffset';
3
-
4
- /**
5
- * Whether `fragment` holds anything a reader can see.
6
- *
7
- * Text counts once it carries a single non-whitespace character; a leaf node (mention, emoji, date,
8
- * media, rule, hardBreak, …) counts in its own right, exactly as `resolveTagAnchorPos` treats one.
9
- * Everything else is descended into, so a paragraph is visible when its content is.
10
- *
11
- * A change whose content is nothing but whitespace — a space typed between two words, a run of
12
- * trailing spaces, an added blank paragraph — has no visible content for a contributor tag to
13
- * caption, so it carries no tag (EDITOR-8855).
14
- */
15
- var _hasVisibleContent = function hasVisibleContent(fragment) {
16
- var visible = false;
17
- fragment.forEach(function (node) {
18
- if (visible) {
19
- return;
20
- }
21
- if (node.isText) {
22
- var _node$text;
23
- visible = _toConsumableArray((_node$text = node.text) !== null && _node$text !== void 0 ? _node$text : '').some(function (char) {
24
- return !isWhitespaceChar(char);
25
- });
26
- return;
27
- }
28
- visible = node.isLeaf || _hasVisibleContent(node.content);
29
- });
30
- return visible;
31
- };
32
- export { _hasVisibleContent as hasVisibleContent };
@@ -1,13 +0,0 @@
1
- import type { Fragment } from '@atlaskit/editor-prosemirror/model';
2
- /**
3
- * Whether `fragment` holds anything a reader can see.
4
- *
5
- * Text counts once it carries a single non-whitespace character; a leaf node (mention, emoji, date,
6
- * media, rule, hardBreak, …) counts in its own right, exactly as `resolveTagAnchorPos` treats one.
7
- * Everything else is descended into, so a paragraph is visible when its content is.
8
- *
9
- * A change whose content is nothing but whitespace — a space typed between two words, a run of
10
- * trailing spaces, an added blank paragraph — has no visible content for a contributor tag to
11
- * caption, so it carries no tag (EDITOR-8855).
12
- */
13
- export declare const hasVisibleContent: (fragment: Fragment) => boolean;