@atlaskit/editor-plugin-synced-block 5.1.9 → 5.1.11

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 (61) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/editor-commands/index.js +2 -2
  3. package/dist/cjs/nodeviews/bodiedSyncedBlock.js +13 -2
  4. package/dist/cjs/nodeviews/syncedBlock.js +21 -2
  5. package/dist/cjs/pm-plugins/experience-tracking/create-reference-experience.js +26 -26
  6. package/dist/cjs/pm-plugins/experience-tracking/create-source-experience.js +14 -30
  7. package/dist/cjs/pm-plugins/experience-tracking/delete-reference-experience.js +175 -0
  8. package/dist/cjs/pm-plugins/experience-tracking/delete-source-experience.js +103 -0
  9. package/dist/cjs/pm-plugins/experience-tracking/get-experience-tracking-plugins.js +30 -0
  10. package/dist/cjs/pm-plugins/experience-tracking/provider-only-experiences.js +128 -0
  11. package/dist/cjs/pm-plugins/utils/experience-tracking-utils.js +85 -0
  12. package/dist/cjs/types/index.js +5 -2
  13. package/dist/cjs/ui/CreateSyncedBlockDropdownItem.js +40 -8
  14. package/dist/cjs/ui/DeleteConfirmationModal.js +3 -1
  15. package/dist/cjs/ui/floating-toolbar.js +4 -2
  16. package/dist/es2019/editor-commands/index.js +2 -2
  17. package/dist/es2019/nodeviews/bodiedSyncedBlock.js +13 -2
  18. package/dist/es2019/nodeviews/syncedBlock.js +19 -4
  19. package/dist/es2019/pm-plugins/experience-tracking/create-reference-experience.js +27 -23
  20. package/dist/es2019/pm-plugins/experience-tracking/create-source-experience.js +14 -27
  21. package/dist/es2019/pm-plugins/experience-tracking/delete-reference-experience.js +181 -0
  22. package/dist/es2019/pm-plugins/experience-tracking/delete-source-experience.js +98 -0
  23. package/dist/es2019/pm-plugins/experience-tracking/get-experience-tracking-plugins.js +24 -0
  24. package/dist/es2019/pm-plugins/experience-tracking/provider-only-experiences.js +127 -0
  25. package/dist/es2019/pm-plugins/utils/experience-tracking-utils.js +65 -0
  26. package/dist/es2019/types/index.js +4 -1
  27. package/dist/es2019/ui/CreateSyncedBlockDropdownItem.js +38 -3
  28. package/dist/es2019/ui/DeleteConfirmationModal.js +3 -1
  29. package/dist/es2019/ui/floating-toolbar.js +3 -1
  30. package/dist/esm/editor-commands/index.js +2 -2
  31. package/dist/esm/nodeviews/bodiedSyncedBlock.js +13 -2
  32. package/dist/esm/nodeviews/syncedBlock.js +21 -2
  33. package/dist/esm/pm-plugins/experience-tracking/create-reference-experience.js +26 -25
  34. package/dist/esm/pm-plugins/experience-tracking/create-source-experience.js +14 -29
  35. package/dist/esm/pm-plugins/experience-tracking/delete-reference-experience.js +169 -0
  36. package/dist/esm/pm-plugins/experience-tracking/delete-source-experience.js +97 -0
  37. package/dist/esm/pm-plugins/experience-tracking/get-experience-tracking-plugins.js +30 -0
  38. package/dist/esm/pm-plugins/experience-tracking/provider-only-experiences.js +122 -0
  39. package/dist/esm/pm-plugins/utils/experience-tracking-utils.js +79 -0
  40. package/dist/esm/types/index.js +4 -1
  41. package/dist/esm/ui/CreateSyncedBlockDropdownItem.js +40 -8
  42. package/dist/esm/ui/DeleteConfirmationModal.js +3 -1
  43. package/dist/esm/ui/floating-toolbar.js +4 -2
  44. package/dist/types/pm-plugins/experience-tracking/create-reference-experience.d.ts +2 -9
  45. package/dist/types/pm-plugins/experience-tracking/create-source-experience.d.ts +4 -15
  46. package/dist/types/pm-plugins/experience-tracking/delete-reference-experience.d.ts +13 -0
  47. package/dist/types/pm-plugins/experience-tracking/delete-source-experience.d.ts +12 -0
  48. package/dist/types/pm-plugins/experience-tracking/get-experience-tracking-plugins.d.ts +2 -13
  49. package/dist/types/pm-plugins/experience-tracking/provider-only-experiences.d.ts +3 -0
  50. package/dist/types/pm-plugins/utils/experience-tracking-utils.d.ts +9 -0
  51. package/dist/types/types/index.d.ts +15 -0
  52. package/dist/types-ts4.5/pm-plugins/experience-tracking/create-reference-experience.d.ts +2 -9
  53. package/dist/types-ts4.5/pm-plugins/experience-tracking/create-source-experience.d.ts +4 -15
  54. package/dist/types-ts4.5/pm-plugins/experience-tracking/delete-reference-experience.d.ts +13 -0
  55. package/dist/types-ts4.5/pm-plugins/experience-tracking/delete-source-experience.d.ts +12 -0
  56. package/dist/types-ts4.5/pm-plugins/experience-tracking/get-experience-tracking-plugins.d.ts +2 -13
  57. package/dist/types-ts4.5/pm-plugins/experience-tracking/provider-only-experiences.d.ts +3 -0
  58. package/dist/types-ts4.5/pm-plugins/utils/experience-tracking-utils.d.ts +9 -0
  59. package/dist/types-ts4.5/types/index.d.ts +15 -0
  60. package/package.json +5 -5
  61. package/build/tsconfig.json +0 -22
@@ -0,0 +1,181 @@
1
+ import { bind } from 'bind-event-listener';
2
+ import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics';
3
+ import { Experience, ExperienceCheckDomMutation, ExperienceCheckTimeout } from '@atlaskit/editor-common/experiences';
4
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
5
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
+ import { EXPERIENCE_ABORT_REASON } from '../../types';
7
+ import { getRemovedResourceIds, getTarget, wasSyncBlockDeletedOrAddedByHistory } from '../utils/experience-tracking-utils';
8
+ const pluginKey = new PluginKey('deleteReferenceSyncBlockExperience');
9
+ const START_METHOD = {
10
+ ELEMENT_TOOLBAR: 'element-toolbar',
11
+ DELETE: 'delete',
12
+ TYPED_OVER: 'typed-over',
13
+ CUT: 'cut',
14
+ UNDO: 'undo',
15
+ REDO: 'redo'
16
+ };
17
+
18
+ /**
19
+ * This experience tracks when a reference sync block is deleted.
20
+ *
21
+ * Start: When user deletes ref sync block from toolbar, presses delete when cursor is in front of ref sync block,
22
+ * presses any key with a ref sync block selected, cuts with a ref sync block selected, triggers undo/redo that deletes a ref sync block
23
+ * Success: When the sync block is removed from the DOM within 2000ms of start
24
+ * Failure: When 2000ms passes without the reference sync block being removed from the DOM
25
+ */
26
+ export const getDeleteReferenceExperiencePlugin = ({
27
+ refs,
28
+ dispatchAnalyticsEvent,
29
+ syncBlockStore
30
+ }) => {
31
+ const experience = getDeleteReferenceExperience({
32
+ refs,
33
+ dispatchAnalyticsEvent
34
+ });
35
+ syncBlockStore.sourceManager.setDeleteExperience(experience);
36
+ const unbindClickListener = bind(document, {
37
+ type: 'click',
38
+ listener: event => {
39
+ const target = event.target;
40
+ if (!target) {
41
+ return;
42
+ }
43
+ const button = target.closest('button[data-testid]');
44
+ if (!button || !(button instanceof HTMLButtonElement)) {
45
+ return;
46
+ }
47
+ const testId = button.dataset.testid;
48
+ if (isReferenceSyncedBlockDeleteButtonId(testId)) {
49
+ experience.start({
50
+ method: START_METHOD.ELEMENT_TOOLBAR
51
+ });
52
+ }
53
+ }
54
+ });
55
+ return new SafePlugin({
56
+ key: pluginKey,
57
+ props: {
58
+ handleDOMEvents: {
59
+ cut: view => {
60
+ const {
61
+ state
62
+ } = view;
63
+ if (hasSyncBlockInSelection(state.selection)) {
64
+ experience.start({
65
+ method: START_METHOD.CUT
66
+ });
67
+ }
68
+ return false;
69
+ },
70
+ keydown: (view, event) => {
71
+ const {
72
+ state
73
+ } = view;
74
+ const hasSelection = hasSyncBlockInSelection(state.selection);
75
+ const hasAdjacent = hasSyncBlockBeforeCursor(state.selection);
76
+ if (hasSelection) {
77
+ experience.start({
78
+ method: START_METHOD.TYPED_OVER
79
+ });
80
+ }
81
+ if (isDeleteKey(event.key) && hasAdjacent) {
82
+ experience.start({
83
+ method: START_METHOD.DELETE
84
+ });
85
+ }
86
+ return false;
87
+ }
88
+ }
89
+ },
90
+ appendTransaction: (transactions, oldState, newState) => {
91
+ transactions.forEach(tr => {
92
+ const {
93
+ hasDeletedSyncBlock,
94
+ isUndo
95
+ } = wasSyncBlockDeletedOrAddedByHistory(tr, oldState, newState);
96
+ if (hasDeletedSyncBlock) {
97
+ experience.start({
98
+ method: isUndo ? START_METHOD.UNDO : START_METHOD.REDO
99
+ });
100
+ }
101
+ });
102
+ return null;
103
+ },
104
+ view: () => {
105
+ return {
106
+ destroy: () => {
107
+ experience.abort({
108
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
109
+ });
110
+ unbindClickListener();
111
+ }
112
+ };
113
+ }
114
+ });
115
+ };
116
+ export const getDeleteReferenceExperience = ({
117
+ refs,
118
+ dispatchAnalyticsEvent
119
+ }) => {
120
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
121
+ actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_DELETE,
122
+ dispatchAnalyticsEvent,
123
+ checks: [new ExperienceCheckTimeout({
124
+ durationMs: 2000
125
+ }), new ExperienceCheckDomMutation({
126
+ onDomMutation: ({
127
+ mutations
128
+ }) => {
129
+ const deletedResourceIds = getRemovedResourceIds(mutations, '[data-prosemirror-node-name="syncBlock"]');
130
+ if (deletedResourceIds.length > 0) {
131
+ return {
132
+ status: 'success',
133
+ metadata: {
134
+ deletedResourceIds
135
+ }
136
+ };
137
+ }
138
+ return undefined;
139
+ },
140
+ observeConfig: () => {
141
+ return {
142
+ target: getTarget(refs.containerElement),
143
+ options: {
144
+ childList: true
145
+ }
146
+ };
147
+ }
148
+ })]
149
+ });
150
+ };
151
+ const isReferenceSyncedBlockDeleteButtonId = testId => testId === 'reference-synced-block-delete-button';
152
+ const isDeleteKey = key => {
153
+ return key === 'Delete' || key === 'Backspace';
154
+ };
155
+ const hasSyncBlockInSelection = selection => {
156
+ const {
157
+ syncBlock
158
+ } = selection.$from.doc.type.schema.nodes;
159
+ let found = false;
160
+ selection.$from.doc.nodesBetween(selection.from, selection.to, node => {
161
+ if (node.type === syncBlock) {
162
+ found = true;
163
+ return false;
164
+ }
165
+ // sync block nodes can only be found at the top level
166
+ return false;
167
+ });
168
+ return found;
169
+ };
170
+ const hasSyncBlockBeforeCursor = selection => {
171
+ if (!selection.empty) {
172
+ return false;
173
+ }
174
+ const {
175
+ syncBlock
176
+ } = selection.$from.doc.type.schema.nodes;
177
+ const {
178
+ nodeBefore
179
+ } = selection.$from;
180
+ return (nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.type) === syncBlock;
181
+ };
@@ -0,0 +1,98 @@
1
+ import { bind } from 'bind-event-listener';
2
+ import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics';
3
+ import { Experience, ExperienceCheckDomMutation, ExperienceCheckTimeout } from '@atlaskit/editor-common/experiences';
4
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
5
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
+ import { EXPERIENCE_ABORT_REASON } from '../../types';
7
+ import { getRemovedResourceIds, getTarget } from '../utils/experience-tracking-utils';
8
+ const pluginKey = new PluginKey('deleteSourceSyncBlockExperience');
9
+ const START_METHOD = {
10
+ DELETE_CONFIRM_BUTTON: 'delete-confirm-button'
11
+ };
12
+
13
+ /**
14
+ * This experience tracks when a source sync block is deleted.
15
+ *
16
+ * Start: When user clicks the delete button in the delete modal
17
+ * Success: When the sync block is removed from the DOM within 2000ms of start
18
+ * Failure: When 2000ms passes without the source sync block being removed from the DOM
19
+ */
20
+ export const getDeleteSourceExperiencePlugin = ({
21
+ refs,
22
+ dispatchAnalyticsEvent,
23
+ syncBlockStore
24
+ }) => {
25
+ const experience = getDeleteSourceExperience({
26
+ refs,
27
+ dispatchAnalyticsEvent
28
+ });
29
+ syncBlockStore.sourceManager.setDeleteExperience(experience);
30
+ const unbindClickListener = bind(document, {
31
+ type: 'click',
32
+ listener: event => {
33
+ const target = event.target;
34
+ if (!target) {
35
+ return;
36
+ }
37
+ const button = target.closest('button[data-testid]');
38
+ if (!button || !(button instanceof HTMLButtonElement)) {
39
+ return;
40
+ }
41
+ const testId = button.dataset.testid;
42
+ if (isSyncedBlockDeleteButtonId(testId)) {
43
+ experience.start({
44
+ method: START_METHOD.DELETE_CONFIRM_BUTTON
45
+ });
46
+ }
47
+ }
48
+ });
49
+ return new SafePlugin({
50
+ key: pluginKey,
51
+ view: () => {
52
+ return {
53
+ destroy: () => {
54
+ experience.abort({
55
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
56
+ });
57
+ unbindClickListener();
58
+ }
59
+ };
60
+ }
61
+ });
62
+ };
63
+ export const getDeleteSourceExperience = ({
64
+ refs,
65
+ dispatchAnalyticsEvent
66
+ }) => {
67
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
68
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_DELETE,
69
+ dispatchAnalyticsEvent,
70
+ checks: [new ExperienceCheckTimeout({
71
+ durationMs: 2000
72
+ }), new ExperienceCheckDomMutation({
73
+ onDomMutation: ({
74
+ mutations
75
+ }) => {
76
+ const deletedResourceIds = getRemovedResourceIds(mutations, '[data-prosemirror-node-name="bodiedSyncBlock"]');
77
+ if (deletedResourceIds.length > 0) {
78
+ return {
79
+ status: 'success',
80
+ metadata: {
81
+ deletedResourceIds
82
+ }
83
+ };
84
+ }
85
+ return undefined;
86
+ },
87
+ observeConfig: () => {
88
+ return {
89
+ target: getTarget(refs.containerElement),
90
+ options: {
91
+ childList: true
92
+ }
93
+ };
94
+ }
95
+ })]
96
+ });
97
+ };
98
+ const isSyncedBlockDeleteButtonId = testId => testId === 'synced-block-delete-confirmation-modal-delete-button';
@@ -1,5 +1,8 @@
1
1
  import { getCreateReferenceExperiencePlugin } from "./create-reference-experience";
2
2
  import { getCreateSourceExperiencePlugin } from "./create-source-experience";
3
+ import { getDeleteReferenceExperiencePlugin } from "./delete-reference-experience";
4
+ import { getDeleteSourceExperiencePlugin } from "./delete-source-experience";
5
+ import { getProviderOnlyExperiencesPlugin } from "./provider-only-experiences";
3
6
  export const getExperienceTrackingPlugins = ({
4
7
  refs,
5
8
  dispatchAnalyticsEvent,
@@ -18,5 +21,26 @@ export const getExperienceTrackingPlugins = ({
18
21
  dispatchAnalyticsEvent,
19
22
  syncBlockStore
20
23
  })
24
+ }, {
25
+ name: 'deleteSourceExperiencePlugin',
26
+ plugin: () => getDeleteSourceExperiencePlugin({
27
+ refs,
28
+ dispatchAnalyticsEvent,
29
+ syncBlockStore
30
+ })
31
+ }, {
32
+ name: 'deleteReferenceExperiencePlugin',
33
+ plugin: () => getDeleteReferenceExperiencePlugin({
34
+ refs,
35
+ dispatchAnalyticsEvent,
36
+ syncBlockStore
37
+ })
38
+ }, {
39
+ name: 'providerOnlySyncedBlockExperiencesPlugin',
40
+ plugin: () => getProviderOnlyExperiencesPlugin({
41
+ refs,
42
+ dispatchAnalyticsEvent,
43
+ syncBlockStore
44
+ })
21
45
  }];
22
46
  };
@@ -0,0 +1,127 @@
1
+ import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics';
2
+ import { Experience, ExperienceCheckTimeout } from '@atlaskit/editor-common/experiences';
3
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
4
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
5
+ import { EXPERIENCE_ABORT_REASON } from '../../types';
6
+ const pluginKey = new PluginKey('providerOnlySyncBlockExperiences');
7
+ export const getProviderOnlyExperiencesPlugin = ({
8
+ refs,
9
+ dispatchAnalyticsEvent,
10
+ syncBlockStore
11
+ }) => {
12
+ const saveSourceExperience = getSaveSourceExperience({
13
+ refs,
14
+ dispatchAnalyticsEvent
15
+ });
16
+ syncBlockStore.sourceManager.setSaveExperience(saveSourceExperience);
17
+ const saveReferenceExperience = getSaveReferenceExperience({
18
+ refs,
19
+ dispatchAnalyticsEvent
20
+ });
21
+ const fetchExperience = getFetchExperience({
22
+ refs,
23
+ dispatchAnalyticsEvent
24
+ });
25
+ const fetchSourceInfoExperience = getFetchSourceInfoExperience({
26
+ refs,
27
+ dispatchAnalyticsEvent
28
+ });
29
+ syncBlockStore.referenceManager.setExperiences(fetchExperience, fetchSourceInfoExperience, saveReferenceExperience);
30
+ return new SafePlugin({
31
+ key: pluginKey,
32
+ view: () => {
33
+ return {
34
+ destroy: () => {
35
+ saveSourceExperience.abort({
36
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
37
+ });
38
+ saveReferenceExperience.abort({
39
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
40
+ });
41
+ fetchExperience.abort({
42
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
43
+ });
44
+ fetchSourceInfoExperience.abort({
45
+ reason: EXPERIENCE_ABORT_REASON.EDITOR_DESTROYED
46
+ });
47
+ }
48
+ };
49
+ }
50
+ });
51
+ };
52
+
53
+ /**
54
+ * This experience tracks when a source sync block is saved to the BE.
55
+ *
56
+ * Start: When the flush source sync block function is called.
57
+ * Success: When the sync block save is successful within 1500ms of start.
58
+ * Failure: When 1500ms passes without the sync block being successfully saved
59
+ */
60
+ const getSaveSourceExperience = ({
61
+ dispatchAnalyticsEvent
62
+ }) => {
63
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
64
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_UPDATE,
65
+ dispatchAnalyticsEvent,
66
+ checks: [new ExperienceCheckTimeout({
67
+ durationMs: 1500
68
+ })]
69
+ });
70
+ };
71
+
72
+ /**
73
+ * This experience tracks when a reference sync block is saved to the BE.
74
+ *
75
+ * Start: When the flush sync block function is called.
76
+ * Success: When the sync block save is successful within 1500ms of start.
77
+ * Failure: When 1500ms passes without the sync block being successfully saved
78
+ */
79
+ const getSaveReferenceExperience = ({
80
+ dispatchAnalyticsEvent
81
+ }) => {
82
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
83
+ actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_UPDATE,
84
+ dispatchAnalyticsEvent,
85
+ checks: [new ExperienceCheckTimeout({
86
+ durationMs: 1500
87
+ })]
88
+ });
89
+ };
90
+
91
+ /**
92
+ * This experience tracks when a reference sync block data is fetched from the BE.
93
+ *
94
+ * Start: When the fetchNodesData function is called.
95
+ * Success: When the fetching the data is successful within 1500ms of start.
96
+ * Failure: When 1500ms passes without the data being successfully fetched, or the fetch fails
97
+ */
98
+ const getFetchExperience = ({
99
+ dispatchAnalyticsEvent
100
+ }) => {
101
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
102
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH,
103
+ dispatchAnalyticsEvent,
104
+ checks: [new ExperienceCheckTimeout({
105
+ durationMs: 1500
106
+ })]
107
+ });
108
+ };
109
+
110
+ /**
111
+ * This experience tracks when a reference sync block source info data (title, url) is fetched from the BE.
112
+ *
113
+ * Start: When the fetchSourceInfo function is called.
114
+ * Success: When the fetching the data is successful within 2500ms of start.
115
+ * Failure: When 2500ms passes without the data being successfully fetched, or the fetch fails
116
+ */
117
+ const getFetchSourceInfoExperience = ({
118
+ dispatchAnalyticsEvent
119
+ }) => {
120
+ return new Experience(ACTION_SUBJECT.SYNCED_BLOCK, {
121
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_GET_SOURCE_INFO,
122
+ dispatchAnalyticsEvent,
123
+ checks: [new ExperienceCheckTimeout({
124
+ durationMs: 2500
125
+ })]
126
+ });
127
+ };
@@ -0,0 +1,65 @@
1
+ import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
2
+ import { findChildren } from '@atlaskit/editor-prosemirror/utils';
3
+ let targetEl;
4
+ export const getTarget = containerElement => {
5
+ if (!targetEl) {
6
+ const element = containerElement === null || containerElement === void 0 ? void 0 : containerElement.querySelector('.ProseMirror');
7
+ if (!element || !(element instanceof HTMLElement)) {
8
+ return null;
9
+ }
10
+ targetEl = element;
11
+ }
12
+ return targetEl;
13
+ };
14
+ export const wasSyncBlockDeletedOrAddedByHistory = (tr, oldState, newState) => {
15
+ const historyMeta = tr.getMeta(pmHistoryPluginKey);
16
+ if (!Boolean(historyMeta)) {
17
+ return {};
18
+ }
19
+ const {
20
+ syncBlock
21
+ } = newState.schema.nodes;
22
+ const oldSyncBlockNodes = findChildren(oldState.doc, node => node.type === syncBlock);
23
+ const newSyncBlockNodes = findChildren(newState.doc, node => node.type === syncBlock);
24
+ const oldSyncBlockIds = new Set(oldSyncBlockNodes.map(nodeWithPos => nodeWithPos.node.attrs.localId).filter(localId => Boolean(localId)));
25
+ const newSyncBlockIds = new Set(newSyncBlockNodes.map(nodeWithPos => nodeWithPos.node.attrs.localId).filter(localId => Boolean(localId)));
26
+ const hasDeletedSyncBlock = Array.from(oldSyncBlockIds).some(localId => !newSyncBlockIds.has(localId));
27
+ const hasAddedSyncBlock = Array.from(newSyncBlockIds).some(localId => !oldSyncBlockIds.has(localId));
28
+ return {
29
+ hasDeletedSyncBlock,
30
+ hasAddedSyncBlock,
31
+ isUndo: historyMeta.redo === false
32
+ };
33
+ };
34
+ const getResourceIds = (nodes, resourceIds, query) => {
35
+ nodes.forEach(node => {
36
+ if (!(node instanceof HTMLElement)) {
37
+ return;
38
+ }
39
+ const syncBlockElements = node.querySelectorAll(query);
40
+ syncBlockElements.forEach(element => {
41
+ const resourceId = element.getAttribute('resourceid');
42
+ if (resourceId) {
43
+ resourceIds.push(resourceId);
44
+ }
45
+ });
46
+ });
47
+ };
48
+ export const getAddedResourceIds = (mutations, query) => {
49
+ const resourceIds = [];
50
+ mutations.forEach(mutation => {
51
+ if (mutation.type === 'childList') {
52
+ getResourceIds(mutation.addedNodes, resourceIds, query);
53
+ }
54
+ });
55
+ return resourceIds;
56
+ };
57
+ export const getRemovedResourceIds = (mutations, query) => {
58
+ const resourceIds = [];
59
+ mutations.forEach(mutation => {
60
+ if (mutation.type === 'childList') {
61
+ getResourceIds(mutation.removedNodes, resourceIds, query);
62
+ }
63
+ });
64
+ return resourceIds;
65
+ };
@@ -5,4 +5,7 @@ export let FLAG_ID = /*#__PURE__*/function (FLAG_ID) {
5
5
  FLAG_ID["FAIL_TO_DELETE"] = "fail-to-delete";
6
6
  FLAG_ID["SYNC_BLOCK_COPIED"] = "sync-block-copied";
7
7
  return FLAG_ID;
8
- }({});
8
+ }({});
9
+ export const EXPERIENCE_ABORT_REASON = {
10
+ EDITOR_DESTROYED: 'editor-destroyed'
11
+ };
@@ -43,17 +43,48 @@ const CreateSyncedBlockDropdownItem = ({
43
43
  }),
44
44
  onClick: onClick,
45
45
  isDisabled: isOffline,
46
- testId: 'create-synced-block-block-menu-btn',
46
+ testId: "create-synced-block-block-menu-btn",
47
47
  elemAfter: /*#__PURE__*/React.createElement(Lozenge, {
48
48
  appearance: "new"
49
49
  }, formatMessage(blockMenuMessages.newLozenge))
50
50
  }, formatMessage(blockMenuMessages.createSyncedBlock));
51
51
  };
52
+ const CopySyncedBlockDropdownItem = ({
53
+ api
54
+ }) => {
55
+ const {
56
+ formatMessage
57
+ } = useIntl();
58
+ const {
59
+ mode
60
+ } = useSharedPluginStateWithSelector(api, ['connectivity'], states => {
61
+ var _states$connectivityS2;
62
+ return {
63
+ mode: (_states$connectivityS2 = states.connectivityState) === null || _states$connectivityS2 === void 0 ? void 0 : _states$connectivityS2.mode
64
+ };
65
+ });
66
+ const onClick = () => {
67
+ var _api$core3, _api$core4, _api$blockControls2, _api$blockControls2$c;
68
+ api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(api === null || api === void 0 ? void 0 : api.syncedBlock.commands.copySyncedBlockReferenceToClipboard());
69
+ api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : (_api$blockControls2$c = _api$blockControls2.commands) === null || _api$blockControls2$c === void 0 ? void 0 : _api$blockControls2$c.toggleBlockMenu({
70
+ closeMenu: true
71
+ }));
72
+ };
73
+ return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
74
+ elemBefore: /*#__PURE__*/React.createElement(SyncBlocksIcon, {
75
+ label: ""
76
+ }),
77
+ onClick: onClick,
78
+ isDisabled: isOfflineMode(mode),
79
+ elemAfter: /*#__PURE__*/React.createElement(Lozenge, {
80
+ appearance: "new"
81
+ }, formatMessage(blockMenuMessages.newLozenge))
82
+ }, formatMessage(blockMenuMessages.copySyncedBlock));
83
+ };
52
84
  export const CreateOrCopySyncedBlockDropdownItem = ({
53
85
  api,
54
86
  enableSourceSyncedBlockCreation
55
87
  }) => {
56
- var _menuTriggerByNode$no;
57
88
  const {
58
89
  menuTriggerByNode
59
90
  } = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
@@ -62,7 +93,11 @@ export const CreateOrCopySyncedBlockDropdownItem = ({
62
93
  menuTriggerByNode: (_states$blockControls3 = (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.menuTriggerByNode) !== null && _states$blockControls3 !== void 0 ? _states$blockControls3 : undefined
63
94
  };
64
95
  });
65
- if (!['syncBlock', 'bodiedSyncBlock'].includes((_menuTriggerByNode$no = menuTriggerByNode === null || menuTriggerByNode === void 0 ? void 0 : menuTriggerByNode.nodeType) !== null && _menuTriggerByNode$no !== void 0 ? _menuTriggerByNode$no : '') && enableSourceSyncedBlockCreation) {
96
+ if ((menuTriggerByNode === null || menuTriggerByNode === void 0 ? void 0 : menuTriggerByNode.nodeType) === 'syncBlock' || (menuTriggerByNode === null || menuTriggerByNode === void 0 ? void 0 : menuTriggerByNode.nodeType) === 'bodiedSyncBlock') {
97
+ return /*#__PURE__*/React.createElement(CopySyncedBlockDropdownItem, {
98
+ api: api
99
+ });
100
+ } else if (enableSourceSyncedBlockCreation) {
66
101
  return /*#__PURE__*/React.createElement(CreateSyncedBlockDropdownItem, {
67
102
  api: api
68
103
  });
@@ -5,6 +5,7 @@ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'
5
5
  import { syncBlockMessages as messages } from '@atlaskit/editor-common/messages';
6
6
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
7
7
  import ModalDialog, { ModalBody, ModalFooter, ModalHeader, ModalTitle, ModalTransition } from '@atlaskit/modal-dialog';
8
+ import { fg } from '@atlaskit/platform-feature-flags';
8
9
  import { Text } from '@atlaskit/primitives/compiled';
9
10
  import { syncedBlockPluginKey } from '../pm-plugins/main';
10
11
  export const DeleteConfirmationModal = ({
@@ -105,6 +106,7 @@ export const DeleteConfirmationModal = ({
105
106
  onClick: handleClick(true),
106
107
  autoFocus: true,
107
108
  isDisabled: isOfflineMode(mode),
108
- isLoading: bodiedSyncBlockDeletionStatus === 'processing'
109
+ isLoading: bodiedSyncBlockDeletionStatus === 'processing',
110
+ testId: fg('platform_synced_block_dogfooding') ? 'synced-block-delete-confirmation-modal-delete-button' : undefined
109
111
  }, formatMessage(messages.deleteConfirmationModalDeleteButton)))));
110
112
  };
@@ -6,6 +6,7 @@ import { SyncBlockError } from '@atlaskit/editor-synced-block-provider';
6
6
  import CopyIcon from '@atlaskit/icon/core/copy';
7
7
  import DeleteIcon from '@atlaskit/icon/core/delete';
8
8
  import EditIcon from '@atlaskit/icon/core/edit';
9
+ import { fg } from '@atlaskit/platform-feature-flags';
9
10
  import { copySyncedBlockReferenceToClipboard, editSyncedBlockSource, removeSyncedBlock } from '../editor-commands';
10
11
  import { findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
11
12
  export const getToolbarConfig = (state, intl, api, syncBlockStore) => {
@@ -42,6 +43,7 @@ export const getToolbarConfig = (state, intl, api, syncBlockStore) => {
42
43
  title: formatMessage(commonMessages.delete),
43
44
  onClick: removeSyncedBlock(api),
44
45
  icon: DeleteIcon,
46
+ testId: fg('platform_synced_block_dogfooding') ? 'reference-synced-block-delete-button' : undefined,
45
47
  ...hoverDecorationProps(nodeType, akEditorSelectedNodeClassName)
46
48
  };
47
49
  items.push(deleteButton);
@@ -53,7 +55,7 @@ export const getToolbarConfig = (state, intl, api, syncBlockStore) => {
53
55
  icon: CopyIcon,
54
56
  title: formatMessage(messages.copySyncBlockLabel),
55
57
  showTitle: false,
56
- tooltipContent: formatMessage(messages.copySyncBlockTooltip),
58
+ tooltipContent: formatMessage(messages.copySyncedBlockTooltip),
57
59
  onClick: copySyncedBlockReferenceToClipboard(syncBlockStore, api),
58
60
  ...hoverDecorationProps(nodeType, akEditorSelectedNodeClassName)
59
61
  };
@@ -53,8 +53,8 @@ export var createSyncedBlock = function createSyncedBlock(_ref) {
53
53
  syncBlockStore.sourceManager.createBodiedSyncBlockNode(_attrs);
54
54
  tr.replaceWith(conversionInfo.from > 0 ? conversionInfo.from - 1 : 0, conversionInfo.to, _newBodiedSyncBlockNode).scrollIntoView();
55
55
 
56
- // set selection to the end of the previous selection + 1 for the position taken up by the start of the new synced block
57
- tr.setSelection(TextSelection.create(tr.doc, conversionInfo.to + 1));
56
+ // set selection to the start of the previous selection for the position taken up by the start of the new synced block
57
+ tr.setSelection(TextSelection.create(tr.doc, conversionInfo.from));
58
58
  }
59
59
 
60
60
  // This transaction will be intercepted in filterTransaction and dispatched when saving to backend succeeds
@@ -6,10 +6,13 @@ import _inherits from "@babel/runtime/helpers/inherits";
6
6
  function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
7
7
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
8
8
  import React from 'react';
9
+ import { ACTION_SUBJECT } from '@atlaskit/editor-common/analytics';
10
+ import { ErrorBoundary } from '@atlaskit/editor-common/error-boundary';
9
11
  import ReactNodeView from '@atlaskit/editor-common/react-node-view';
10
12
  import { BodiedSyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
11
13
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
12
14
  import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
15
+ import { fg } from '@atlaskit/platform-feature-flags';
13
16
  import { BodiedSyncBlockWrapper } from '../ui/BodiedSyncBlockWrapper';
14
17
  var toDOM = function toDOM() {
15
18
  return ['div', {
@@ -82,12 +85,20 @@ var BodiedSyncBlock = /*#__PURE__*/function (_ReactNodeView) {
82
85
  }, {
83
86
  key: "render",
84
87
  value: function render(_props, forwardRef) {
85
- var _this$api5;
88
+ var _this$api5, _this$api6;
86
89
  var syncBlockStore = (_this$api5 = this.api) === null || _this$api5 === void 0 || (_this$api5 = _this$api5.syncedBlock.sharedState) === null || _this$api5 === void 0 || (_this$api5 = _this$api5.currentState()) === null || _this$api5 === void 0 ? void 0 : _this$api5.syncBlockStore;
87
90
  if (!syncBlockStore) {
88
91
  return null;
89
92
  }
90
- return /*#__PURE__*/React.createElement(BodiedSyncBlockWrapper, {
93
+ return fg('platform_synced_block_dogfooding') ? /*#__PURE__*/React.createElement(ErrorBoundary, {
94
+ component: ACTION_SUBJECT.SYNCED_BLOCK,
95
+ dispatchAnalyticsEvent: (_this$api6 = this.api) === null || _this$api6 === void 0 || (_this$api6 = _this$api6.analytics) === null || _this$api6 === void 0 ? void 0 : _this$api6.actions.fireAnalyticsEvent,
96
+ fallbackComponent: null
97
+ }, /*#__PURE__*/React.createElement(BodiedSyncBlockWrapper, {
98
+ ref: forwardRef,
99
+ syncBlockStore: syncBlockStore,
100
+ node: this.node
101
+ })) : /*#__PURE__*/React.createElement(BodiedSyncBlockWrapper, {
91
102
  ref: forwardRef,
92
103
  syncBlockStore: syncBlockStore,
93
104
  node: this.node