UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

927 lines (909 loc) • 35.4 kB
/* 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))))); };