@atlaskit/editor-plugin-show-diff 14.3.2 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/afm-cc/tsconfig.json +18 -0
  3. package/afm-products/tsconfig.json +18 -0
  4. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +47 -26
  5. package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +61 -9
  6. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +77 -20
  7. package/dist/cjs/pm-plugins/decorations/colorSchemes/factory.js +10 -0
  8. package/dist/cjs/pm-plugins/decorations/colorSchemes/types.js +2 -0
  9. package/dist/cjs/pm-plugins/decorations/createAnchorDecorationWidgets.js +52 -7
  10. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +172 -0
  11. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +37 -5
  12. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +40 -7
  13. package/dist/cjs/pm-plugins/decorations/decorationKeys.js +31 -5
  14. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +128 -0
  15. package/dist/cjs/pm-plugins/main.js +57 -6
  16. package/dist/cjs/pm-plugins/resolveDiffContributors.js +143 -0
  17. package/dist/cjs/showDiffPlugin.js +21 -6
  18. package/dist/cjs/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  19. package/dist/cjs/ui/ContributorTag/ContributorTag.js +383 -0
  20. package/dist/cjs/ui/ContributorTag/ContributorTagWidget.js +50 -0
  21. package/dist/cjs/ui/ContributorTag/messages.js +24 -0
  22. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +48 -27
  23. package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +39 -0
  24. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +53 -11
  25. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +9 -0
  26. package/dist/es2019/pm-plugins/decorations/colorSchemes/types.js +2 -0
  27. package/dist/es2019/pm-plugins/decorations/createAnchorDecorationWidgets.js +43 -1
  28. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +163 -0
  29. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +38 -5
  30. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +38 -6
  31. package/dist/es2019/pm-plugins/decorations/decorationKeys.js +30 -2
  32. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +101 -0
  33. package/dist/es2019/pm-plugins/main.js +52 -3
  34. package/dist/es2019/pm-plugins/resolveDiffContributors.js +128 -0
  35. package/dist/es2019/showDiffPlugin.js +20 -5
  36. package/dist/es2019/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  37. package/dist/es2019/ui/ContributorTag/ContributorTag.js +312 -0
  38. package/dist/es2019/ui/ContributorTag/ContributorTagWidget.js +43 -0
  39. package/dist/es2019/ui/ContributorTag/messages.js +18 -0
  40. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +49 -28
  41. package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +60 -8
  42. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +77 -19
  43. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +9 -0
  44. package/dist/esm/pm-plugins/decorations/colorSchemes/types.js +2 -0
  45. package/dist/esm/pm-plugins/decorations/createAnchorDecorationWidgets.js +51 -6
  46. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +166 -0
  47. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +37 -5
  48. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +40 -7
  49. package/dist/esm/pm-plugins/decorations/decorationKeys.js +30 -5
  50. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +122 -0
  51. package/dist/esm/pm-plugins/main.js +57 -6
  52. package/dist/esm/pm-plugins/resolveDiffContributors.js +137 -0
  53. package/dist/esm/showDiffPlugin.js +21 -6
  54. package/dist/esm/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  55. package/dist/esm/ui/ContributorTag/ContributorTag.js +375 -0
  56. package/dist/esm/ui/ContributorTag/ContributorTagWidget.js +43 -0
  57. package/dist/esm/ui/ContributorTag/messages.js +18 -0
  58. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  59. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +7 -1
  60. package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +10 -0
  61. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +24 -2
  62. package/dist/types/pm-plugins/decorations/colorSchemes/factory.d.ts +7 -1
  63. package/dist/types/pm-plugins/decorations/colorSchemes/schemes.d.ts +1 -2
  64. package/dist/types/pm-plugins/decorations/colorSchemes/types.d.ts +2 -1
  65. package/dist/types/pm-plugins/decorations/createAnchorDecorationWidgets.d.ts +14 -0
  66. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +55 -0
  67. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +5 -1
  68. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +5 -1
  69. package/dist/types/pm-plugins/decorations/decorationKeys.d.ts +28 -3
  70. package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +9 -0
  71. package/dist/types/pm-plugins/main.d.ts +8 -2
  72. package/dist/types/pm-plugins/resolveDiffContributors.d.ts +7 -0
  73. package/dist/types/showDiffPluginType.d.ts +65 -2
  74. package/dist/types/ui/ContributorTag/ContributorTag.d.ts +37 -0
  75. package/dist/types/ui/ContributorTag/ContributorTagWidget.d.ts +22 -0
  76. package/dist/types/ui/ContributorTag/messages.d.ts +6 -0
  77. package/package.json +16 -7
@@ -5,6 +5,7 @@ import { isExtendedEnabled } from '../isExtendedEnabled';
5
5
  import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
6
6
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
7
7
  import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
8
+ import { createContributorTagWidget } from './createContributorTagWidget';
8
9
  import { getAtomicInlineNodeClassNameLegacy, resolveInlineChangedStyleLegacy } from './createInlineChangedDecoration.styles.legacy';
9
10
  import { buildDiffDecorationSpec } from './decorationKeys';
10
11
  const displayNoneStyle = convertToInlineCss({
@@ -92,28 +93,38 @@ export const getAtomicInlineChangedAttrs = (inlineNodeName, colorScheme) => reso
92
93
  */
93
94
 
94
95
  export const createInlineChangedDecoration = ({
96
+ attributionKey,
95
97
  change,
96
98
  colorScheme,
97
99
  isActive = false,
98
100
  isInserted = true,
99
101
  isAtomicInlineNode = false,
100
102
  shouldHideDeleted = false,
103
+ showContributorTags = false,
101
104
  showIndicators = false,
102
105
  doc,
103
106
  diffType,
104
107
  hideAddedDiffsUnderline = false,
105
108
  inlineNodeName,
106
109
  hasDeletedWidget = false,
107
- isDeletedWidgetBelow = false
110
+ isDeletedWidgetBelow = false,
111
+ tagMountContext
108
112
  }) => {
109
- const diffId = crypto.randomUUID();
113
+ // Derived from the range so it survives a recalculation: stepping to the next/previous change
114
+ // rebuilds decorations without touching the doc, and a fresh id would change the React key and the
115
+ // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
116
+ const diffId = showContributorTags ? `inline-${change.fromB}-${change.toB}` : crypto.randomUUID();
110
117
  if (shouldHideDeleted) {
111
118
  return [Decoration.inline(change.fromB, change.toB, {
112
119
  style: displayNoneStyle
113
120
  }, buildDiffDecorationSpec({
121
+ // No attribution: this decoration paints nothing and emits no tag host, so a tag
122
+ // resolved from it would have nowhere to render.
123
+ colorScheme,
114
124
  decorationType: 'inline',
115
125
  diffId,
116
- isActive
126
+ isActive,
127
+ isInserted
117
128
  }))];
118
129
  }
119
130
  const colors = getColorScheme(colorScheme);
@@ -132,11 +143,18 @@ export const createInlineChangedDecoration = ({
132
143
  ...(atomicInlineAttrs && {
133
144
  class: atomicInlineAttrs.className
134
145
  }),
135
- 'data-testid': 'show-diff-changed-decoration'
146
+ 'data-testid': 'show-diff-changed-decoration',
147
+ // Lets the contributor tag find its highlight on hover.
148
+ ...(showContributorTags && {
149
+ 'data-diff-id': diffId
150
+ })
136
151
  }, buildDiffDecorationSpec({
152
+ attributionKey,
153
+ colorScheme,
137
154
  decorationType: 'inline',
138
155
  diffId,
139
- isActive
156
+ isActive,
157
+ isInserted
140
158
  }))];
141
159
  if (showIndicators && doc && isExtendedEnabled(diffType)) {
142
160
  // For paragraphs, fromB/toB land on the outer block
@@ -173,5 +191,20 @@ export const createInlineChangedDecoration = ({
173
191
  diffId
174
192
  }));
175
193
  }
194
+
195
+ // Built from the raw change range, not the anchors above: a tag lines up with the changed text,
196
+ // while the bar's anchors are held on a block boundary when a deleted widget renders beside them.
197
+ if (showContributorTags && doc && isExtendedEnabled(diffType)) {
198
+ const tagWidget = createContributorTagWidget({
199
+ doc,
200
+ from: change.fromB,
201
+ to: change.toB,
202
+ diffId,
203
+ mountContext: tagMountContext
204
+ });
205
+ if (tagWidget) {
206
+ decorations.push(tagWidget);
207
+ }
208
+ }
176
209
  return decorations;
177
210
  };
@@ -3,6 +3,7 @@ import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { isExtendedEnabled } from '../isExtendedEnabled';
4
4
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
5
5
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
6
+ import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
6
7
  import { buildDiffDecorationSpec, buildAnchorDecorationKey } from './decorationKeys';
7
8
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
8
9
  import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
@@ -89,6 +90,7 @@ const createTableCellContentWidgets = ({
89
90
  * that is not in the current document.
90
91
  */
91
92
  export const createNodeChangedDecorationWidget = ({
93
+ attributionKey,
92
94
  change,
93
95
  doc,
94
96
  nodeViewSerializer,
@@ -98,13 +100,15 @@ export const createNodeChangedDecorationWidget = ({
98
100
  activeIndexPos,
99
101
  // This is false by default as this is generally used to show deleted content
100
102
  isInserted = false,
103
+ showContributorTags = false,
101
104
  showIndicators = false,
102
105
  // When true, render the deleted content *after* (below) the new content instead of
103
106
  // before it. Used for `smart` node-level changes so the deleted node appears beneath
104
107
  // its replacement (gray + strikethrough).
105
108
  placeBelow = false,
106
109
  diffType,
107
- hideAddedDiffsUnderline = false
110
+ hideAddedDiffsUnderline = false,
111
+ tagMountContext
108
112
  }) => {
109
113
  var _slice$content, _slice$content2, _slice$content2$first, _slice$content3, _slice$content3$first, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
110
114
  const slice = doc.slice(change.fromA, change.toA);
@@ -196,7 +200,9 @@ export const createNodeChangedDecorationWidget = ({
196
200
  subtree: true
197
201
  });
198
202
  }
199
- const diffId = crypto.randomUUID();
203
+ // Derived from the deleted range so it survives a recalculation — see the same reasoning in
204
+ // `createInlineChangedDecoration`.
205
+ const diffId = showContributorTags ? `widget-${change.fromA}-${change.toA}` : crypto.randomUUID();
200
206
  const decorations = [];
201
207
  const replacementNode = newDoc.nodeAt(change.fromB);
202
208
  const firstReplacedNode = slice.content.firstChild;
@@ -313,24 +319,48 @@ export const createNodeChangedDecorationWidget = ({
313
319
  }
314
320
  });
315
321
  dom.setAttribute('data-testid', 'show-diff-deleted-decoration');
322
+
323
+ // Needed even when the indicator bar is off, because a contributor tag also anchors against the
324
+ // widget.
325
+ if ((showIndicators || showContributorTags) && isExtendedEnabled(diffType)) {
326
+ dom.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
327
+ diffId
328
+ })}`);
329
+ }
330
+
331
+ // Taken down in the widget's `destroy` below: `dom` is rebuilt on every recalculation, so the tag
332
+ // mounted into it must not outlive the copy ProseMirror is drawing.
333
+ let tagMount;
334
+ if (showContributorTags) {
335
+ // Lets the contributor tag find the deleted content on hover.
336
+ dom.setAttribute('data-diff-id', diffId);
337
+
338
+ // Hosted at the start of the deleted run rather than on `dom` itself: `dom` is inline, so once
339
+ // the deleted content wraps its box is the union of its line fragments.
340
+ const tagHost = createContributorTagHost(diffId, tagMountContext);
341
+ if (tagHost) {
342
+ dom.prepend(tagHost.host);
343
+ tagMount = tagHost.mount;
344
+ }
345
+ }
316
346
  if (showIndicators && isExtendedEnabled(diffType)) {
317
347
  const leftAnchor = createLeftAnchorWidget({
318
348
  doc: newDoc,
319
349
  from: safeInsertPos,
320
350
  diffId
321
351
  });
322
- dom.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
323
- diffId
324
- })}`);
325
352
  if (leftAnchor) {
326
353
  decorations.push(leftAnchor);
327
354
  }
328
355
  }
329
356
  decorations.push(Decoration.widget(safeInsertPos, dom, {
330
357
  ...buildDiffDecorationSpec({
358
+ attributionKey,
359
+ colorScheme,
331
360
  decorationType: 'widget',
332
361
  diffId,
333
362
  isActive,
363
+ isInserted,
334
364
  diffType,
335
365
  ...(isExtendedEnabled(diffType) && {
336
366
  // placeBelow anchors at the end of the new content, so render on the
@@ -347,7 +377,9 @@ export const createNodeChangedDecorationWidget = ({
347
377
  }),
348
378
  destroy: () => {
349
379
  var _constrainMediaObserv2;
350
- return (_constrainMediaObserv2 = constrainMediaObserver) === null || _constrainMediaObserv2 === void 0 ? void 0 : _constrainMediaObserv2.disconnect();
380
+ (_constrainMediaObserv2 = constrainMediaObserver) === null || _constrainMediaObserv2 === void 0 ? void 0 : _constrainMediaObserv2.disconnect();
381
+ unmountContributorTag(tagMount);
382
+ tagMount = undefined;
351
383
  }
352
384
  }));
353
385
 
@@ -1,12 +1,12 @@
1
1
  import { isExtendedEnabled } from '../isExtendedEnabled';
2
-
3
2
  /**
4
3
  * Decoration families produced by the show-diff plugin.
5
4
  * Each family owns its own key space and spec shape.
6
5
  */
7
6
  export const DecorationFamily = {
8
7
  diff: 'diff',
9
- anchor: 'anchor'
8
+ anchor: 'anchor',
9
+ contributorTag: 'contributor-tag'
10
10
  };
11
11
  export const AnchorTypeKey = {
12
12
  from: 'from',
@@ -14,6 +14,12 @@ export const AnchorTypeKey = {
14
14
  left: 'left',
15
15
  docMargin: 'doc-margin'
16
16
  };
17
+
18
+ /**
19
+ * The host widget one contributor tag renders into. Its own family, so the tag never reaches the
20
+ * consumers that read diff decorations — change counting, navigation and `getDeletedWidgets`.
21
+ */
22
+
17
23
  /**
18
24
  * The diff-decoration kinds produced by the show-diff plugin. Each value is
19
25
  * the leading segment of the generated key, so a diff decoration's kind can be
@@ -49,9 +55,12 @@ export const buildDiffDecorationKey = ({
49
55
  return isActive !== undefined ? `${decorationKeyPrefix}-${isActive ? 'active' : 'inactive'}` : decorationKeyPrefix;
50
56
  };
51
57
  export const buildDiffDecorationSpec = ({
58
+ attributionKey,
59
+ colorScheme,
52
60
  decorationType,
53
61
  diffId,
54
62
  isActive,
63
+ isInserted,
55
64
  nodeName,
56
65
  side,
57
66
  diffType
@@ -65,6 +74,19 @@ export const buildDiffDecorationSpec = ({
65
74
  isActive,
66
75
  diffType
67
76
  }),
77
+ ...(attributionKey ? {
78
+ attributionKey
79
+ } : {}),
80
+ // Only alongside an attribution key, so the spec shape is unchanged when the gate is off.
81
+ ...(attributionKey && isActive !== undefined ? {
82
+ isActive
83
+ } : {}),
84
+ ...(attributionKey && isInserted !== undefined ? {
85
+ isInserted
86
+ } : {}),
87
+ ...(attributionKey && colorScheme ? {
88
+ colorScheme
89
+ } : {}),
68
90
  ...(nodeName ? {
69
91
  nodeName
70
92
  } : {}),
@@ -103,6 +125,12 @@ export function buildAnchorDecorationSpec({
103
125
  } : {})
104
126
  };
105
127
  }
128
+ export const buildContributorTagDecorationSpec = diffId => ({
129
+ decorationFamily: DecorationFamily.contributorTag,
130
+ diffId,
131
+ // Stable across recalculations, so ProseMirror keeps the host element the tag is portalled into.
132
+ key: `${DecorationFamily.contributorTag}-${diffId}`
133
+ });
106
134
  export const isDiffDecorationSpec = spec => Boolean(spec && typeof spec === 'object' && 'decorationFamily' in spec && spec.decorationFamily === DecorationFamily.diff);
107
135
  export const isAnchorDecorationSpec = spec => Boolean(spec && typeof spec === 'object' && 'decorationFamily' in spec && spec.decorationFamily === DecorationFamily.anchor);
108
136
  export const isDiffDecoration = decoration => isDiffDecorationSpec(decoration.spec);
@@ -0,0 +1,101 @@
1
+ import { isDiffDecoration, isDiffDecorationSpec } from './decorationKeys';
2
+
3
+ /**
4
+ * Diff kinds that expose an anchor a tag can be pinned to: `inline` is added/changed content,
5
+ * `widget` is deleted content. Block diffs carry no attribution.
6
+ */
7
+ const TAGGABLE_DECORATION_TYPES = new Set(['inline', 'widget']);
8
+
9
+ /** The connection is an internal key, so it is dropped on the way to the tag. */
10
+ const toTagContributor = ({
11
+ connectedToKey: _connectedToKey,
12
+ ...tagContributor
13
+ }) => tagContributor;
14
+ /**
15
+ * Whether a deleted-content widget belongs to the same change as an inline decoration. A replacement
16
+ * renders both — the inserted text inline, the deleted text as a widget anchored to one end of that
17
+ * inline range — and the two must not each claim a tag.
18
+ */
19
+ const isSameChange = (widget, inline) => widget.spec.attributionKey === inline.spec.attributionKey && widget.from >= inline.from && widget.from <= inline.to;
20
+
21
+ /**
22
+ * One model per diff decoration whose attribution resolves to a supplied contributor; anything
23
+ * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
24
+ * into the single inline tag — see `isSameChange`.
25
+ */
26
+ export const extractContributorTags = (decorations, contributors) => {
27
+ if (!contributors) {
28
+ return [];
29
+ }
30
+ const targets = [];
31
+ for (const decoration of decorations.find(undefined, undefined, isDiffDecorationSpec)) {
32
+ if (!isDiffDecoration(decoration)) {
33
+ continue;
34
+ }
35
+ const {
36
+ spec
37
+ } = decoration;
38
+ if (!spec.attributionKey || !TAGGABLE_DECORATION_TYPES.has(spec.decorationType)) {
39
+ continue;
40
+ }
41
+ const contributor = contributors[spec.attributionKey];
42
+ if (!contributor) {
43
+ continue;
44
+ }
45
+ targets.push({
46
+ contributor,
47
+ decoration
48
+ });
49
+ }
50
+
51
+ // The inline decoration keeps the tag: it captions content still in the document.
52
+ const inlineTargets = targets.filter(({
53
+ decoration
54
+ }) => decoration.spec.decorationType === 'inline');
55
+ const linkedDiffIds = new Map();
56
+ const folded = new Set();
57
+ for (const target of targets) {
58
+ var _linkedDiffIds$get;
59
+ if (target.decoration.spec.decorationType !== 'widget') {
60
+ continue;
61
+ }
62
+ const host = inlineTargets.find(({
63
+ decoration
64
+ }) => isSameChange(target.decoration, decoration));
65
+ if (!host) {
66
+ continue;
67
+ }
68
+ folded.add(target);
69
+ const hostDiffId = host.decoration.spec.diffId;
70
+ linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), target.decoration.spec.diffId]);
71
+ }
72
+ return targets.filter(target => !folded.has(target)).map(({
73
+ contributor,
74
+ decoration: {
75
+ spec
76
+ }
77
+ }) => {
78
+ const connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
79
+ const connectedContributor = connected ? toTagContributor(connected) : undefined;
80
+ const linked = linkedDiffIds.get(spec.diffId);
81
+ return {
82
+ contributor: toTagContributor(contributor),
83
+ diffId: spec.diffId,
84
+ ...(connectedContributor ? {
85
+ connectedContributor
86
+ } : {}),
87
+ ...(spec.colorScheme ? {
88
+ colorScheme: spec.colorScheme
89
+ } : {}),
90
+ ...(spec.isActive !== undefined ? {
91
+ isActive: spec.isActive
92
+ } : {}),
93
+ ...(spec.isInserted !== undefined ? {
94
+ isInserted: spec.isInserted
95
+ } : {}),
96
+ ...(linked ? {
97
+ linkedDiffIds: linked
98
+ } : {})
99
+ };
100
+ });
101
+ };
@@ -5,13 +5,31 @@ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
5
5
  import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform-override';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
7
  import { calculateDiffDecorations } from './calculateDiff/calculateDiffDecorations';
8
+ import { isDiffDecoration, isDiffDecorationSpec } from './decorations/decorationKeys';
8
9
  import { enforceCustomStepRegisters } from './enforceCustomStepRegisters';
9
10
  import { getScrollableDecorations } from './getScrollableDecorations';
10
11
  import { getDefaultDiffType, isExtendedEnabled } from './isExtendedEnabled';
11
12
  import { NodeViewSerializer } from './NodeViewSerializer';
12
13
  import { scrollToDecoration } from './scrollToDiff';
13
14
  export const showDiffPluginKey = new PluginKey('showDiffPlugin');
14
- export const createPlugin = (config, getIntl, api, onEditorView) => {
15
+ /**
16
+ * Tags whose diff decoration survived the transaction. A document change can map a decoration away
17
+ * without any `showDiff`/`hideDiff` following it, and a tag left behind has no host to render into.
18
+ * Returns the given array when every tag is still live.
19
+ */
20
+ const dropOrphanedContributorTags = (contributorTags, decorations) => {
21
+ if (!(contributorTags !== null && contributorTags !== void 0 && contributorTags.length)) {
22
+ return contributorTags;
23
+ }
24
+ const liveDiffIds = new Set(decorations.find(undefined, undefined, isDiffDecorationSpec).filter(isDiffDecoration).map(({
25
+ spec
26
+ }) => spec.diffId));
27
+ const liveTags = contributorTags.filter(({
28
+ diffId
29
+ }) => liveDiffIds.has(diffId));
30
+ return liveTags.length === contributorTags.length ? contributorTags : liveTags;
31
+ };
32
+ export const createPlugin = (config, getIntl, api, portalProviderAPI, onEditorView) => {
15
33
  enforceCustomStepRegisters();
16
34
  const nodeViewSerializer = new NodeViewSerializer({});
17
35
  const setNodeViewSerializer = editorView => {
@@ -19,6 +37,23 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
19
37
  editorView
20
38
  });
21
39
  };
40
+
41
+ /**
42
+ * This editor's own content root. The contributor tags scope every DOM lookup to it, so they take
43
+ * it from the view the plugin was mounted on rather than resolving `.ProseMirror` by class name
44
+ * from a decoration — a class this package does not own, and one whose nearest match is the wrong
45
+ * editor whenever editors are nested. A getter because the view arrives with the ProseMirror view
46
+ * lifecycle, which decorations are calculated independently of.
47
+ */
48
+ let currentEditorView;
49
+ const tagMountContext = portalProviderAPI ? {
50
+ api,
51
+ getEditorRoot: () => {
52
+ var _currentEditorView$do, _currentEditorView;
53
+ return (_currentEditorView$do = (_currentEditorView = currentEditorView) === null || _currentEditorView === void 0 ? void 0 : _currentEditorView.dom) !== null && _currentEditorView$do !== void 0 ? _currentEditorView$do : null;
54
+ },
55
+ portalProviderAPI
56
+ } : undefined;
22
57
  return new SafePlugin({
23
58
  key: showDiffPluginKey,
24
59
  state: {
@@ -56,6 +91,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
56
91
  };
57
92
  // Calculate and store decorations in state
58
93
  const {
94
+ contributorTags,
59
95
  decorations,
60
96
  diffDescriptors
61
97
  } = calculateDiffDecorations({
@@ -66,6 +102,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
66
102
  intl: getIntl(),
67
103
  activeIndexPos: newPluginState.activeIndexPos,
68
104
  api,
105
+ tagMountContext,
69
106
  ...(isExtendedEnabled((_newPluginState = newPluginState) === null || _newPluginState === void 0 ? void 0 : _newPluginState.diffType) ? {
70
107
  isInverted: (_newPluginState2 = newPluginState) === null || _newPluginState2 === void 0 ? void 0 : _newPluginState2.isInverted,
71
108
  diffType: (_newPluginState3 = newPluginState) === null || _newPluginState3 === void 0 ? void 0 : _newPluginState3.diffType,
@@ -79,6 +116,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
79
116
  });
80
117
  // Update the decorations and their ids
81
118
  newPluginState.decorations = decorations;
119
+ newPluginState.contributorTags = contributorTags;
82
120
  if (isExtendedEnabled((_newPluginState0 = newPluginState) === null || _newPluginState0 === void 0 ? void 0 : _newPluginState0.diffType)) {
83
121
  newPluginState.diffDescriptors = diffDescriptors;
84
122
  }
@@ -89,6 +127,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
89
127
  decorations: DecorationSet.empty,
90
128
  isDisplayingChanges: false,
91
129
  activeIndex: undefined,
130
+ contributorTags: [],
92
131
  /**
93
132
  * Reset isInverted & diffType state when hiding diffs
94
133
  * Otherwise this should persist for the diff-showing session
@@ -132,6 +171,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
132
171
  };
133
172
  // Recalculate decorations with the new active index
134
173
  const {
174
+ contributorTags: updatedContributorTags,
135
175
  decorations: updatedDecorations,
136
176
  diffDescriptors: updatedDiffDescriptors
137
177
  } = calculateDiffDecorations({
@@ -142,6 +182,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
142
182
  intl: getIntl(),
143
183
  activeIndexPos: newPluginState.activeIndexPos,
144
184
  api,
185
+ tagMountContext,
145
186
  ...(isExtendedEnabled(newPluginState.diffType) ? {
146
187
  isInverted: newPluginState.isInverted,
147
188
  diffType: newPluginState.diffType,
@@ -154,6 +195,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
154
195
  } : {})
155
196
  });
156
197
  newPluginState.decorations = updatedDecorations;
198
+ newPluginState.contributorTags = updatedContributorTags;
157
199
  if (isExtendedEnabled(newPluginState.diffType)) {
158
200
  newPluginState.diffDescriptors = updatedDiffDescriptors;
159
201
  }
@@ -165,14 +207,20 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
165
207
  };
166
208
  }
167
209
  }
168
- return {
210
+ const mappedDecorations = newPluginState.decorations.map(tr.mapping, tr.doc);
211
+ const nextState = {
169
212
  ...newPluginState,
170
- decorations: newPluginState.decorations.map(tr.mapping, tr.doc)
213
+ decorations: mappedDecorations
171
214
  };
215
+ if (tr.docChanged) {
216
+ nextState.contributorTags = dropOrphanedContributorTags(newPluginState.contributorTags, mappedDecorations);
217
+ }
218
+ return nextState;
172
219
  }
173
220
  },
174
221
  view(editorView) {
175
222
  setNodeViewSerializer(editorView);
223
+ currentEditorView = editorView;
176
224
  onEditorView === null || onEditorView === void 0 ? void 0 : onEditorView(editorView);
177
225
  let isFirst = true;
178
226
  let previousActiveIndex;
@@ -224,6 +272,7 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
224
272
  var _cancelPendingScrollT3;
225
273
  (_cancelPendingScrollT3 = cancelPendingScrollToDecoration) === null || _cancelPendingScrollT3 === void 0 ? void 0 : _cancelPendingScrollT3();
226
274
  cancelPendingScrollToDecoration = null;
275
+ currentEditorView = undefined;
227
276
  onEditorView === null || onEditorView === void 0 ? void 0 : onEditorView(undefined);
228
277
  }
229
278
  };
@@ -0,0 +1,128 @@
1
+ import { getAttributionKey } from './decorations/colorSchemes/attributions';
2
+
3
+ /** A brand name, so a literal rather than a translated message. */
4
+ const ROVO_AGENT_NAME = 'Rovo';
5
+
6
+ /** `agentType` values that render as "Rovo" rather than as a generic external agent. */
7
+ const ROVO_AGENT_TYPES = new Set(['convo-ai', 'rovo_chat']);
8
+ const hasAgentIdentity = attribution => {
9
+ var _attribution$agentId, _attribution$agentTyp;
10
+ return Boolean(((_attribution$agentId = attribution.agentId) === null || _attribution$agentId === void 0 ? void 0 : _attribution$agentId.trim()) || ((_attribution$agentTyp = attribution.agentType) === null || _attribution$agentTyp === void 0 ? void 0 : _attribution$agentTyp.trim()));
11
+ };
12
+ const toProfilesByAccountId = profiles => {
13
+ const profilesByAccountId = new Map();
14
+ profiles === null || profiles === void 0 ? void 0 : profiles.forEach(profile => {
15
+ // Trimmed to match how attribution ids are keyed, so the two sides compare like for like.
16
+ const accountId = profile.accountId.trim();
17
+ if (accountId && !profilesByAccountId.has(accountId)) {
18
+ profilesByAccountId.set(accountId, profile);
19
+ }
20
+ });
21
+ return profilesByAccountId;
22
+ };
23
+ const resolveUser = (userId, profilesByAccountId) => {
24
+ const profile = profilesByAccountId.get(userId);
25
+ return profile ? {
26
+ avatarUrl: profile.avatarUrl,
27
+ kind: 'user',
28
+ name: profile.name
29
+ } : undefined;
30
+ };
31
+
32
+ /** An agent always resolves: its profile, else "Rovo" for a first-party type, else external. */
33
+ const resolveAgent = (attribution, profilesByAccountId) => {
34
+ var _attribution$agentId2, _attribution$agentTyp2;
35
+ const agentId = (_attribution$agentId2 = attribution.agentId) === null || _attribution$agentId2 === void 0 ? void 0 : _attribution$agentId2.trim();
36
+ const agentProfile = agentId ? profilesByAccountId.get(agentId) : undefined;
37
+ if (agentProfile) {
38
+ return {
39
+ agentKind: 'identified',
40
+ avatarUrl: agentProfile.avatarUrl,
41
+ kind: 'agent',
42
+ name: agentProfile.name
43
+ };
44
+ }
45
+ const agentType = (_attribution$agentTyp2 = attribution.agentType) === null || _attribution$agentTyp2 === void 0 ? void 0 : _attribution$agentTyp2.trim().toLowerCase();
46
+
47
+ // An empty name lets the tag fall back to its own localised "External agent" label.
48
+ return agentType && ROVO_AGENT_TYPES.has(agentType) ? {
49
+ agentKind: 'rovo',
50
+ kind: 'agent',
51
+ name: ROVO_AGENT_NAME
52
+ } : {
53
+ agentKind: 'external',
54
+ kind: 'agent',
55
+ name: ''
56
+ };
57
+ };
58
+
59
+ /**
60
+ * One contributor per distinct actor behind the attributed steps, resolved against the identities
61
+ * the consumer supplied. Returns `undefined` if any actor cannot be named: a half-credited diff
62
+ * reads as "nobody touched this", so the whole list is dropped and the plain diff renders instead.
63
+ */
64
+ export const resolveDiffContributors = (stepsWithAttribution, profiles) => {
65
+ const profilesByAccountId = toProfilesByAccountId(profiles);
66
+ const contributors = [];
67
+ /** Keyed as the plugin keys changes, so no two entries can collapse onto one another. */
68
+ const creditedActors = new Set();
69
+ for (const {
70
+ stepAttribution
71
+ } of stepsWithAttribution) {
72
+ var _stepAttribution$user;
73
+ // An attribution naming nobody (e.g. one carrying only `wasOffline`) has nobody to credit.
74
+ const actor = stepAttribution ? getAttributionKey(stepAttribution) : undefined;
75
+ if (!stepAttribution || !actor || creditedActors.has(actor)) {
76
+ continue;
77
+ }
78
+ const userId = (_stepAttribution$user = stepAttribution.userId) === null || _stepAttribution$user === void 0 ? void 0 : _stepAttribution$user.trim();
79
+ if (hasAgentIdentity(stepAttribution)) {
80
+ const agent = resolveAgent(stepAttribution, profilesByAccountId);
81
+ if (!userId) {
82
+ // Agent mode: the agent acted on its own, so it is credited on its own.
83
+ contributors.push({
84
+ ...agent,
85
+ attribution: stepAttribution
86
+ });
87
+ creditedActors.add(actor);
88
+ continue;
89
+ }
90
+
91
+ // User-invoked agent: credit both actors and link the agent back to the invoking user.
92
+ const invokingUser = {
93
+ userId
94
+ };
95
+ const user = resolveUser(userId, profilesByAccountId);
96
+ if (!user) {
97
+ return undefined;
98
+ }
99
+
100
+ // Skipped if the invoking user's own step already credited them.
101
+ const invokingUserActor = getAttributionKey(invokingUser);
102
+ if (invokingUserActor && !creditedActors.has(invokingUserActor)) {
103
+ contributors.push({
104
+ ...user,
105
+ attribution: invokingUser
106
+ });
107
+ creditedActors.add(invokingUserActor);
108
+ }
109
+ contributors.push({
110
+ ...agent,
111
+ attribution: stepAttribution,
112
+ connectedTo: invokingUser
113
+ });
114
+ creditedActors.add(actor);
115
+ continue;
116
+ }
117
+ const user = userId ? resolveUser(userId, profilesByAccountId) : undefined;
118
+ if (!user) {
119
+ return undefined;
120
+ }
121
+ contributors.push({
122
+ ...user,
123
+ attribution: stepAttribution
124
+ });
125
+ creditedActors.add(actor);
126
+ }
127
+ return contributors.length > 0 ? contributors : undefined;
128
+ };