@atlaskit/editor-plugin-synced-block 17.0.8 → 17.0.9

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.
@@ -2,7 +2,7 @@
2
2
  import "./SyncedLocationDropdown.compiled.css";
3
3
  import * as React from 'react';
4
4
  import { ax, ix } from "@compiled/react/runtime";
5
- import { useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
6
6
  import { cx } from '@compiled/react';
7
7
  import DropdownItem from '@atlaskit/dropdown-menu/dropdown-menu-item';
8
8
  import DropdownItemGroup from '@atlaskit/dropdown-menu/dropdown-menu-item-group';
@@ -27,6 +27,7 @@ import IconTile from '@atlaskit/icon/icon-tile';
27
27
  import { ConfluenceIcon, JiraIcon } from '@atlaskit/logo';
28
28
  import { AtlassianIcon } from '@atlaskit/logo/atlassian-icon';
29
29
  import Lozenge from '@atlaskit/lozenge/lozenge';
30
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
30
31
  import { Box, Text, Inline, Anchor, Stack } from '@atlaskit/primitives/compiled';
31
32
  import Spinner from '@atlaskit/spinner/spinner';
32
33
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
@@ -49,6 +50,8 @@ const styles = {
49
50
  activationDropdownContent: "_1rjcze3t _1bsb1kdj",
50
51
  activationNoResultsContainer: "_vchhusvi _1bsb1kdj _ca0q1ejb _n3td1ejb",
51
52
  activationNoResultsContent: "_195g1wug _1bsb12aa _y3gn1h6o",
53
+ fieldAwareDropdownContent: "_1bsb1kdj",
54
+ fieldSecondLine: "_1reo15vq _18m915vq _o5721q9c _1bto1l2s",
52
55
  dropdownContent: "_1rjcv77o _1bsbsmdz _c71lko4j _1e0c1txw _1bah1h6o _4cvr1h6o",
53
56
  containerWithMinHeight: "_1tkeqkoa",
54
57
  contentContainer: "_y44vfmxe _1bsb1osq _1wpz1fhb _18m91wug",
@@ -57,6 +60,19 @@ const styles = {
57
60
  learnMoreLink: "_4bfu1r31 _1hmsglyw _ajmmnqa1",
58
61
  requestAccess: "_1bsb19n7 _o5721q9c _ahbq12x7 _syaz1rpy"
59
62
  };
63
+
64
+ /**
65
+ * One field-aware dropdown row. Every string the row shows is composed here from the untouched
66
+ * provider `reference`. Only built with `editor_synced_blocks_jira_custom_rich_text` on.
67
+ */
68
+
69
+ /** Decided once per fetch. The control shape is what the dropdown held before the experiment. */
70
+
71
+ const EMPTY_LOCATIONS = {
72
+ kind: 'control',
73
+ references: []
74
+ };
75
+ const countLocations = locations => locations.kind === 'control' ? locations.references.length : locations.items.length;
60
76
  const shouldApplyMinHeight = (fetchStatus, itemCount) => {
61
77
  // When there are 1/2 items, dropdown height is less than minHeight 144px
62
78
  return !(fetchStatus === 'success' && itemCount > 0);
@@ -287,6 +303,97 @@ export const processReferenceData = (referenceData, intl) => {
287
303
  });
288
304
  return sortedReferences;
289
305
  };
306
+ const isFieldAwareLocationList = references => {
307
+ var _references$some;
308
+ return ((_references$some = references === null || references === void 0 ? void 0 : references.some(reference => (reference === null || reference === void 0 ? void 0 : reference.productType) === 'jira-work-item')) !== null && _references$some !== void 0 ? _references$some : false) && isExperimentEnabled('editor_synced_blocks_jira_custom_rich_text');
309
+ };
310
+ const groupBySourceAri = referenceData => {
311
+ const groups = new Map();
312
+ referenceData === null || referenceData === void 0 ? void 0 : referenceData.forEach(reference => {
313
+ if (!reference) {
314
+ return;
315
+ }
316
+ const group = groups.get(reference.sourceAri);
317
+ if (group) {
318
+ group.push(reference);
319
+ } else {
320
+ groups.set(reference.sourceAri, [reference]);
321
+ }
322
+ });
323
+ return groups;
324
+ };
325
+
326
+ /** Sources first, then accessible locations. `0` leaves the title order to the caller. */
327
+ const compareLocationOrder = (a, b) => {
328
+ if (a.isSource !== b.isSource) {
329
+ return b.isSource ? 1 : -1;
330
+ }
331
+ if (a.hasAccess !== b.hasAccess) {
332
+ return a.hasAccess ? -1 : 1;
333
+ }
334
+ return 0;
335
+ };
336
+
337
+ /** The title alone. The field name is placed by the row and the tooltip, not baked in here. */
338
+ const getBaseTitle = ({
339
+ blockIndex,
340
+ formatMessage,
341
+ reference
342
+ }) => {
343
+ const title = reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title || reference.url || '';
344
+ return blockIndex === undefined ? title : `${title}: ${formatMessage(messages.syncedLocationDropdownTitleBlockIndex, {
345
+ index: blockIndex
346
+ })}`;
347
+ };
348
+
349
+ /** Jira rows are noted from `locationScope`; other rows keep the pre-experiment `onSameDocument` note. */
350
+ const getFieldAwareNote = (reference, formatMessage) => {
351
+ var _reference$locationSc;
352
+ if (reference.productType !== 'jira-work-item') {
353
+ if (!reference.onSameDocument) {
354
+ return undefined;
355
+ }
356
+ return formatMessage(reference.productType === 'confluence-page' ? messages.syncedLocationDropdownTitleNoteForConfluencePage : messages.syncedLocationDropdownTitleNoteForJiraWorkItem);
357
+ }
358
+
359
+ // A provider that computes no scope still reports `onSameDocument`. It is the same host
360
+ // comparison `getLocationScope` starts with, and in Jira the host document is one field.
361
+ const locationScope = (_reference$locationSc = reference.locationScope) !== null && _reference$locationSc !== void 0 ? _reference$locationSc : reference.onSameDocument ? 'same-document' : undefined;
362
+ switch (locationScope) {
363
+ case 'same-document':
364
+ return formatMessage(messages.syncedLocationDropdownTitleNoteForJiraWorkItemField);
365
+ case 'same-parent-document':
366
+ return formatMessage(messages.syncedLocationDropdownTitleNoteForJiraWorkItem);
367
+ default:
368
+ return undefined;
369
+ }
370
+ };
371
+ export const buildFieldAwareItems = (referenceData, formatMessage) => Array.from(groupBySourceAri(referenceData).values()).flatMap(group => group.map((reference, index) => {
372
+ const fieldName = reference.productType === 'jira-work-item' ? reference.fieldName : undefined;
373
+ const note = getFieldAwareNote(reference, formatMessage);
374
+ return {
375
+ baseTitle: getBaseTitle({
376
+ blockIndex: group.length > 1 ? index + 1 : undefined,
377
+ formatMessage,
378
+ reference
379
+ }),
380
+ ...(fieldName !== undefined && {
381
+ fieldName
382
+ }),
383
+ key: `${reference.sourceAri}#${index}`,
384
+ ...(note !== undefined && {
385
+ note
386
+ }),
387
+ reference
388
+ };
389
+ })).sort((a, b) => compareLocationOrder(a.reference, b.reference) || a.baseTitle.localeCompare(b.baseTitle));
390
+ const toSyncedLocations = (references, intl) => isFieldAwareLocationList(references) ? {
391
+ kind: 'field-aware',
392
+ items: buildFieldAwareItems(references, intl.formatMessage)
393
+ } : {
394
+ kind: 'control',
395
+ references: processReferenceData(references, intl)
396
+ };
290
397
  export const SyncedLocationDropdown = ({
291
398
  syncBlockStore,
292
399
  resourceId,
@@ -382,7 +489,7 @@ export const SyncedLocationDropdownWithCount = ({
382
489
  const [isOpen, setIsOpen] = useState(false);
383
490
  const {
384
491
  fetchStatus,
385
- referenceData
492
+ locations
386
493
  } = useReferenceData({
387
494
  intl,
388
495
  isSource,
@@ -390,16 +497,25 @@ export const SyncedLocationDropdownWithCount = ({
390
497
  resourceId,
391
498
  syncBlockStore
392
499
  });
393
- const referenceCount = useMemo(() => referenceData.filter(({
394
- isSource: isSourceItem
395
- }) => !isSourceItem).length, [referenceData]);
500
+ const referenceCount = useMemo(() => {
501
+ switch (locations.kind) {
502
+ case 'control':
503
+ return locations.references.filter(({
504
+ isSource: isSourceItem
505
+ }) => !isSourceItem).length;
506
+ case 'field-aware':
507
+ return locations.items.filter(({
508
+ reference
509
+ }) => !reference.isSource).length;
510
+ }
511
+ }, [locations]);
396
512
  const tooltipContent = !isOpen && fetchStatus === 'success' && referenceCount === 0 ? intl.formatMessage(messages.syncedLocationDropdownNoReferencesTooltip) : null;
397
513
  const content = isOpen ? /*#__PURE__*/React.createElement(DropdownContentWithReferenceData, {
398
514
  resourceId: resourceId,
399
515
  intl: intl,
400
516
  api: api,
401
517
  fetchStatus: fetchStatus,
402
- referenceData: referenceData
518
+ locations: locations
403
519
  }) : null;
404
520
  const toggleOpen = useCallback(() => {
405
521
  setIsOpen(currentIsOpen => !currentIsOpen);
@@ -462,7 +578,7 @@ const DropdownContent = ({
462
578
  api
463
579
  }) => {
464
580
  const [fetchStatus, setFetchStatus] = useState('none');
465
- const [referenceData, setReferenceData] = useState([]);
581
+ const [locations, setLocations] = useState(EMPTY_LOCATIONS);
466
582
  useEffect(() => {
467
583
  setFetchStatus('loading');
468
584
  const getReferenceData = async () => {
@@ -471,7 +587,7 @@ const DropdownContent = ({
471
587
  setFetchStatus('error');
472
588
  return;
473
589
  }
474
- setReferenceData(processReferenceData(response.references, intl));
590
+ setLocations(toSyncedLocations(response.references, intl));
475
591
  setFetchStatus('success');
476
592
  };
477
593
  void getReferenceData();
@@ -481,7 +597,7 @@ const DropdownContent = ({
481
597
  intl: intl,
482
598
  api: api,
483
599
  fetchStatus: fetchStatus,
484
- referenceData: referenceData
600
+ locations: locations
485
601
  });
486
602
  };
487
603
  const useReferenceData = ({
@@ -494,13 +610,13 @@ const useReferenceData = ({
494
610
  const [isNewSourceBlock] = useState(() => isSource && syncBlockStore.sourceManager.isNewSourceBlock(resourceId));
495
611
  const [state, setState] = useState(() => ({
496
612
  fetchStatus: isNewSourceBlock ? 'success' : 'loading',
497
- referenceData: []
613
+ locations: EMPTY_LOCATIONS
498
614
  }));
499
615
  useEffect(() => {
500
616
  let isCurrentRequest = true;
501
617
  setState({
502
618
  fetchStatus: isNewSourceBlock ? 'success' : 'loading',
503
- referenceData: []
619
+ locations: EMPTY_LOCATIONS
504
620
  });
505
621
  const getReferenceData = async () => {
506
622
  const response = await syncBlockStore.fetchReferencesSourceInfo(resourceId, localId, isSource);
@@ -513,13 +629,13 @@ const useReferenceData = ({
513
629
  if (response.error) {
514
630
  setState({
515
631
  fetchStatus: 'error',
516
- referenceData: []
632
+ locations: EMPTY_LOCATIONS
517
633
  });
518
634
  return;
519
635
  }
520
636
  setState({
521
637
  fetchStatus: 'success',
522
- referenceData: processReferenceData(response.references, intl)
638
+ locations: toSyncedLocations(response.references, intl)
523
639
  });
524
640
  };
525
641
  void getReferenceData();
@@ -529,7 +645,7 @@ const useReferenceData = ({
529
645
  }, [syncBlockStore, resourceId, intl, isSource, localId, isNewSourceBlock]);
530
646
  return {
531
647
  fetchStatus: state.fetchStatus,
532
- referenceData: state.referenceData
648
+ locations: state.locations
533
649
  };
534
650
  };
535
651
  const SyncedLocationTriggerContent = ({
@@ -568,11 +684,12 @@ const DropdownContentWithReferenceData = ({
568
684
  intl,
569
685
  api,
570
686
  fetchStatus,
571
- referenceData
687
+ locations
572
688
  }) => {
573
689
  const {
574
690
  formatMessage
575
691
  } = intl;
692
+ const locationCount = countLocations(locations);
576
693
  const handleLocationClick = () => {
577
694
  var _api$analytics, _api$analytics$action;
578
695
  api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent({
@@ -596,41 +713,22 @@ const DropdownContentWithReferenceData = ({
596
713
  formatMessage: formatMessage
597
714
  });
598
715
  case 'success':
599
- if (referenceData.length > 0) {
716
+ if (locationCount > 0) {
600
717
  return /*#__PURE__*/React.createElement("div", {
601
718
  "data-testid": "synced-locations-dropdown-content",
602
719
  className: ax([styles.contentContainer, "_ku9g126e _18bk1rpy"])
603
720
  }, /*#__PURE__*/React.createElement(DropdownItemGroup, {
604
721
  title: formatMessage(messages.syncedLocationDropdownHeading, {
605
- count: `${referenceData.length > 99 ? '99+' : referenceData.length}`
722
+ count: `${locationCount > 99 ? '99+' : locationCount}`
606
723
  })
607
- }, referenceData.map(reference => {
608
- const title = reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title || reference.url || '';
609
- return /*#__PURE__*/React.createElement("div", {
610
- key: reference.title,
611
- className: ax(["_2ll012x7"])
612
- }, /*#__PURE__*/React.createElement(Tooltip, {
613
- content: title
614
- }, /*#__PURE__*/React.createElement(DropdownItem, {
615
- elemBefore: /*#__PURE__*/React.createElement(ItemIcon, {
616
- reference: reference,
617
- intl: intl
618
- }),
619
- href: reference.url,
620
- target: "_blank",
621
- key: reference.title,
622
- rel: "noopener noreferrer"
623
- // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
624
- ,
625
- onClick: () => handleLocationClick()
626
- }, /*#__PURE__*/React.createElement(ItemTitle, {
627
- title: title,
628
- formatMessage: formatMessage,
629
- onSameDocument: reference.onSameDocument,
630
- isSource: reference.isSource,
631
- hasAccess: reference.hasAccess,
632
- productType: reference.productType
633
- }))));
724
+ }, locations.kind === 'field-aware' ? /*#__PURE__*/React.createElement(FieldAwareLocationRows, {
725
+ items: locations.items,
726
+ intl: intl,
727
+ handleLocationClick: handleLocationClick
728
+ }) : /*#__PURE__*/React.createElement(ControlLocationRows, {
729
+ referenceData: locations.references,
730
+ intl: intl,
731
+ handleLocationClick: handleLocationClick
634
732
  })));
635
733
  } else {
636
734
  return /*#__PURE__*/React.createElement(NoResultScreen, {
@@ -640,9 +738,121 @@ const DropdownContentWithReferenceData = ({
640
738
  }
641
739
  };
642
740
  return /*#__PURE__*/React.createElement(Box, {
643
- xcss: cx(styles.dropdownContent, expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true) && fetchStatus === 'success' && referenceData.length === 0 && styles.activationDropdownContent, shouldApplyMinHeight(fetchStatus, referenceData.length) && styles.containerWithMinHeight)
741
+ xcss: cx(styles.dropdownContent,
742
+ // Read off the discriminant rather than the experiment, so the control cohort
743
+ // cannot reach this width and no second exposure is fired for a style.
744
+ locations.kind === 'field-aware' && styles.fieldAwareDropdownContent, expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true) && fetchStatus === 'success' && locationCount === 0 && styles.activationDropdownContent, shouldApplyMinHeight(fetchStatus, locationCount) && styles.containerWithMinHeight)
644
745
  }, content());
645
746
  };
747
+ // The pre-experiment rows. This block is master's, moved into a component; remove with the experiment.
748
+ const ControlLocationRows = ({
749
+ referenceData,
750
+ intl,
751
+ handleLocationClick
752
+ }) => {
753
+ const {
754
+ formatMessage
755
+ } = intl;
756
+ return /*#__PURE__*/React.createElement(Fragment, null, referenceData.map(reference => {
757
+ const title = reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title || reference.url || '';
758
+ return /*#__PURE__*/React.createElement("div", {
759
+ key: reference.title,
760
+ className: ax(["_2ll012x7"])
761
+ }, /*#__PURE__*/React.createElement(Tooltip, {
762
+ content: title
763
+ }, /*#__PURE__*/React.createElement(DropdownItem, {
764
+ elemBefore: /*#__PURE__*/React.createElement(ItemIcon, {
765
+ reference: reference,
766
+ intl: intl
767
+ }),
768
+ href: reference.url,
769
+ target: "_blank",
770
+ key: reference.title,
771
+ rel: "noopener noreferrer"
772
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
773
+ ,
774
+ onClick: () => handleLocationClick()
775
+ }, /*#__PURE__*/React.createElement(ItemTitle, {
776
+ title: title,
777
+ formatMessage: formatMessage,
778
+ onSameDocument: reference.onSameDocument,
779
+ isSource: reference.isSource,
780
+ hasAccess: reference.hasAccess,
781
+ productType: reference.productType
782
+ }))));
783
+ }));
784
+ };
785
+ const FieldAwareItemTitle = ({
786
+ item: {
787
+ baseTitle,
788
+ fieldName,
789
+ note,
790
+ reference
791
+ },
792
+ formatMessage
793
+ }) => {
794
+ const titleLine = /*#__PURE__*/React.createElement(Inline, null, /*#__PURE__*/React.createElement(Box, {
795
+ as: "span",
796
+ xcss: styles.title
797
+ }, baseTitle), note && /*#__PURE__*/React.createElement(Box, {
798
+ as: "span",
799
+ xcss: styles.note
800
+ }, "\xA0- ", note), reference.isSource && /*#__PURE__*/React.createElement(Box, {
801
+ as: "span",
802
+ xcss: styles.lozenge
803
+ }, /*#__PURE__*/React.createElement(Lozenge, null, formatMessage(messages.syncedLocationDropdownSourceLozenge))), !reference.hasAccess && /*#__PURE__*/React.createElement(Box, {
804
+ as: "span",
805
+ xcss: styles.requestAccess
806
+ }, formatMessage(messages.syncedLocationDropdownRequestAccess)));
807
+ if (fieldName === undefined) {
808
+ return titleLine;
809
+ }
810
+
811
+ // AGG localises the field name, so it is shown as given rather than wrapped in a message.
812
+ return /*#__PURE__*/React.createElement(Stack, null, titleLine, /*#__PURE__*/React.createElement(Box, {
813
+ as: "span",
814
+ xcss: styles.fieldSecondLine
815
+ }, /*#__PURE__*/React.createElement(Text, {
816
+ size: "small",
817
+ color: "color.text.subtlest"
818
+ }, fieldName)));
819
+ };
820
+ const FieldAwareLocationRows = ({
821
+ items,
822
+ intl,
823
+ handleLocationClick
824
+ }) => {
825
+ const {
826
+ formatMessage
827
+ } = intl;
828
+ return /*#__PURE__*/React.createElement(Fragment, null, items.map(item => /*#__PURE__*/React.createElement("div", {
829
+ key: item.key,
830
+ className: ax(["_2ll012x7"])
831
+ }, /*#__PURE__*/React.createElement(Tooltip, {
832
+ content: item.fieldName ?
833
+ /*#__PURE__*/
834
+ // Two lines in the tooltip's own text flow. Wrapping them in `Text` would
835
+ // default to `font.body` and override the container's `font.body.small`,
836
+ // so the field-aware tooltip would not match every other one.
837
+ React.createElement(Fragment, null, item.baseTitle, /*#__PURE__*/React.createElement("br", null), formatMessage(messages.syncedLocationDropdownTooltipFieldName, {
838
+ fieldName: item.fieldName
839
+ })) : item.baseTitle
840
+ }, /*#__PURE__*/React.createElement(DropdownItem, {
841
+ elemBefore: /*#__PURE__*/React.createElement(ItemIcon, {
842
+ reference: item.reference,
843
+ intl: intl
844
+ }),
845
+ href: item.reference.url,
846
+ target: "_blank",
847
+ rel: "noopener noreferrer"
848
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
849
+ ,
850
+ onClick: () => handleLocationClick()
851
+ }, /*#__PURE__*/React.createElement(FieldAwareItemTitle, {
852
+ item: item,
853
+ formatMessage: formatMessage
854
+ }))))));
855
+ };
646
856
  const LoadingScreen = ({
647
857
  formatMessage
648
858
  }) => {