@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
927 lines (909 loc) • 35.4 kB
JavaScript
/* SyncedLocationDropdown.tsx generated by @compiled/babel-plugin v3.0.2 */
import "./SyncedLocationDropdown.compiled.css";
import * as React from 'react';
import { ax, ix } from "@compiled/react/runtime";
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { cx } from '@compiled/react';
import DropdownItem from '@atlaskit/dropdown-menu/dropdown-menu-item';
import DropdownItemGroup from '@atlaskit/dropdown-menu/dropdown-menu-item-group';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { syncBlockMessages as messages } from '@atlaskit/editor-common/messages';
import { SYNCED_BLOCKS_DOCUMENTATION_URL } from '@atlaskit/editor-common/sync-block';
import { FloatingToolbarButton as Button } from '@atlaskit/editor-common/ui';
import { ArrowKeyNavigationType, DropdownContainer } from '@atlaskit/editor-common/ui-menu';
import { getPageIdAndTypeFromConfluencePageAri } from '@atlaskit/editor-synced-block-provider';
// eslint-disable-next-line import/order -- CI requires icon-lab imports before core icon imports.
import PageLiveDocIcon from '@atlaskit/icon-lab/core/page-live-doc';
import BugIcon from '@atlaskit/icon/core/bug';
import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
import EpicIcon from '@atlaskit/icon/core/epic';
import LockLockedIcon from '@atlaskit/icon/core/lock-locked';
import PageIcon from '@atlaskit/icon/core/page';
import QuotationMarkIcon from '@atlaskit/icon/core/quotation-mark';
import StatusErrorIcon from '@atlaskit/icon/core/status-error';
import StoryIcon from '@atlaskit/icon/core/story';
import SubtaskIcon from '@atlaskit/icon/core/subtasks';
import TaskIcon from '@atlaskit/icon/core/task';
import IconTile from '@atlaskit/icon/icon-tile';
import { ConfluenceIcon, JiraIcon } from '@atlaskit/logo';
import { AtlassianIcon } from '@atlaskit/logo/atlassian-icon';
import Lozenge from '@atlaskit/lozenge/lozenge';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { Box, Text, Inline, Anchor, Stack } from '@atlaskit/primitives/compiled';
import Spinner from '@atlaskit/spinner/spinner';
import Tooltip from '@atlaskit/tooltip/Tooltip';
import { SYNCED_BLOCK_BUTTON_TEST_ID } from '../types';
import { SyncedLocationsEmptyStateIllustration } from './assets/SyncedLocationsEmptyStateIllustration';
const fadeIn = null;
const headingStyles = null;
const dropdownItemStyles = null;
// logo icon does not fit in ADS IconTile, hence we need custom styles to match with other icons
const logoTileStyles = null;
// Breadcrumbs and smart links draw the work item avatar without a tile. The 20px box keeps
// Jira rows aligned with the tiled Confluence rows.
const issueTypeAvatarStyles = null;
const SYNCED_LOCATIONS_DROPDOWN_TEST_ID = 'synced-block-synced-locations-dropdown';
const styles = {
title: "_1reo15vq _18m915vq _syazazsu _1bto1l2s _o5721q9c",
note: "_syaz1rpy _o5721q9c",
lozenge: "_ahbq12x7 _1ul91wqb",
activationDropdownContent: "_1rjcze3t _1bsb1kdj",
activationNoResultsContainer: "_vchhusvi _1bsb1kdj _ca0q1ejb _n3td1ejb",
activationNoResultsContent: "_195g1wug _1bsb12aa _y3gn1h6o",
fieldAwareDropdownContent: "_1bsb1kdj",
fieldSecondLine: "_1reo15vq _18m915vq _o5721q9c _1bto1l2s",
dropdownContent: "_1rjcv77o _1bsbsmdz _c71lko4j _1e0c1txw _1bah1h6o _4cvr1h6o",
containerWithMinHeight: "_1tkeqkoa",
contentContainer: "_y44vfmxe _1bsb1osq _1wpz1fhb _18m91wug",
errorContainer: "_1bsbo8uj _1e0c1txw",
errorIcon: "_1mour5cr",
learnMoreLink: "_4bfu1r31 _1hmsglyw _ajmmnqa1",
requestAccess: "_1bsb19n7 _o5721q9c _ahbq12x7 _syaz1rpy",
restrictedIcon: "_1e0c1txw _4cvr1h6o _1wpz1h6o _ahbq1b66"
};
const isRestrictedLocation = reference => !reference.hasAccess && isExperimentEnabled('platform_editor_blocks_patch_11');
/** The user requests access on the location itself, so a restricted row only shows a lock. */
const NoAccessIndicator = ({
formatMessage
}) => isExperimentEnabled('platform_editor_blocks_patch_11') ? /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.restrictedIcon
}, /*#__PURE__*/React.createElement(LockLockedIcon, {
label: "",
size: "small",
color: "var(--ds-icon-danger, #C9372C)"
})) : /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.requestAccess
}, formatMessage(messages.syncedLocationDropdownRequestAccess));
/**
* One field-aware dropdown row. Every string the row shows is composed here from the untouched
* provider `reference`. Only built with `editor_synced_blocks_jira_custom_rich_text` on.
*/
/** Decided once per fetch. The control shape is what the dropdown held before the experiment. */
const EMPTY_LOCATIONS = {
kind: 'control',
references: []
};
const countLocations = locations => locations.kind === 'control' ? locations.references.length : locations.items.length;
const shouldApplyMinHeight = (fetchStatus, itemCount) => {
// When there are 1/2 items, dropdown height is less than minHeight 144px
return !(fetchStatus === 'success' && itemCount > 0);
};
const ItemTitle = ({
title,
formatMessage,
onSameDocument,
isSource,
hasAccess,
productType
}) => {
return /*#__PURE__*/React.createElement(Inline, null, /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.title
}, title), onSameDocument && /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.note
}, "\xA0-", ' ', formatMessage(productType === 'confluence-page' ? messages.syncedLocationDropdownTitleNoteForConfluencePage : messages.syncedLocationDropdownTitleNoteForJiraWorkItem)), isSource && /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.lozenge
}, /*#__PURE__*/React.createElement(Lozenge, null, formatMessage(messages.syncedLocationDropdownSourceLozenge))), !hasAccess && /*#__PURE__*/React.createElement(NoAccessIndicator, {
formatMessage: formatMessage
}));
};
const productIconMap = {
'confluence-page': ConfluenceIcon,
'jira-work-item': JiraIcon
};
const subTypeIconMap = {
live: PageLiveDocIcon,
page: PageIcon
};
const getConfluenceSubTypeIcon = (sourceAri, subType) => {
try {
const {
type: pageType
} = getPageIdAndTypeFromConfluencePageAri({
ari: sourceAri
});
if (pageType === 'blogpost') {
return QuotationMarkIcon;
} else {
return subType && subType in subTypeIconMap ? subTypeIconMap[subType] : PageIcon;
}
} catch {
return PageIcon;
}
};
const ProductIcon = ({
product
}) => {
var _productIconMap$produ;
const ProductIcon = product ? (_productIconMap$produ = productIconMap[product]) !== null && _productIconMap$produ !== void 0 ? _productIconMap$produ : AtlassianIcon : AtlassianIcon;
return /*#__PURE__*/React.createElement("span", {
className: ax(["_2rkol0p1 _bfhki8nm _1bsbgktf _4t3igktf _1e0c1txw _4cvr1h6o _1bah1h6o"])
}, /*#__PURE__*/React.createElement(ProductIcon, {
size: "xxsmall",
appearance: "neutral"
}));
};
// Map AGG issue-type names to ADS icons. The mapping is by the English `name` returned
// from AGG because Jira's REST/GraphQL API does not localise it at this layer. With
// `platform_editor_blocks_patch_11` the AGG `iconUrl` wins and these icons are the fallback.
//
// Type the icons as the same shape as `TaskIcon` so we don't import `NewCoreIconProps`
// from a private icon entrypoint.
const jiraIssueTypeIconMap = {
Task: {
icon: TaskIcon,
messageKey: 'syncedLocationDropdownIssueTypeTask'
},
Bug: {
icon: BugIcon,
messageKey: 'syncedLocationDropdownIssueTypeBug'
},
Story: {
icon: StoryIcon,
messageKey: 'syncedLocationDropdownIssueTypeStory'
},
Epic: {
icon: EpicIcon,
messageKey: 'syncedLocationDropdownIssueTypeEpic'
},
Subtask: {
icon: SubtaskIcon,
messageKey: 'syncedLocationDropdownIssueTypeSubtask'
},
'Sub-task': {
icon: SubtaskIcon,
messageKey: 'syncedLocationDropdownIssueTypeSubtask'
}
};
/**
* Creates an icon component from a custom Jira issue-type `iconUrl` that conforms to the
* ADS icon component contract expected by `IconTile`. This lets us reuse `IconTile` for
* custom issue types — ensuring consistent sizing, background, and border-radius with the
* standard ADS icons used for known issue types (Bug, Story, etc.).
*
* The returned component ignores ADS icon props (color, spacing, etc.) because the image
* is an external raster/SVG asset that doesn't respond to design tokens.
*/
const customIconCache = new Map();
const createCustomIssueTypeIcon = iconUrl => {
const cached = customIconCache.get(iconUrl);
if (cached) {
return cached;
}
const CustomIssueTypeIcon = () => /*#__PURE__*/React.createElement("img", {
src: iconUrl,
alt: "",
width: "12",
height: "12"
});
CustomIssueTypeIcon.displayName = 'CustomIssueTypeIcon';
customIconCache.set(iconUrl, CustomIssueTypeIcon);
return CustomIssueTypeIcon;
};
/**
* Returns the icon to render for a Jira issue type, or `null` when neither an ADS icon
* mapping nor an AGG-provided `iconUrl` is available so the caller can fall back to a
* generic product icon.
*
* Implemented as a plain function (not a React component) so the `null` check actually
* narrows — a JSX expression always evaluates to a truthy `ReactElement` object,
* meaning callers cannot distinguish a "would render nothing" component from one that
* renders an icon.
*/
const renderJiraIssueTypeIcon = (issueType, intl) => {
const mapped = jiraIssueTypeIconMap[issueType.name];
const label = intl.formatMessage(mapped ? messages[mapped.messageKey] : messages.syncedLocationDropdownIssueTypeGeneric);
if (issueType.iconUrl && isExperimentEnabled('platform_editor_blocks_patch_11')) {
return /*#__PURE__*/React.createElement("span", {
className: ax(["_1bsbgktf _4t3igktf _1e0c1txw _1o9zidpf _4cvr1h6o _1bah1h6o"])
}, /*#__PURE__*/React.createElement("img", {
src: issueType.iconUrl,
alt: label,
width: "16",
height: "16"
}));
}
if (mapped) {
return /*#__PURE__*/React.createElement(IconTile, {
icon: mapped.icon,
label: label,
appearance: 'gray',
size: "xsmall"
});
}
// Custom Jira issue types — render inside `IconTile` using a wrapper component so the
// icon gets the same tile background, border-radius, and sizing as known issue types.
if (issueType.iconUrl) {
const CustomIcon = createCustomIssueTypeIcon(issueType.iconUrl);
return /*#__PURE__*/React.createElement(IconTile, {
icon: CustomIcon,
label: label,
appearance: 'gray',
size: "xsmall"
});
}
return null;
};
const ItemIcon = ({
reference,
intl
}) => {
const {
hasAccess,
subType,
productType,
sourceAri,
issueType
} = reference;
if (productType === 'confluence-page' && hasAccess) {
return /*#__PURE__*/React.createElement(IconTile, {
icon: getConfluenceSubTypeIcon(sourceAri, subType),
label: "",
appearance: 'gray',
size: "xsmall"
});
}
// Render a Jira issue-type icon when we have one.
// Falls through to the generic product icon when:
// - we don't have access (issueType is not surfaced for no-access references),
// - AGG returned no `issueType` (partial index, deleted, etc.), or
// - the issue type is unrecognised AND has no `iconUrl`.
if (productType === 'jira-work-item' && hasAccess && issueType) {
const icon = renderJiraIssueTypeIcon(issueType, intl);
if (icon !== null) {
return icon;
}
}
// Generic product fallback for `jira-work-item` (and any future product).
return /*#__PURE__*/React.createElement(ProductIcon, {
product: productType
});
};
export const processReferenceData = (referenceData, intl) => {
const {
formatMessage
} = intl;
const sourceInfoMap = new Map();
referenceData === null || referenceData === void 0 ? void 0 : referenceData.forEach(reference => {
if (!reference) {
return;
}
if (sourceInfoMap.has(reference.sourceAri)) {
var _sourceInfoMap$get;
(_sourceInfoMap$get = sourceInfoMap.get(reference.sourceAri)) === null || _sourceInfoMap$get === void 0 ? void 0 : _sourceInfoMap$get.push(reference);
} else {
sourceInfoMap.set(reference.sourceAri, [reference]);
}
});
for (const references of sourceInfoMap.values()) {
if (references.length > 1) {
references.forEach((reference, index) => reference.title = `${reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title}: ${formatMessage(messages.syncedLocationDropdownTitleBlockIndex, {
index: index + 1
})}`);
}
}
const sortedReferences = Array.from(sourceInfoMap.values()).flat().sort((a, b) => {
if (a.isSource !== b.isSource) {
return b.isSource ? 1 : -1;
}
if (a.hasAccess !== b.hasAccess) {
return a.hasAccess ? -1 : 1;
}
return (a.title || '').localeCompare(b.title || '');
});
return sortedReferences;
};
const isFieldAwareLocationList = references => {
var _references$some;
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');
};
const groupBySourceAri = referenceData => {
const groups = new Map();
referenceData === null || referenceData === void 0 ? void 0 : referenceData.forEach(reference => {
if (!reference) {
return;
}
const group = groups.get(reference.sourceAri);
if (group) {
group.push(reference);
} else {
groups.set(reference.sourceAri, [reference]);
}
});
return groups;
};
/** Sources first, then accessible locations. `0` leaves the title order to the caller. */
const compareLocationOrder = (a, b) => {
if (a.isSource !== b.isSource) {
return b.isSource ? 1 : -1;
}
if (a.hasAccess !== b.hasAccess) {
return a.hasAccess ? -1 : 1;
}
return 0;
};
/** The title alone. The field name is placed by the row and the tooltip, not baked in here. */
const getBaseTitle = ({
blockIndex,
formatMessage,
reference
}) => {
if (isRestrictedLocation(reference)) {
return formatMessage(messages.syncedLocationDropdownRestrictedContent);
}
const title = reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title || reference.url || '';
return blockIndex === undefined ? title : `${title}: ${formatMessage(messages.syncedLocationDropdownTitleBlockIndex, {
index: blockIndex
})}`;
};
/** Jira rows are noted from `locationScope`; other rows keep the pre-experiment `onSameDocument` note. */
const getFieldAwareNote = (reference, formatMessage) => {
var _reference$locationSc;
if (reference.productType !== 'jira-work-item') {
if (!reference.onSameDocument) {
return undefined;
}
return formatMessage(reference.productType === 'confluence-page' ? messages.syncedLocationDropdownTitleNoteForConfluencePage : messages.syncedLocationDropdownTitleNoteForJiraWorkItem);
}
// A provider that computes no scope still reports `onSameDocument`. It is the same host
// comparison `getLocationScope` starts with, and in Jira the host document is one field.
const locationScope = (_reference$locationSc = reference.locationScope) !== null && _reference$locationSc !== void 0 ? _reference$locationSc : reference.onSameDocument ? 'same-document' : undefined;
switch (locationScope) {
case 'same-document':
return formatMessage(messages.syncedLocationDropdownTitleNoteForJiraWorkItemField);
case 'same-parent-document':
return formatMessage(messages.syncedLocationDropdownTitleNoteForJiraWorkItem);
default:
return undefined;
}
};
export const buildFieldAwareItems = (referenceData, formatMessage) => Array.from(groupBySourceAri(referenceData).values()).flatMap(group => group.map((reference, index) => {
const fieldName = reference.productType === 'jira-work-item' ? reference.fieldName : undefined;
const note = getFieldAwareNote(reference, formatMessage);
return {
baseTitle: getBaseTitle({
blockIndex: group.length > 1 ? index + 1 : undefined,
formatMessage,
reference
}),
...(fieldName !== undefined && {
fieldName
}),
key: `${reference.sourceAri}#${index}`,
...(note !== undefined && {
note
}),
reference
};
})).sort((a, b) => compareLocationOrder(a.reference, b.reference) || a.baseTitle.localeCompare(b.baseTitle));
const toSyncedLocations = (references, intl) => isFieldAwareLocationList(references) ? {
kind: 'field-aware',
items: buildFieldAwareItems(references, intl.formatMessage)
} : {
kind: 'control',
references: processReferenceData(references, intl)
};
export const SyncedLocationDropdown = ({
syncBlockStore,
resourceId,
intl,
isSource,
localId,
api,
floatingToolbarRenderContext
}) => {
return /*#__PURE__*/React.createElement(EditorPositionedSyncedLocationDropdown, {
syncBlockStore: syncBlockStore,
resourceId: resourceId,
intl: intl,
isSource: isSource,
localId: localId,
api: api,
floatingToolbarRenderContext: floatingToolbarRenderContext
});
};
const EditorPositionedSyncedLocationDropdown = ({
syncBlockStore,
resourceId,
intl,
isSource,
localId,
api,
floatingToolbarRenderContext
}) => {
const triggerTitle = intl.formatMessage(messages.syncedLocationDropdownTitle);
const [isOpen, setIsOpen] = useState(false);
const content = isOpen ? /*#__PURE__*/React.createElement(DropdownContent, {
syncBlockStore: syncBlockStore,
resourceId: resourceId,
intl: intl,
isSource: isSource,
localId: localId,
api: api
}) : null;
const toggleOpen = useCallback(() => {
setIsOpen(currentIsOpen => !currentIsOpen);
}, []);
const closeDropdown = useCallback(() => {
setIsOpen(false);
}, []);
const setDisableParentScroll = floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.setDisableParentScroll;
useEffect(() => {
if (!isOpen) {
return;
}
setDisableParentScroll === null || setDisableParentScroll === void 0 ? void 0 : setDisableParentScroll(true);
return () => {
setDisableParentScroll === null || setDisableParentScroll === void 0 ? void 0 : setDisableParentScroll(false);
};
}, [isOpen, setDisableParentScroll]);
const trigger = useMemo(() => /*#__PURE__*/React.createElement(Button, {
areAnyNewToolbarFlagsEnabled: true,
testId: SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSyncedLocationsTrigger,
selected: isOpen,
iconAfter: /*#__PURE__*/React.createElement(ChevronDownIcon, {
color: "currentColor",
spacing: "spacious",
label: "",
size: "small"
}),
onClick: toggleOpen,
ariaHasPopup: true
}, triggerTitle), [isOpen, toggleOpen, triggerTitle]);
return /*#__PURE__*/React.createElement(DropdownContainer, {
testId: SYNCED_LOCATIONS_DROPDOWN_TEST_ID,
isOpen: isOpen,
trigger: trigger,
handleClickOutside: closeDropdown,
handleEscapeKeydown: closeDropdown,
mountTo: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsMountPoint,
boundariesElement: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsBoundariesElement,
scrollableElement: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsScrollableElement
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
arrowKeyNavigationProviderOptions: {
type: ArrowKeyNavigationType.MENU
}
}, content);
};
export const SyncedLocationDropdownWithCount = ({
syncBlockStore,
resourceId,
intl,
isSource,
localId,
api,
floatingToolbarRenderContext
}) => {
const [isOpen, setIsOpen] = useState(false);
const {
fetchStatus,
locations
} = useReferenceData({
intl,
isSource,
localId,
resourceId,
syncBlockStore
});
const referenceCount = useMemo(() => {
switch (locations.kind) {
case 'control':
return locations.references.filter(({
isSource: isSourceItem
}) => !isSourceItem).length;
case 'field-aware':
return locations.items.filter(({
reference
}) => !reference.isSource).length;
}
}, [locations]);
const tooltipContent = !isOpen && fetchStatus === 'success' && referenceCount === 0 ? intl.formatMessage(messages.syncedLocationDropdownNoReferencesTooltip) : null;
const content = isOpen ? /*#__PURE__*/React.createElement(DropdownContentWithReferenceData, {
resourceId: resourceId,
intl: intl,
api: api,
fetchStatus: fetchStatus,
locations: locations
}) : null;
const toggleOpen = useCallback(() => {
setIsOpen(currentIsOpen => !currentIsOpen);
}, []);
const closeDropdown = useCallback(() => {
setIsOpen(false);
}, []);
const setDisableParentScroll = floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.setDisableParentScroll;
useEffect(() => {
if (!isOpen) {
return;
}
setDisableParentScroll === null || setDisableParentScroll === void 0 ? void 0 : setDisableParentScroll(true);
return () => {
setDisableParentScroll === null || setDisableParentScroll === void 0 ? void 0 : setDisableParentScroll(false);
};
}, [isOpen, setDisableParentScroll]);
const trigger = useMemo(() => /*#__PURE__*/React.createElement(Button, {
areAnyNewToolbarFlagsEnabled: true,
testId: SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSyncedLocationsTrigger,
selected: isOpen,
iconAfter: /*#__PURE__*/React.createElement(ChevronDownIcon, {
color: "currentColor",
spacing: "spacious",
label: "",
size: "small"
}),
onClick: toggleOpen,
ariaHasPopup: true,
tooltipContent: tooltipContent !== null && tooltipContent !== void 0 ? tooltipContent : undefined
}, /*#__PURE__*/React.createElement(SyncedLocationTriggerContent, {
intl: intl,
fetchStatus: fetchStatus,
referenceCount: referenceCount
})), [fetchStatus, intl, isOpen, referenceCount, toggleOpen, tooltipContent]);
return /*#__PURE__*/React.createElement(DropdownContainer, {
alignDropdownWithParentElement: true,
alignX: "left",
testId: SYNCED_LOCATIONS_DROPDOWN_TEST_ID,
isOpen: isOpen,
trigger: trigger,
handleClickOutside: closeDropdown,
handleEscapeKeydown: closeDropdown,
mountTo: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsMountPoint,
boundariesElement: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsBoundariesElement,
scrollableElement: floatingToolbarRenderContext === null || floatingToolbarRenderContext === void 0 ? void 0 : floatingToolbarRenderContext.popupsScrollableElement
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
arrowKeyNavigationProviderOptions: {
type: ArrowKeyNavigationType.MENU
}
}, content);
};
const DropdownContent = ({
syncBlockStore,
resourceId,
intl,
isSource,
localId,
api
}) => {
const [fetchStatus, setFetchStatus] = useState('none');
const [locations, setLocations] = useState(EMPTY_LOCATIONS);
useEffect(() => {
setFetchStatus('loading');
const getReferenceData = async () => {
const response = await syncBlockStore.fetchReferencesSourceInfo(resourceId, localId, isSource);
if (response.error) {
setFetchStatus('error');
return;
}
setLocations(toSyncedLocations(response.references, intl));
setFetchStatus('success');
};
void getReferenceData();
}, [syncBlockStore, intl, isSource, localId, resourceId]);
return /*#__PURE__*/React.createElement(DropdownContentWithReferenceData, {
resourceId: resourceId,
intl: intl,
api: api,
fetchStatus: fetchStatus,
locations: locations
});
};
const useReferenceData = ({
syncBlockStore,
resourceId,
intl,
isSource,
localId
}) => {
const [isNewSourceBlock] = useState(() => isSource && syncBlockStore.sourceManager.isNewSourceBlock(resourceId));
const [state, setState] = useState(() => ({
fetchStatus: isNewSourceBlock ? 'success' : 'loading',
locations: EMPTY_LOCATIONS
}));
useEffect(() => {
let isCurrentRequest = true;
setState({
fetchStatus: isNewSourceBlock ? 'success' : 'loading',
locations: EMPTY_LOCATIONS
});
const getReferenceData = async () => {
const response = await syncBlockStore.fetchReferencesSourceInfo(resourceId, localId, isSource);
if (!isCurrentRequest) {
return;
}
if (isNewSourceBlock) {
syncBlockStore.sourceManager.clearNewSourceBlock(resourceId);
}
if (response.error) {
setState({
fetchStatus: 'error',
locations: EMPTY_LOCATIONS
});
return;
}
setState({
fetchStatus: 'success',
locations: toSyncedLocations(response.references, intl)
});
};
void getReferenceData();
return () => {
isCurrentRequest = false;
};
}, [syncBlockStore, resourceId, intl, isSource, localId, isNewSourceBlock]);
return {
fetchStatus: state.fetchStatus,
locations: state.locations
};
};
const SyncedLocationTriggerContent = ({
intl,
fetchStatus,
referenceCount
}) => {
const {
formatMessage
} = intl;
const triggerTitle = formatMessage(messages.syncedLocationDropdownTitle);
const syncedLocationCount = referenceCount === 0 ? 0 : referenceCount + 1;
switch (fetchStatus) {
case 'loading':
return /*#__PURE__*/React.createElement(Inline, {
alignBlock: "center",
space: "space.050"
}, triggerTitle, /*#__PURE__*/React.createElement(Spinner, {
size: "small",
label: formatMessage(messages.syncedLocationDropdownLoading)
}));
case 'success':
{
const count = syncedLocationCount > 99 ? '99+' : intl.formatNumber(syncedLocationCount);
return formatMessage(messages.syncedLocationDropdownTitleWithCount, {
count
});
}
case 'none':
case 'error':
return triggerTitle;
}
};
const DropdownContentWithReferenceData = ({
resourceId,
intl,
api,
fetchStatus,
locations
}) => {
const {
formatMessage
} = intl;
const locationCount = countLocations(locations);
const handleLocationClick = () => {
var _api$analytics, _api$analytics$action;
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({
eventType: EVENT_TYPE.OPERATIONAL,
action: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_CLICK_SYNCED_LOCATION,
attributes: {
resourceId
}
});
};
const content = () => {
switch (fetchStatus) {
case 'loading':
return /*#__PURE__*/React.createElement(LoadingScreen, {
formatMessage: formatMessage
});
case 'error':
return /*#__PURE__*/React.createElement(ErrorScreen, {
formatMessage: formatMessage
});
case 'success':
if (locationCount > 0) {
return /*#__PURE__*/React.createElement("div", {
"data-testid": "synced-locations-dropdown-content",
className: ax([styles.contentContainer, "_ku9g126e _18bk1rpy"])
}, /*#__PURE__*/React.createElement(DropdownItemGroup, {
title: formatMessage(messages.syncedLocationDropdownHeading, {
count: `${locationCount > 99 ? '99+' : locationCount}`
})
}, locations.kind === 'field-aware' ? /*#__PURE__*/React.createElement(FieldAwareLocationRows, {
items: locations.items,
intl: intl,
handleLocationClick: handleLocationClick
}) : /*#__PURE__*/React.createElement(ControlLocationRows, {
referenceData: locations.references,
intl: intl,
handleLocationClick: handleLocationClick
})));
} else {
return /*#__PURE__*/React.createElement(NoResultScreen, {
formatMessage: formatMessage
});
}
}
};
return /*#__PURE__*/React.createElement(Box, {
xcss: cx(styles.dropdownContent, locations.kind === 'field-aware' && styles.fieldAwareDropdownContent, fetchStatus === 'success' && locationCount === 0 && styles.activationDropdownContent, shouldApplyMinHeight(fetchStatus, locationCount) && styles.containerWithMinHeight)
}, content());
};
// The pre-experiment rows. This block is master's, moved into a component; remove with the experiment.
const ControlLocationRows = ({
referenceData,
intl,
handleLocationClick
}) => {
const {
formatMessage
} = intl;
return /*#__PURE__*/React.createElement(Fragment, null, referenceData.map((reference, index) => {
const title = isRestrictedLocation(reference) ? formatMessage(messages.syncedLocationDropdownRestrictedContent) : reference.title === '' && reference.hasAccess ? formatMessage(messages.syncedLocationDropdownUntitledPage) : reference.title || reference.url || '';
return /*#__PURE__*/React.createElement("div", {
key: isExperimentEnabled('platform_editor_blocks_patch_11') ? `${reference.sourceAri}#${index}` : reference.title,
className: ax(["_2ll012x7"])
}, /*#__PURE__*/React.createElement(Tooltip, {
content: title
}, /*#__PURE__*/React.createElement(DropdownItem, {
elemBefore: /*#__PURE__*/React.createElement(ItemIcon, {
reference: reference,
intl: intl
}),
href: reference.url,
target: "_blank",
key: reference.title,
rel: "noopener noreferrer"
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onClick: () => handleLocationClick()
}, /*#__PURE__*/React.createElement(ItemTitle, {
title: title,
formatMessage: formatMessage,
onSameDocument: reference.onSameDocument,
isSource: reference.isSource,
hasAccess: reference.hasAccess,
productType: reference.productType
}))));
}));
};
const FieldAwareItemTitle = ({
item: {
baseTitle,
fieldName,
note,
reference
},
formatMessage
}) => {
const titleLine = /*#__PURE__*/React.createElement(Inline, null, /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.title
}, baseTitle), note && /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.note
}, "\xA0- ", note), reference.isSource && /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.lozenge
}, /*#__PURE__*/React.createElement(Lozenge, null, formatMessage(messages.syncedLocationDropdownSourceLozenge))), !reference.hasAccess && /*#__PURE__*/React.createElement(NoAccessIndicator, {
formatMessage: formatMessage
}));
if (fieldName === undefined) {
return titleLine;
}
// AGG localises the field name, so it is shown as given rather than wrapped in a message.
return /*#__PURE__*/React.createElement(Stack, null, titleLine, /*#__PURE__*/React.createElement(Box, {
as: "span",
xcss: styles.fieldSecondLine
}, /*#__PURE__*/React.createElement(Text, {
size: "small",
color: "color.text.subtlest"
}, fieldName)));
};
const FieldAwareLocationRows = ({
items,
intl,
handleLocationClick
}) => {
const {
formatMessage
} = intl;
return /*#__PURE__*/React.createElement(Fragment, null, items.map(item => /*#__PURE__*/React.createElement("div", {
key: item.key,
className: ax(["_2ll012x7"])
}, /*#__PURE__*/React.createElement(Tooltip, {
content: item.fieldName ?
/*#__PURE__*/
// Two lines in the tooltip's own text flow. Wrapping them in `Text` would
// default to `font.body` and override the container's `font.body.small`,
// so the field-aware tooltip would not match every other one.
React.createElement(Fragment, null, item.baseTitle, /*#__PURE__*/React.createElement("br", null), formatMessage(messages.syncedLocationDropdownTooltipFieldName, {
fieldName: item.fieldName
})) : item.baseTitle
}, /*#__PURE__*/React.createElement(DropdownItem, {
elemBefore: /*#__PURE__*/React.createElement(ItemIcon, {
reference: item.reference,
intl: intl
}),
href: item.reference.url,
target: "_blank",
rel: "noopener noreferrer"
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onClick: () => handleLocationClick()
}, /*#__PURE__*/React.createElement(FieldAwareItemTitle, {
item: item,
formatMessage: formatMessage
}))))));
};
const LoadingScreen = ({
formatMessage
}) => {
return /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Spinner, {
label: formatMessage(messages.syncedLocationDropdownLoading)
}));
};
const ErrorScreen = ({
formatMessage
}) => {
return /*#__PURE__*/React.createElement(Box, {
xcss: styles.errorContainer,
testId: "synced-locations-dropdown-content-error"
}, /*#__PURE__*/React.createElement(Box, {
xcss: styles.errorIcon
}, /*#__PURE__*/React.createElement(StatusErrorIcon, {
color: "var(--ds-icon-danger, #C9372C)",
spacing: "spacious",
label: "",
size: "small"
})), /*#__PURE__*/React.createElement(Text, {
as: "p",
size: "medium"
}, formatMessage(messages.syncedLocationDropdownError)));
};
const NoResultScreen = ({
formatMessage
}) => {
return /*#__PURE__*/React.createElement(Box, {
xcss: styles.activationNoResultsContainer,
testId: "synced-locations-dropdown-content-no-results"
}, /*#__PURE__*/React.createElement(Stack, {
alignInline: "center",
xcss: styles.activationNoResultsContent,
space: "space.150"
}, /*#__PURE__*/React.createElement(SyncedLocationsEmptyStateIllustration, null), /*#__PURE__*/React.createElement(Text, {
as: "p"
}, formatMessage(messages.syncedLocationDropdownActivationNoResults)), /*#__PURE__*/React.createElement(Text, {
as: "p"
}, /*#__PURE__*/React.createElement(Anchor, {
href: SYNCED_BLOCKS_DOCUMENTATION_URL,
target: "_blank",
rel: "noopener noreferrer",
xcss: styles.learnMoreLink
}, formatMessage(messages.syncedLocationDropdownLearnMoreLink)))));
};