@atlaskit/editor-plugin-show-diff 25.1.0 → 26.0.1

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 (22) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +39 -6
  3. package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +14 -2
  4. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  5. package/dist/cjs/pm-plugins/decorations/createReplacedCellDecorations.js +350 -0
  6. package/dist/cjs/pm-plugins/decorations/utils/cellAttrs.js +30 -0
  7. package/dist/cjs/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +13 -4
  8. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +40 -7
  9. package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +12 -2
  10. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  11. package/dist/es2019/pm-plugins/decorations/createReplacedCellDecorations.js +338 -0
  12. package/dist/es2019/pm-plugins/decorations/utils/cellAttrs.js +19 -0
  13. package/dist/es2019/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +13 -4
  14. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +39 -6
  15. package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +14 -2
  16. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  17. package/dist/esm/pm-plugins/decorations/createReplacedCellDecorations.js +343 -0
  18. package/dist/esm/pm-plugins/decorations/utils/cellAttrs.js +25 -0
  19. package/dist/esm/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +13 -4
  20. package/dist/types/pm-plugins/decorations/createReplacedCellDecorations.d.ts +67 -0
  21. package/dist/types/pm-plugins/decorations/utils/cellAttrs.d.ts +13 -0
  22. package/package.json +9 -9
@@ -1,6 +1,7 @@
1
1
  import isEqual from 'lodash/isEqual';
2
2
  import memoizeOne from 'memoize-one';
3
3
  import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
4
+ import { isAiXstateMigrationEnabledNoExposure } from '@atlaskit/editor-common/ai-xstate-migration-no-exposure';
4
5
  import { areDocsEqualByBlockStructureAndText } from '@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText';
5
6
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
6
7
  import { Mapping } from '@atlaskit/editor-prosemirror/transform';
@@ -8,12 +9,12 @@ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
8
9
  import { TableMap } from '@atlaskit/editor-tables/table-map';
9
10
  import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
10
11
  import { fg } from '@atlaskit/platform-feature-flags/fg';
11
- import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
12
12
  import { createAttributionColorMap, getAttributionKey, getAttributionKeyForChange, getColorSchemeForChange, isAttributionColoringEnabled, isContributorTagsEnabled } from '../decorations/colorSchemes/attributions';
13
13
  import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorationWidgets';
14
14
  import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
15
15
  import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
16
16
  import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
17
+ import { createReplacedCellDecorations, getReplacedCells } from '../decorations/createReplacedCellDecorations';
17
18
  import { extractDiffDescriptors } from '../decorations/decorationKeys';
18
19
  import { extractContributorTags } from '../decorations/extractContributorTags';
19
20
  import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
@@ -548,7 +549,11 @@ const calculateDiffDecorationsInner = ({
548
549
  // suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
549
550
  // asked to compare against the original, and the deleted side is the only place the
550
551
  // previous formatting is visible.
551
- const isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && change.deleted.length > 0 && isMarkOnlyChange({
552
+ //
553
+ // Not on an inverted diff: there the widget's side is the suggested content, which the clean
554
+ // view keeps. Suppressing it hides the suggestion along with the displayed side it replaces,
555
+ // e.g. a cell whose only change is its background, as the signature ignores attrs.
556
+ const isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && !(isInverted && fg('platform_editor_ai_show_diff_patch_2')) && change.deleted.length > 0 && isMarkOnlyChange({
552
557
  change,
553
558
  originalDoc,
554
559
  newDoc: tr.doc
@@ -615,7 +620,7 @@ const calculateDiffDecorationsInner = ({
615
620
  // For a large inserted table, skip the per-cell inline decorations that
616
621
  // freeze the browser on re-render. Only active when Review moment is
617
622
  // eligible (xstate + M1).
618
- const useCoarseTableDecoration = isSmartNodeLevel && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true && isLargeInsertedTableRange(tr.doc, change.fromB, change.toB);
623
+ const useCoarseTableDecoration = isSmartNodeLevel && isAiXstateMigrationEnabledNoExposure() && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true && isLargeInsertedTableRange(tr.doc, change.fromB, change.toB);
619
624
  // Whether the deleted-content widget will actually be rendered for this
620
625
  // change. Used to decide if indicator anchor positions should be adjusted
621
626
  // inward — when the widget is present the anchor must stay at the block
@@ -625,7 +630,35 @@ const calculateDiffDecorationsInner = ({
625
630
  hideDeletedDiffs,
626
631
  isInverted
627
632
  });
628
- if (isSmartNodeLevel) {
633
+
634
+ // A whole-cell replacement keeps its cells in place, so it takes its own decorations
635
+ // instead of the generic inline and block ones.
636
+ const replacedCells = fg('platform_editor_ai_show_diff_patch_2') ? getReplacedCells({
637
+ slice: originalDoc.slice(change.fromA, change.toA),
638
+ doc: tr.doc,
639
+ fromB: change.fromB,
640
+ toB: change.toB
641
+ }) : [];
642
+ if (replacedCells.length > 0) {
643
+ decorations.push(...createReplacedCellDecorations({
644
+ replacedCells,
645
+ change,
646
+ doc: tr.doc,
647
+ attributionKey,
648
+ colorScheme: changeColorScheme,
649
+ hasDeletedWidget: willRenderDeletedWidget,
650
+ hideAddedDiffsUnderline,
651
+ isActive,
652
+ isDeletedWidgetBelow,
653
+ isInserted,
654
+ leftAnchorId,
655
+ reveal,
656
+ shouldHideDeleted,
657
+ showContributorTags,
658
+ showIndicators,
659
+ tagMountContext
660
+ }));
661
+ } else if (isSmartNodeLevel) {
629
662
  // For a large inserted table, skip the O(cells) inline decorations inside the
630
663
  // table (the cell overlays added below provide the highlight without the
631
664
  // expensive content re-render); leaf blocks outside the table still get theirs.
@@ -674,7 +707,7 @@ const calculateDiffDecorationsInner = ({
674
707
  tagMountContext
675
708
  }));
676
709
  }
677
- decorations.push(...calculateNodesForBlockDecoration({
710
+ decorations.push(...(replacedCells.length > 0 ? [] : calculateNodesForBlockDecoration({
678
711
  // A deleted widget placed below anchors at the change's end, inside the very block
679
712
  // this would let the decoration claim.
680
713
  allowTrailingCloseTokens: !isDeletedWidgetBelow,
@@ -694,7 +727,7 @@ const calculateDiffDecorationsInner = ({
694
727
  intl,
695
728
  coarseTableCellsOnly: useCoarseTableDecoration,
696
729
  leftAnchorId
697
- }));
730
+ })));
698
731
  }
699
732
  if (change.deleted.length > 0 && !isMarkOnly && !hasNoDeletedContent) {
700
733
  const shouldHideDeleted = shouldHideDeletedSide({
@@ -845,7 +878,7 @@ const calculateDiffDecorationsInner = ({
845
878
  }));
846
879
  // If the original node position is known (e.g. a panel type change), also render
847
880
  // the old block node as a "deleted" widget so the reviewer sees the before/after.
848
- const isSmartNodeLevelAttrChange = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true;
881
+ const isSmartNodeLevelAttrChange = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && isAiXstateMigrationEnabledNoExposure() && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true;
849
882
  if (isSmartNodeLevelAttrChange && change.fromA !== undefined && change.toA !== undefined) {
850
883
  const placeBelow = deletedDiffPlacement === 'bottom';
851
884
  decorations.push(...createNodeChangedDecorationWidget({
@@ -3,6 +3,7 @@ import { Mark } from '@atlaskit/editor-prosemirror/model';
3
3
  import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
4
4
  import { fg } from '@atlaskit/platform-feature-flags/fg';
5
5
  import { getAttributionKey } from '../decorations/colorSchemes/attributions';
6
+ import { isCellAttrChanged } from '../decorations/utils/cellAttrs';
6
7
  import { buildCharsByOffset } from '../utils/charsByOffset';
7
8
  import { attrAwareTokenEncoder } from './attrAwareTokenEncoder';
8
9
  import { optimizeChanges } from './optimizeChanges';
@@ -17,6 +18,14 @@ const WHITESPACE_REGEX = /\s/u;
17
18
  * excluded: `groupDeletedColumnChanges` relies on a replaced table being one whole-table change.
18
19
  */
19
20
  const isGranularIsolatingNode = node => !!node.type.spec.isolating && node.type.spec.tableRole !== 'table';
21
+ const CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
22
+
23
+ /**
24
+ * Whether a table cell's replacement changes one of its diffable attributes, such as its background.
25
+ * Such a cell is diffed whole: it renders as a cell style plus a copy of its row, which already shows
26
+ * the cell's new content, so inline marks inside it would only repeat that.
27
+ */
28
+ const isCellAttrChange = (replacedNode, replacingNode) => CELL_TYPE_NAMES.includes(replacedNode.type.name) && isCellAttrChanged(replacedNode, replacingNode);
20
29
  const mapPosition = (mapping, pos) => mapping.map(pos);
21
30
 
22
31
  /**
@@ -206,7 +215,8 @@ const mergeOverlappingByNewDocRange = changes => {
206
215
  * - The replaced slice has only one child
207
216
  * - The replacing slice has only one child
208
217
  * - Both children are the same textblock type, or (behind `platform_editor_ai_show_diff_patch_2`)
209
- * the same isolating, non-table node type whose text content differs
218
+ * the same isolating, non-table node type whose text content differs (for a table cell, only when
219
+ * its diffable attributes are unchanged)
210
220
  * - The replaced slice and replacing slice have the same text content
211
221
  * - The replaced slice and replacing slice have the same child marks (if text content is equal)
212
222
  */
@@ -244,7 +254,7 @@ const shouldCheckGranularDiff = (step, before, from, to) => {
244
254
  // `hasSameChildMarks` only compares an isolating node's direct (block) children, so it cannot
245
255
  // vouch for one with unchanged text. Keep the existing whole-node fallback for that case.
246
256
  if (isIsolatingNode) {
247
- return !isTextContentEqual;
257
+ return !isTextContentEqual && !(replacingNode && isCellAttrChange(replacedNode, replacingNode));
248
258
  }
249
259
  return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
250
260
  };
@@ -10,6 +10,7 @@ import { clampAnchorPosIntoCell, createAnchorNameSpan, createLeftAnchorWidget }
10
10
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
11
11
  import { createContributorTagHost, resolveHoistedCodeBlockAnchor, unmountContributorTag } from './createContributorTagWidget';
12
12
  import { getAtomicInlineChangedAttrs } from './createInlineChangedDecoration';
13
+ import { createReplacedCellWidgets } from './createReplacedCellDecorations';
13
14
  import { AnchorTypeKey, buildContributorTagDecorationSpec, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
14
15
  import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
15
16
  import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
@@ -214,6 +215,19 @@ export const createNodeChangedDecorationWidget = ({
214
215
  isInserted
215
216
  });
216
217
  if (tableDiffMode.kind === 'cells') {
218
+ if (fg('platform_editor_ai_show_diff_patch_2')) {
219
+ return createReplacedCellWidgets({
220
+ slice,
221
+ newDoc,
222
+ change,
223
+ nodeViewSerializer,
224
+ colorScheme,
225
+ intl,
226
+ isActive,
227
+ isInserted,
228
+ leftAnchorId
229
+ });
230
+ }
217
231
  return createTableCellContentWidgets({
218
232
  slice,
219
233
  newDoc,
@@ -0,0 +1,338 @@
1
+ import { getDarkModeLCHColor } from '@atlaskit/adf-schema/get-dark-mode-lch-color';
2
+ import { isRgb } from '@atlaskit/adf-schema/is-rgb';
3
+ import { rgbToHex } from '@atlaskit/adf-schema/rgb-to-hex';
4
+ import { getCellDomAttrs } from '@atlaskit/adf-schema/tableNodes';
5
+ import { convertToInlineCss } from '@atlaskit/editor-common/css-helper';
6
+ import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
7
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
8
+ import { createBlockChangedDecoration } from './createBlockChangedDecoration';
9
+ import { createInlineChangedDecoration } from './createInlineChangedDecoration';
10
+ import { buildDiffDecorationSpec } from './decorationKeys';
11
+ import { isCellAttrChanged, isCellBackgroundChanged, normalizeCellBackground } from './utils/cellAttrs';
12
+ import { injectInnerWrapper, wrapBlockNodeView } from './utils/wrapBlockNodeView';
13
+ const CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
14
+ /**
15
+ * Pairs each cell of a whole-cell replacement with the live cell it replaces, or `[]` when the slice
16
+ * is not a run of whole cells matching the live ones. Whole-cell replacement preserves cell count and
17
+ * order, so the nth live cell in the range matches the nth cell in the slice.
18
+ */
19
+ export const getReplacedCells = ({
20
+ slice,
21
+ doc,
22
+ fromB,
23
+ toB
24
+ }) => {
25
+ const otherCells = slice.content.content;
26
+ if (slice.openStart !== 0 || slice.openEnd !== 0 || otherCells.length === 0 || !otherCells.every(node => CELL_TYPE_NAMES.includes(node.type.name))) {
27
+ return [];
28
+ }
29
+ const liveCells = [];
30
+ doc.nodesBetween(fromB, toB, (node, pos) => {
31
+ if (CELL_TYPE_NAMES.includes(node.type.name)) {
32
+ liveCells.push({
33
+ node,
34
+ pos
35
+ });
36
+ return false;
37
+ }
38
+ return true;
39
+ });
40
+ if (liveCells.length !== otherCells.length) {
41
+ return [];
42
+ }
43
+ return otherCells.map((otherCell, index) => ({
44
+ otherCell,
45
+ liveCell: liveCells[index].node,
46
+ liveCellPos: liveCells[index].pos
47
+ }));
48
+ };
49
+
50
+ /**
51
+ * Whether two cells hold the same content, ignoring the cells' own attributes and `localId`s, which
52
+ * a suggestion can regenerate. Such a replacement only changed the cell (e.g. its background), so
53
+ * there is no content to diff.
54
+ */
55
+ const isCellContentEqual = (left, right) => {
56
+ if (left.childCount !== right.childCount) {
57
+ return false;
58
+ }
59
+ for (let index = 0; index < left.childCount; index++) {
60
+ if (!areNodesEqualIgnoreAttrs(left.child(index), right.child(index), ['localId'])) {
61
+ return false;
62
+ }
63
+ }
64
+ return true;
65
+ };
66
+
67
+ /**
68
+ * Whether the cell itself changed — its type, or an attribute that represents a change such as its
69
+ * background or span — as opposed to only its content.
70
+ */
71
+ const isCellChanged = (left, right) => left.type !== right.type || isCellAttrChanged(left, right);
72
+
73
+ /**
74
+ * A custom (non-palette) cell colour as the schema renders it: inverted in dark mode so text stays
75
+ * readable, except for design tokens, which are already themed.
76
+ */
77
+ const getCustomCellColor = background => {
78
+ const color = isRgb(background) && rgbToHex(background) || background;
79
+ if (color.startsWith('var(') || document.documentElement.getAttribute('data-color-mode') !== 'dark') {
80
+ return background;
81
+ }
82
+ return getDarkModeLCHColor(color);
83
+ };
84
+
85
+ /**
86
+ * The colour `cell` renders with. A palette colour is the editor's own themed variable for its colour
87
+ * name (e.g. for dark mode), the one `td[colorname]` cells use; a custom colour is the schema's own
88
+ * treatment of it; no colour is the editor's default.
89
+ */
90
+ const getCellBackgroundColor = cell => {
91
+ const background = normalizeCellBackground(cell.attrs.background);
92
+ if (!background) {
93
+ return cell.type.name === 'tableHeader' ? "var(--ds-background-accent-gray-subtlest, #F0F1F2)" : "var(--ds-surface, #FFFFFF)";
94
+ }
95
+ const {
96
+ colorname
97
+ } = getCellDomAttrs(cell);
98
+ return colorname ? `var(--ak-editor-table-cell-background-${colorname.toLowerCase().split(' ').join('-')})` : getCustomCellColor(background);
99
+ };
100
+
101
+ /**
102
+ * A layer painting `color` over the cell's own background and under its content, so the cell reads
103
+ * as having the new background without any of the cell's own attributes being overridden: removing
104
+ * a decoration removes the attributes it set rather than restoring the ones they replaced.
105
+ */
106
+ const createCellBackgroundLayer = color => {
107
+ const layer = document.createElement('div');
108
+ layer.setAttribute('aria-hidden', 'true');
109
+ layer.contentEditable = 'false';
110
+ layer.dataset.testid = 'show-diff-new-cell-background';
111
+ layer.setAttribute('style', convertToInlineCss({
112
+ position: 'absolute',
113
+ inset: 0,
114
+ zIndex: -1,
115
+ borderRadius: 'inherit',
116
+ pointerEvents: 'none',
117
+ backgroundColor: color
118
+ }));
119
+ return layer;
120
+ };
121
+
122
+ /** The other side's cell content, as blocks, for the top of the live cell. */
123
+ const createCellContentWidget = ({
124
+ cell,
125
+ pos,
126
+ nodeViewSerializer,
127
+ colorScheme,
128
+ intl,
129
+ isActive,
130
+ isInserted,
131
+ leftAnchorId
132
+ }) => {
133
+ const dom = document.createElement('div');
134
+ cell.content.forEach(block => {
135
+ const nodeView = nodeViewSerializer.tryCreateNodeView(block, 0, undefined, isInserted, colorScheme);
136
+ if (nodeView) {
137
+ wrapBlockNodeView({
138
+ dom,
139
+ nodeView,
140
+ targetNode: block,
141
+ colorScheme,
142
+ intl,
143
+ isActive,
144
+ isInserted
145
+ });
146
+ return;
147
+ }
148
+ const serialized = nodeViewSerializer.serializeNode(block);
149
+ if (serialized instanceof HTMLElement) {
150
+ dom.append(injectInnerWrapper({
151
+ node: serialized,
152
+ colorScheme,
153
+ isActive,
154
+ isInserted
155
+ }));
156
+ }
157
+ });
158
+ if (dom.childNodes.length === 0) {
159
+ return undefined;
160
+ }
161
+ dom.setAttribute('data-testid', isInserted ? 'show-diff-changed-decoration' : 'show-diff-deleted-decoration');
162
+
163
+ // Before the cell's own blocks, as a block of its own, so it never lands inside a paragraph the
164
+ // displayed side may hide.
165
+ return Decoration.widget(pos + 1, dom, {
166
+ ...buildDiffDecorationSpec({
167
+ colorScheme,
168
+ decorationType: 'widget',
169
+ diffId: crypto.randomUUID(),
170
+ leftAnchorId,
171
+ isActive,
172
+ isInserted,
173
+ side: -1
174
+ }),
175
+ marks: []
176
+ });
177
+ };
178
+
179
+ /**
180
+ * The other side of a whole-cell replacement, drawn inside the live cells so the table keeps its
181
+ * shape: each live cell takes the other cell's background when it differs, and its content when that
182
+ * differs. A cell whose content did not change has nothing to diff.
183
+ */
184
+ export const createReplacedCellWidgets = ({
185
+ slice,
186
+ newDoc,
187
+ change,
188
+ nodeViewSerializer,
189
+ colorScheme,
190
+ intl,
191
+ isActive,
192
+ isInserted,
193
+ leftAnchorId
194
+ }) => getReplacedCells({
195
+ slice,
196
+ doc: newDoc,
197
+ fromB: change.fromB,
198
+ toB: change.toB
199
+ }).flatMap(({
200
+ otherCell,
201
+ liveCell,
202
+ liveCellPos
203
+ }) => {
204
+ const decorations = [];
205
+ if (isInserted && isCellBackgroundChanged(otherCell, liveCell)) {
206
+ const cellEnd = liveCellPos + liveCell.nodeSize;
207
+ decorations.push(
208
+ // The cell becomes the layer's stacking context, so the layer sits above the cell's own
209
+ // background rather than behind it. Through `z-index` at the cell overlay's own level, not
210
+ // `isolation`: an isolated cell would trap its overlay below later cells, which then cover
211
+ // the overlay's outline where it overlaps their border. Properties the cell never sets, so
212
+ // removing them takes nothing of its own.
213
+ Decoration.node(liveCellPos, cellEnd, {
214
+ style: convertToInlineCss({
215
+ position: 'relative',
216
+ // The highest z-index a cell diff overlay takes (the rounded-corner variant).
217
+ zIndex: 2
218
+ })
219
+ }, {
220
+ key: `show-diff-new-cell-background-host-${liveCellPos}`
221
+ }),
222
+ // At the cell's end, beside its diff overlay, so the cell's first block stays its first child.
223
+ Decoration.widget(cellEnd - 1, createCellBackgroundLayer(getCellBackgroundColor(otherCell)), {
224
+ key: `show-diff-new-cell-background-${liveCellPos}`,
225
+ side: -1,
226
+ marks: [],
227
+ ignoreSelection: true
228
+ }));
229
+ }
230
+ if (!isCellContentEqual(otherCell, liveCell)) {
231
+ const contentWidget = createCellContentWidget({
232
+ cell: otherCell,
233
+ pos: liveCellPos,
234
+ nodeViewSerializer,
235
+ colorScheme,
236
+ intl,
237
+ isActive,
238
+ isInserted,
239
+ leftAnchorId
240
+ });
241
+ if (contentWidget) {
242
+ decorations.push(contentWidget);
243
+ }
244
+ }
245
+ return decorations;
246
+ });
247
+
248
+ /**
249
+ * The displayed side of a whole-cell replacement. A table cell is never removed on its own, only
250
+ * replaced in place, so a live cell is never hidden — hiding it would leave the row a cell short. It
251
+ * takes the changed-cell treatment only when the cell itself changed; its content is diffed, and
252
+ * hidden in the clean view, only when that changed. A cell whose content stayed the same keeps
253
+ * showing it.
254
+ */
255
+ export const createReplacedCellDecorations = ({
256
+ replacedCells,
257
+ change,
258
+ doc,
259
+ attributionKey,
260
+ colorScheme,
261
+ hasDeletedWidget,
262
+ hideAddedDiffsUnderline,
263
+ isActive,
264
+ isDeletedWidgetBelow,
265
+ isInserted,
266
+ leftAnchorId,
267
+ reveal,
268
+ shouldHideDeleted,
269
+ showContributorTags,
270
+ showIndicators,
271
+ tagMountContext
272
+ }) => replacedCells.flatMap(({
273
+ otherCell,
274
+ liveCell,
275
+ liveCellPos
276
+ }) => {
277
+ const cellEnd = liveCellPos + liveCell.nodeSize;
278
+ const decorations = isCellChanged(otherCell, liveCell) ? createBlockChangedDecoration({
279
+ attributionKey,
280
+ change: {
281
+ from: liveCellPos,
282
+ to: cellEnd,
283
+ name: liveCell.type.name
284
+ },
285
+ colorScheme,
286
+ doc,
287
+ isActive,
288
+ // The cell stays either way, so it reads as changed, not removed.
289
+ isInserted: true,
290
+ leftAnchorId,
291
+ showContributorTags,
292
+ showIndicators,
293
+ tagMountContext
294
+ }) : [];
295
+ if (isCellContentEqual(otherCell, liveCell)) {
296
+ return decorations;
297
+ }
298
+ decorations.push(...createInlineChangedDecoration({
299
+ attributionKey,
300
+ change: {
301
+ ...change,
302
+ fromB: liveCellPos,
303
+ toB: cellEnd
304
+ },
305
+ colorScheme,
306
+ doc,
307
+ hasDeletedWidget,
308
+ hideAddedDiffsUnderline,
309
+ isActive,
310
+ isDeletedWidgetBelow,
311
+ isInserted,
312
+ leftAnchorId,
313
+ reveal,
314
+ shouldHideDeleted,
315
+ showContributorTags,
316
+ showIndicators,
317
+ tagMountContext
318
+ }));
319
+ if (shouldHideDeleted) {
320
+ liveCell.forEach((block, offset) => {
321
+ const blockPos = liveCellPos + 1 + offset;
322
+ decorations.push(...createBlockChangedDecoration({
323
+ change: {
324
+ from: blockPos,
325
+ to: blockPos + block.nodeSize,
326
+ name: block.type.name
327
+ },
328
+ colorScheme,
329
+ doc,
330
+ isActive,
331
+ isInserted,
332
+ leftAnchorId,
333
+ shouldHideDeleted
334
+ }));
335
+ });
336
+ }
337
+ return decorations;
338
+ });
@@ -0,0 +1,19 @@
1
+ import { getDiffableAttrNames } from './diffableAttrs';
2
+
3
+ /**
4
+ * A cell background as it renders: absent, empty and `"null"` — the string agent payloads send for
5
+ * no background — all mean the cell has none.
6
+ */
7
+ export const normalizeCellBackground = background => typeof background === 'string' && background !== '' && background !== 'null' ? background : null;
8
+
9
+ /** Whether two cells render different backgrounds. */
10
+ export const isCellBackgroundChanged = (left, right) => normalizeCellBackground(left.attrs.background) !== normalizeCellBackground(right.attrs.background);
11
+
12
+ /**
13
+ * Whether two cells differ by an attribute that represents a change, such as their background or
14
+ * span, as opposed to only their content.
15
+ */
16
+ export const isCellAttrChanged = (left, right) => {
17
+ var _getDiffableAttrNames;
18
+ return ((_getDiffableAttrNames = getDiffableAttrNames(left.type.name, left.attrs)) !== null && _getDiffableAttrNames !== void 0 ? _getDiffableAttrNames : []).some(name => name === 'background' ? isCellBackgroundChanged(left, right) : left.attrs[name] !== right.attrs[name]);
19
+ };
@@ -1,12 +1,14 @@
1
+ import { PanelSharedCssClassName } from '@atlaskit/editor-common/styles/panel';
2
+ import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
1
3
  /**
2
4
  * Node types that can safely be reduced to an empty shell.
3
5
  *
4
6
  * A shell only replicates a margin if it collapses to nothing itself, which needs zero height and
5
- * no vertical border or padding. Textblocks and lists qualify; nodes with intrinsic sizing (tables
6
- * are `display: table`, media has a measured height) or a painted box (a zero-height panel is a
7
- * visible stripe of background) do not, and are left alone.
7
+ * no vertical border or padding. Textblocks and lists qualify; panels need their painted box
8
+ * hidden and their flex layout removed. Nodes with intrinsic sizing (tables are `display: table`,
9
+ * media has a measured height) are left alone.
8
10
  */
9
- const isShapeableNode = node => node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name);
11
+ const isShapeableNode = node => node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name) || getBaseNodeTypeName(node.type) === 'panel';
10
12
 
11
13
  /**
12
14
  * An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
@@ -50,6 +52,13 @@ export const createNodeShapedMarginSpacer = ({
50
52
  serialized.dataset.testid = testId;
51
53
  serialized.setAttribute('aria-hidden', 'true');
52
54
  serialized.contentEditable = 'false';
55
+ if (getBaseNodeTypeName(node.type) === 'panel') {
56
+ // Schema serialization lacks the node view's class, which carries the panel's margin.
57
+ // Hide the painted shell and allow its margins to collapse through a zero-height block.
58
+ serialized.classList.add(PanelSharedCssClassName.prefix);
59
+ serialized.style.display = 'block';
60
+ serialized.style.visibility = 'hidden';
61
+ }
53
62
 
54
63
  // Zero height with no vertical border or padding keeps the spacer self-collapsing: its margins
55
64
  // stay adjoining, so they collapse with the neighbours into a single margin equal to the largest
@@ -9,6 +9,7 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
9
9
  import isEqual from 'lodash/isEqual';
10
10
  import memoizeOne from 'memoize-one';
11
11
  import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
12
+ import { isAiXstateMigrationEnabledNoExposure } from '@atlaskit/editor-common/ai-xstate-migration-no-exposure';
12
13
  import { areDocsEqualByBlockStructureAndText } from '@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText';
13
14
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
14
15
  import { Mapping } from '@atlaskit/editor-prosemirror/transform';
@@ -16,12 +17,12 @@ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
16
17
  import { TableMap } from '@atlaskit/editor-tables/table-map';
17
18
  import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
18
19
  import { fg } from '@atlaskit/platform-feature-flags/fg';
19
- import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
20
20
  import { createAttributionColorMap, getAttributionKey, getAttributionKeyForChange, getColorSchemeForChange, isAttributionColoringEnabled, isContributorTagsEnabled } from '../decorations/colorSchemes/attributions';
21
21
  import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorationWidgets';
22
22
  import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
23
23
  import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
24
24
  import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
25
+ import { createReplacedCellDecorations, getReplacedCells } from '../decorations/createReplacedCellDecorations';
25
26
  import { extractDiffDescriptors } from '../decorations/decorationKeys';
26
27
  import { extractContributorTags } from '../decorations/extractContributorTags';
27
28
  import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
@@ -596,7 +597,11 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
596
597
  // suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
597
598
  // asked to compare against the original, and the deleted side is the only place the
598
599
  // previous formatting is visible.
599
- var isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && change.deleted.length > 0 && isMarkOnlyChange({
600
+ //
601
+ // Not on an inverted diff: there the widget's side is the suggested content, which the clean
602
+ // view keeps. Suppressing it hides the suggestion along with the displayed side it replaces,
603
+ // e.g. a cell whose only change is its background, as the signature ignores attrs.
604
+ var isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && !(isInverted && fg('platform_editor_ai_show_diff_patch_2')) && change.deleted.length > 0 && isMarkOnlyChange({
600
605
  change: change,
601
606
  originalDoc: originalDoc,
602
607
  newDoc: tr.doc
@@ -672,7 +677,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
672
677
  // For a large inserted table, skip the per-cell inline decorations that
673
678
  // freeze the browser on re-render. Only active when Review moment is
674
679
  // eligible (xstate + M1).
675
- var useCoarseTableDecoration = isSmartNodeLevel && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true && isLargeInsertedTableRange(tr.doc, change.fromB, change.toB);
680
+ var useCoarseTableDecoration = isSmartNodeLevel && isAiXstateMigrationEnabledNoExposure() && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true && isLargeInsertedTableRange(tr.doc, change.fromB, change.toB);
676
681
  // Whether the deleted-content widget will actually be rendered for this
677
682
  // change. Used to decide if indicator anchor positions should be adjusted
678
683
  // inward — when the widget is present the anchor must stay at the block
@@ -682,7 +687,35 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
682
687
  hideDeletedDiffs: hideDeletedDiffs,
683
688
  isInverted: isInverted
684
689
  });
685
- if (isSmartNodeLevel) {
690
+
691
+ // A whole-cell replacement keeps its cells in place, so it takes its own decorations
692
+ // instead of the generic inline and block ones.
693
+ var replacedCells = fg('platform_editor_ai_show_diff_patch_2') ? getReplacedCells({
694
+ slice: originalDoc.slice(change.fromA, change.toA),
695
+ doc: tr.doc,
696
+ fromB: change.fromB,
697
+ toB: change.toB
698
+ }) : [];
699
+ if (replacedCells.length > 0) {
700
+ decorations.push.apply(decorations, _toConsumableArray(createReplacedCellDecorations({
701
+ replacedCells: replacedCells,
702
+ change: change,
703
+ doc: tr.doc,
704
+ attributionKey: attributionKey,
705
+ colorScheme: changeColorScheme,
706
+ hasDeletedWidget: willRenderDeletedWidget,
707
+ hideAddedDiffsUnderline: hideAddedDiffsUnderline,
708
+ isActive: isActive,
709
+ isDeletedWidgetBelow: isDeletedWidgetBelow,
710
+ isInserted: isInserted,
711
+ leftAnchorId: leftAnchorId,
712
+ reveal: reveal,
713
+ shouldHideDeleted: shouldHideDeleted,
714
+ showContributorTags: showContributorTags,
715
+ showIndicators: showIndicators,
716
+ tagMountContext: tagMountContext
717
+ })));
718
+ } else if (isSmartNodeLevel) {
686
719
  // For a large inserted table, skip the O(cells) inline decorations inside the
687
720
  // table (the cell overlays added below provide the highlight without the
688
721
  // expensive content re-render); leaf blocks outside the table still get theirs.
@@ -742,7 +775,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
742
775
  tagMountContext: tagMountContext
743
776
  })));
744
777
  }
745
- decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
778
+ decorations.push.apply(decorations, _toConsumableArray(replacedCells.length > 0 ? [] : calculateNodesForBlockDecoration({
746
779
  // A deleted widget placed below anchors at the change's end, inside the very block
747
780
  // this would let the decoration claim.
748
781
  allowTrailingCloseTokens: !isDeletedWidgetBelow,
@@ -932,7 +965,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
932
965
  })));
933
966
  // If the original node position is known (e.g. a panel type change), also render
934
967
  // the old block node as a "deleted" widget so the reviewer sees the before/after.
935
- var isSmartNodeLevelAttrChange = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true;
968
+ var isSmartNodeLevelAttrChange = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && isAiXstateMigrationEnabledNoExposure() && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true;
936
969
  if (isSmartNodeLevelAttrChange && change.fromA !== undefined && change.toA !== undefined) {
937
970
  var placeBelow = deletedDiffPlacement === 'bottom';
938
971
  decorations.push.apply(decorations, _toConsumableArray(createNodeChangedDecorationWidget({