@atlaskit/editor-common 122.4.0 → 122.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/collab-agent-edit-changed-ranges/package.json +10 -0
  3. package/dist/cjs/collab/agent-edit-changed-ranges.js +304 -0
  4. package/dist/cjs/monitoring/error.js +1 -1
  5. package/dist/cjs/transforms/getFormattedNode.js +1 -5
  6. package/dist/cjs/transforms/list-transforms.js +2 -8
  7. package/dist/cjs/transforms/transformTaskListToBlockNodes.js +1 -5
  8. package/dist/cjs/ui/DropList/index.js +1 -1
  9. package/dist/cjs/ui/Emoji/index.js +5 -1
  10. package/dist/cjs/utils/isSingleEmoji.js +21 -0
  11. package/dist/cjs/utils/validator.js +6 -2
  12. package/dist/es2019/collab/agent-edit-changed-ranges.js +255 -0
  13. package/dist/es2019/monitoring/error.js +1 -1
  14. package/dist/es2019/transforms/getFormattedNode.js +1 -5
  15. package/dist/es2019/transforms/list-transforms.js +2 -8
  16. package/dist/es2019/transforms/transformTaskListToBlockNodes.js +1 -5
  17. package/dist/es2019/ui/DropList/index.js +1 -1
  18. package/dist/es2019/ui/Emoji/index.js +5 -1
  19. package/dist/es2019/utils/isSingleEmoji.js +15 -0
  20. package/dist/es2019/utils/validator.js +6 -1
  21. package/dist/esm/collab/agent-edit-changed-ranges.js +297 -0
  22. package/dist/esm/monitoring/error.js +1 -1
  23. package/dist/esm/transforms/getFormattedNode.js +1 -5
  24. package/dist/esm/transforms/list-transforms.js +2 -8
  25. package/dist/esm/transforms/transformTaskListToBlockNodes.js +1 -5
  26. package/dist/esm/ui/DropList/index.js +1 -1
  27. package/dist/esm/ui/Emoji/index.js +5 -1
  28. package/dist/esm/utils/isSingleEmoji.js +15 -0
  29. package/dist/esm/utils/validator.js +6 -2
  30. package/dist/types/collab/agent-edit-changed-ranges.d.ts +12 -0
  31. package/dist/types/provider-factory/profile-card-provider.d.ts +5 -0
  32. package/dist/types/utils/isSingleEmoji.d.ts +9 -0
  33. package/package.json +5 -2
  34. package/utils/isSingleEmoji/package.json +10 -0
@@ -0,0 +1,255 @@
1
+ import isEqual from 'lodash/isEqual';
2
+ // localId is identity metadata: regenerating it alone should not create activity.
3
+ const sameAttrs = (left, right) => {
4
+ const keys = Object.keys(left).filter(key => key !== 'localId');
5
+ return keys.length === Object.keys(right).filter(key => key !== 'localId').length && keys.every(key => isEqual(left[key], right[key]));
6
+ };
7
+
8
+ // Compare the node's own type, attributes and marks, without comparing its children.
9
+ const sameMarkup = (left, right) => left.type === right.type && sameAttrs(left.attrs, right.attrs) && left.marks.length === right.marks.length && left.marks.every((mark, index) => mark.type === right.marks[index].type && sameAttrs(mark.attrs, right.marks[index].attrs));
10
+ const sameNode = (left, right) => {
11
+ if (left === right) {
12
+ return true;
13
+ }
14
+ if (!sameMarkup(left, right) || left.text !== right.text || left.childCount !== right.childCount) {
15
+ return false;
16
+ }
17
+ for (let index = 0; index < left.childCount; index++) {
18
+ if (!sameNode(left.child(index), right.child(index))) {
19
+ return false;
20
+ }
21
+ }
22
+ return true;
23
+ };
24
+
25
+ // Positions include document structure, not just character offsets. A non-leaf node
26
+ // uses one position for each opening/closing boundary, plus the size of its content.
27
+ // Example: paragraph('Hi') at pos 0 has nodeSize 4, outer range [0, 4),
28
+ // and text content [1, 3). All ranges here use an exclusive end position.
29
+
30
+ // Keep the reason internally so a panel attribute change can survive child edits.
31
+ // Only ordinary { from, to } ranges are returned to the chrome consumer.
32
+
33
+ const collectChanges = (before, after, map, ranges) => {
34
+ const {
35
+ node,
36
+ pos
37
+ } = after;
38
+ if (before && sameNode(before.node, node)) {
39
+ return;
40
+ }
41
+ // Text edits belong to their textblock. A node's own markup change instead
42
+ // owns its full content, so we stop here rather than adding child ranges too.
43
+ const markupChanged = !!before && node.isBlock && !sameMarkup(before.node, node);
44
+ if (node.isTextblock || markupChanged) {
45
+ if (node.content.size > 0) {
46
+ ranges.push({
47
+ from: pos,
48
+ to: pos + node.nodeSize,
49
+ markupChanged
50
+ });
51
+ }
52
+ return;
53
+ }
54
+ const byPosition = new Map();
55
+ const byId = new Map();
56
+ const oldChildren = [];
57
+ before === null || before === void 0 ? void 0 : before.node.forEach((child, offset) => {
58
+ // forEach offsets start inside the parent. Skip its opening boundary (+1)
59
+ // to convert this child offset into an absolute document position.
60
+ const previous = {
61
+ node: child,
62
+ pos: before.pos + 1 + offset
63
+ };
64
+ oldChildren.push(previous);
65
+ // This 1 is a mapping bias, not an added offset: follow the right side
66
+ // of an insertion at the child's start, keeping the original child after it.
67
+ const mapped = map.mapResult(previous.pos, 1);
68
+ if (!mapped.deleted) {
69
+ byPosition.set(mapped.pos, previous);
70
+ }
71
+ const id = child.attrs.localId;
72
+ if (typeof id === 'string' && id) {
73
+ // Duplicate IDs cannot identify a unique old child.
74
+ byId.set(id, byId.has(id) ? null : previous);
75
+ }
76
+ });
77
+ const nextChildren = [];
78
+ node.forEach((child, offset) => nextChildren.push({
79
+ node: child,
80
+ pos: pos + 1 + offset
81
+ }));
82
+ // Narrow edits usually preserve mapped positions. Whole-container replacements
83
+ // may erase those positions, so stable child IDs provide a second way to match.
84
+ const preferred = nextChildren.map(next => {
85
+ var _ref, _byPosition$get;
86
+ return (_ref = (_byPosition$get = byPosition.get(next.pos)) !== null && _byPosition$get !== void 0 ? _byPosition$get : byId.get(next.node.attrs.localId)) !== null && _ref !== void 0 ? _ref : undefined;
87
+ });
88
+ const claims = new Map();
89
+ for (const match of preferred) {
90
+ if (match) {
91
+ var _claims$get;
92
+ claims.set(match, ((_claims$get = claims.get(match)) !== null && _claims$get !== void 0 ? _claims$get : 0) + 1);
93
+ }
94
+ }
95
+ nextChildren.forEach((next, index) => {
96
+ const match = preferred[index];
97
+ // Same-index fallback supports replacements that regenerate IDs but keep
98
+ // the child structure. Different child counts make that correspondence unsafe.
99
+ const aligned = oldChildren.length === node.childCount ? oldChildren[index] : undefined;
100
+ // Reserve every position/ID match before fallback, including later siblings.
101
+ // Reusing one old sibling can otherwise disguise an equal-text insertion.
102
+ const previous = match && claims.get(match) === 1 ? match : aligned && !claims.has(aligned) ? aligned : undefined;
103
+ collectChanges(previous, next, map, ranges);
104
+ });
105
+ };
106
+ const findPreservedNode = (doc, previous, markupChanged, indexes) => {
107
+ const id = previous.attrs.localId;
108
+ if (typeof id !== 'string' || !id) {
109
+ return;
110
+ }
111
+ let byId = indexes.get(doc);
112
+ if (!byId) {
113
+ // Build only when mapping loses a candidate, then reuse for every range
114
+ // visiting this immutable snapshot. Ordinary mapped edits need no scan.
115
+ const nodeIndex = new Map();
116
+ doc.descendants((node, pos) => {
117
+ const localId = node.attrs.localId;
118
+ if (typeof localId === 'string' && localId) {
119
+ // Keep duplicates ambiguous, including IDs outside the changed subtree.
120
+ nodeIndex.set(localId, nodeIndex.has(localId) ? null : {
121
+ node,
122
+ pos
123
+ });
124
+ }
125
+ });
126
+ indexes.set(doc, nodeIndex);
127
+ byId = nodeIndex;
128
+ }
129
+ const match = byId.get(id);
130
+ // Attribute/mark activity belongs to the node even when its children are edited.
131
+ // Content-only activity still requires the entire candidate to survive unchanged.
132
+ const isPreserved = markupChanged ? sameMarkup : sameNode;
133
+ return match && isPreserved(previous, match.node) ? match : undefined;
134
+ };
135
+ const mapToFinalBlocks = (tr, index, range, indexes) => {
136
+ var _tr$docs;
137
+ let {
138
+ from,
139
+ to
140
+ } = range;
141
+ // Track inner content separately from outer node bounds: +1 skips the opening
142
+ // boundary and -1 excludes the closing boundary (see paragraph example above).
143
+ let contentFrom = from + 1;
144
+ let contentTo = to - 1;
145
+ // docs[i] is the document BEFORE step i, so docs[i + 1] is its result.
146
+ // The last step has no next snapshot; tr.doc is its result instead.
147
+ let node = ((_tr$docs = tr.docs[index + 1]) !== null && _tr$docs !== void 0 ? _tr$docs : tr.doc).nodeAt(from);
148
+ if (!node) {
149
+ return [];
150
+ }
151
+ // Later non-AI steps also shift/delete existing activity, even though they
152
+ // cannot create new activity themselves. Process every intermediate document.
153
+ for (let next = index + 1; next < tr.steps.length; next++) {
154
+ var _tr$docs2;
155
+ const map = tr.mapping.maps[next];
156
+ const after = (_tr$docs2 = tr.docs[next + 1]) !== null && _tr$docs2 !== void 0 ? _tr$docs2 : tr.doc;
157
+ // Mapping biases +1 (right) and -1 (left) keep boundary insertions outside
158
+ // the tracked content. These arguments do not add/subtract document positions.
159
+ const start = map.mapResult(contentFrom, 1);
160
+ const end = map.mapResult(contentTo, -1);
161
+ if (start.deleted && end.deleted || end.pos <= start.pos) {
162
+ // A cumulative replacement can preserve the candidate content or owned markup.
163
+ // Check every intermediate doc so deletion then reinsertion never resurrects it.
164
+ const preserved = findPreservedNode(after, node, range.markupChanged, indexes);
165
+ if (!preserved) {
166
+ return [];
167
+ }
168
+ from = preserved.pos;
169
+ to = from + preserved.node.nodeSize;
170
+ contentFrom = from + 1;
171
+ contentTo = to - 1;
172
+ node = preserved.node;
173
+ } else {
174
+ // Apply the same inward biases to the outer node bounds.
175
+ from = map.map(from, 1);
176
+ to = map.map(to, -1);
177
+ contentFrom = start.pos;
178
+ contentTo = end.pos;
179
+ const current = after.nodeAt(from);
180
+ if (current && current.nodeSize === to - from) {
181
+ node = current;
182
+ }
183
+ }
184
+ }
185
+ if (from < 0 || to > tr.doc.content.size || contentTo <= contentFrom) {
186
+ return [];
187
+ }
188
+ const finalNode = tr.doc.nodeAt(from);
189
+ if (finalNode !== null && finalNode !== void 0 && finalNode.isBlock && finalNode.nodeSize === to - from) {
190
+ return [{
191
+ from,
192
+ to
193
+ }];
194
+ }
195
+ // Joins/splits can leave mapped boundaries inside the final textblock.
196
+ const blocks = [];
197
+ tr.doc.nodesBetween(contentFrom, contentTo, (child, pos) => {
198
+ if (child.isTextblock) {
199
+ blocks.push({
200
+ from: pos,
201
+ to: pos + child.nodeSize
202
+ });
203
+ return false;
204
+ }
205
+ });
206
+ return blocks;
207
+ };
208
+
209
+ /**
210
+ * Returns whole content-block ranges for selected applied steps, in tr.doc coordinates.
211
+ * Callers select actual AI writes (excluding restores, acknowledgments and preparation)
212
+ * and own experiment/lifecycle decisions. This helper does not dispatch or render.
213
+ *
214
+ * Comparing each step's snapshots also handles attribute/mark steps with empty maps.
215
+ * Shared unchanged subtrees are skipped by reference. Only localId changes are ignored;
216
+ * meaningful attributes and marks retain the affected node's full content scope.
217
+ */
218
+ export function getAgentEditChangedRanges(tr, includedStepIndexes) {
219
+ // Positions belong to each intermediate document, not just tr.doc. Keep the
220
+ // lazy indexes local to this call so deletion/reinsertion checks stay intact
221
+ // and no cache is retained between transactions or streaming chunks.
222
+ const indexes = new WeakMap();
223
+ const ranges = [];
224
+ for (const index of new Set(includedStepIndexes)) {
225
+ var _tr$docs3;
226
+ if (!Number.isInteger(index) || index < 0 || index >= tr.steps.length) {
227
+ continue;
228
+ }
229
+ const changed = [];
230
+ // The root document has no opening boundary. Using -1 makes the recursive
231
+ // parent.pos + 1 + offset formula place its first child at position 0.
232
+ collectChanges({
233
+ node: tr.docs[index],
234
+ pos: -1
235
+ }, {
236
+ node: (_tr$docs3 = tr.docs[index + 1]) !== null && _tr$docs3 !== void 0 ? _tr$docs3 : tr.doc,
237
+ pos: -1
238
+ }, tr.steps[index].getMap(), changed);
239
+ for (const range of changed) {
240
+ ranges.push(...mapToFinalBlocks(tr, index, range, indexes));
241
+ }
242
+ }
243
+
244
+ // Return only the surviving final-document ranges, never intermediate step states.
245
+ // Sort outer ranges before their children, then discard duplicates/contained ranges.
246
+ // Separate ranges stay separate; this must not highlight the untouched gaps.
247
+ const unique = [];
248
+ for (const range of ranges.sort((left, right) => left.from - right.from || right.to - left.to)) {
249
+ const previous = unique[unique.length - 1];
250
+ if (!previous || range.to > previous.to) {
251
+ unique.push(range);
252
+ }
253
+ }
254
+ return unique;
255
+ }
@@ -13,7 +13,7 @@ const NETWORK_FAILURE_REGEX = /^network failure/i;
13
13
  const RESIZE_OBSERVER_LOOP_REGEX = /ResizeObserver loop completed with undelivered notifications/;
14
14
  const SENTRY_DSN = 'https://0b10c8e02fb44d8796c047b102c9bee8@o55978.ingest.sentry.io/4505129224110080';
15
15
  const packageName = 'editor-common'; // Sentry doesn't accept '/' in its releases https://docs.sentry.io/platforms/javascript/configuration/releases/
16
- const packageVersion = "122.3.3";
16
+ const packageVersion = "122.5.0";
17
17
  const sanitiseSentryEvents = (data, _hint) => {
18
18
  // Remove URL as it has UGC
19
19
  // Ignored via go/ees007
@@ -1,5 +1,4 @@
1
1
  import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
2
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
2
  export const getFormattedNode = tr => {
4
3
  const {
5
4
  selection
@@ -7,10 +6,7 @@ export const getFormattedNode = tr => {
7
6
  const {
8
7
  nodes
9
8
  } = tr.doc.type.schema;
10
-
11
- // gating behind platform_editor_small_font_size to support task lists with font size applied,
12
- // but keep this solution general
13
- const isBlockTaskItemEnabled = !!nodes.blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
9
+ const isBlockTaskItemEnabled = !!nodes.blockTaskItem;
14
10
 
15
11
  // Find the node to format from the current selection
16
12
  let nodeToFormat;
@@ -1,5 +1,4 @@
1
1
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
2
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
2
  import { convertBlockToInlineContent } from './convertBlockToInlineContent';
4
3
  import { createBlockTaskItem } from './createBlockTaskItem';
5
4
  import { isBulletOrOrderedList } from './isBulletOrOrderedList';
@@ -34,10 +33,7 @@ export const transformListRecursively = (props, onhandleUnsupportedContent) => {
34
33
  paragraph,
35
34
  blockTaskItem
36
35
  } = schema.nodes;
37
-
38
- // gating behind platform_editor_small_font_size to support task lists with font size applied,
39
- // but keep this solution general
40
- const isBlockTaskEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
36
+ const isBlockTaskEnabled = !!blockTaskItem;
41
37
 
42
38
  /**
43
39
  * Extracts paragraph children from a blockTaskItem, preserving their marks.
@@ -204,9 +200,7 @@ export const transformToTaskList = (tr, range, targetNodeType, targetAttrs, node
204
200
  paragraph,
205
201
  blockTaskItem
206
202
  } = nodes;
207
- // gating behind platform_editor_small_font_size to support task lists with font size applied,
208
- // but keep this solution general
209
- const isBlockTaskItemEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
203
+ const isBlockTaskItemEnabled = !!blockTaskItem;
210
204
  const listItems = [];
211
205
 
212
206
  // Process each block in the range
@@ -1,6 +1,5 @@
1
1
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
2
2
  import { findChildrenByType } from '@atlaskit/editor-prosemirror/utils';
3
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
3
  export const transformTaskListToBlockNodes = context => {
5
4
  const {
6
5
  tr,
@@ -16,10 +15,7 @@ export const transformTaskListToBlockNodes = context => {
16
15
  const {
17
16
  blockTaskItem
18
17
  } = schema.nodes;
19
-
20
- // gating behind platform_editor_small_font_size to support task lists with font size applied,
21
- // but keep this solution general
22
- const isBlockTaskItemEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
18
+ const isBlockTaskItemEnabled = !!blockTaskItem;
23
19
  if (isBlockTaskItemEnabled) {
24
20
  const blockTaskItemsResult = findChildrenByType(sourceNode, blockTaskItem);
25
21
  if (blockTaskItemsResult.length > 0 && targetNodeType === schema.nodes.paragraph) {
@@ -14,7 +14,7 @@ import withAnalyticsEvents from '@atlaskit/analytics-next/withAnalyticsEvents';
14
14
  import { fg } from '@atlaskit/platform-feature-flags/fg';
15
15
  import Layer from '../Layer';
16
16
  const packageName = "@atlaskit/editor-common";
17
- const packageVersion = "122.3.3";
17
+ const packageVersion = "122.5.0";
18
18
  const halfFocusRing = 1;
19
19
  const dropOffset = '0, 8';
20
20
  const fadeIn = keyframes({
@@ -1,5 +1,7 @@
1
1
  import React, { useMemo } from 'react';
2
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
2
3
  import { ResourcedEmoji } from '@atlaskit/emoji/element';
4
+ import { isSingleEmoji } from '../../utils/isSingleEmoji';
3
5
  const EmojiNodeFunctional = props => {
4
6
  var _resourceConfig$optim;
5
7
  const {
@@ -18,12 +20,13 @@ const EmojiNodeFunctional = props => {
18
20
  fallback
19
21
  }), [shortName, id, fallback]);
20
22
  const emojiProviderResolver = useMemo(() => Promise.resolve(emojiProvider), [emojiProvider]);
23
+ const customFallback = fg('platform_editor_custom_emoji_unicode_fallback') && !isSingleEmoji(fallback || shortName) ? '\uFFFD' : undefined;
21
24
  if (allowTextFallback && !emojiProvider) {
22
25
  return /*#__PURE__*/React.createElement("span", {
23
26
  "data-emoji-id": id,
24
27
  "data-emoji-short-name": shortName,
25
28
  "data-emoji-text": fallback || shortName
26
- }, fallback || shortName);
29
+ }, customFallback || fallback || shortName);
27
30
  }
28
31
  if (!emojiProvider) {
29
32
  return null;
@@ -34,6 +37,7 @@ const EmojiNodeFunctional = props => {
34
37
  showTooltip: showTooltip,
35
38
  fitToHeight: fitToHeight,
36
39
  optimistic: true,
40
+ customFallback: customFallback,
37
41
  optimisticImageURL: resourceConfig === null || resourceConfig === void 0 ? void 0 : (_resourceConfig$optim = resourceConfig.optimisticImageApi) === null || _resourceConfig$optim === void 0 ? void 0 : _resourceConfig$optim.getUrl({
38
42
  id,
39
43
  fallback,
@@ -0,0 +1,15 @@
1
+ const SINGLE_EMOJI_REGEX =
2
+ // Regular expression to match a single emoji character
3
+ /^(\p{Emoji_Presentation}(?:[\u{1F3FB}-\u{1F3FF}])?|\p{Extended_Pictographic}\u{FE0F}(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{200D}\p{Extended_Pictographic}\u{FE0F}?(?:[\u{1F3FB}-\u{1F3FF}])?)*|\p{Extended_Pictographic}\u{FE0F}?(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{200D}\p{Extended_Pictographic}\u{FE0F}?(?:[\u{1F3FB}-\u{1F3FF}])?)+|\p{Regional_Indicator}\p{Regional_Indicator})$/u;
4
+
5
+ /**
6
+ * Check if we can nicely fallback to the nodes text
7
+ *
8
+ * @param fallbackText string of the nodes fallback text
9
+ *
10
+ * @example
11
+ * isSingleEmoji('😀') // true
12
+ */
13
+ export function isSingleEmoji(fallbackText) {
14
+ return SINGLE_EMOJI_REGEX.test(fallbackText !== null && fallbackText !== void 0 ? fallbackText : '');
15
+ }
@@ -480,6 +480,7 @@ export const getValidNode = (originalNode, schema = defaultSchema, adfStage = 'f
480
480
  let mentionText = '';
481
481
  let mentionId;
482
482
  let mentionAccess;
483
+ let localId;
483
484
  if (attrs) {
484
485
  const {
485
486
  text,
@@ -490,6 +491,7 @@ export const getValidNode = (originalNode, schema = defaultSchema, adfStage = 'f
490
491
  mentionText = text || displayName;
491
492
  mentionId = id;
492
493
  mentionAccess = accessLevel;
494
+ localId = attrs.localId;
493
495
  }
494
496
  if (!mentionText) {
495
497
  mentionText = text || '@unknown';
@@ -500,7 +502,10 @@ export const getValidNode = (originalNode, schema = defaultSchema, adfStage = 'f
500
502
  attrs: {
501
503
  id: mentionId,
502
504
  text: mentionText,
503
- accessLevel: ''
505
+ accessLevel: '',
506
+ ...(localId ? {
507
+ localId
508
+ } : {})
504
509
  },
505
510
  ...(fg('editor_inline_comments_on_inline_nodes') ? {
506
511
  marks