@atlaskit/profilecard
Version:
A React component to display a card with user information.
401 lines • 15.2 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useIntl } from 'react-intl-next';
import { GiveKudosLauncherLazy, KudosType } from '@atlaskit/give-kudos';
import { fg } from '@atlaskit/platform-feature-flags';
import Popup from '@atlaskit/popup';
import { useAnalyticsEvents } from '@atlaskit/teams-app-internal-analytics';
import { layers } from '@atlaskit/theme/constants';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import filterActionsInner from '../../internal/filterActions';
import getLabelMessage from '../../internal/getLabelMessage';
import { CardWrapper } from '../../styled/UserTrigger';
import { PACKAGE_META_DATA } from '../../util/analytics';
import { DELAY_MS_HIDE, DELAY_MS_SHOW } from '../../util/config';
import { getPageTime } from '../../util/performance';
import { AgentProfileCardResourced } from '../Agent/AgentProfileCardResourced';
import { ProfileCardLazy } from './lazyProfileCard';
import UserLoadingState from './UserLoadingState';
function ProfileCardContent({
profilecardProps,
userId,
cloudId,
resourceClient,
trigger,
isAgent,
profileCardAction,
hasError,
errorType,
agentActions,
isRenderedInPortal,
addFlag,
hideAgentMoreActions,
hideAiDisclaimer
}) {
if (isAgent) {
return /*#__PURE__*/React.createElement(AgentProfileCardResourced, {
accountId: userId,
cloudId: cloudId,
resourceClient: resourceClient,
trigger: trigger,
onChatClick: agentActions === null || agentActions === void 0 ? void 0 : agentActions.onChatClick,
onConversationStartersClick: agentActions === null || agentActions === void 0 ? void 0 : agentActions.onConversationStartersClick,
addFlag: addFlag,
hideMoreActions: fg('jira_ai_profilecard_hide_agent_actions') && !!hideAgentMoreActions,
hideConversationStarters: fg('jira_ai_hide_conversation_starters_profilecard') && !!profilecardProps.hideAgentConversationStarters,
hideAiDisclaimer: hideAiDisclaimer
});
} else {
return /*#__PURE__*/React.createElement(Suspense, {
fallback: null
}, /*#__PURE__*/React.createElement(ProfileCardLazy, _extends({}, profilecardProps, {
isRenderedInPortal: isRenderedInPortal,
actions: profileCardAction,
hasError: hasError,
errorType: errorType,
withoutElevation: true
})));
}
}
export default function ProfilecardTriggerNext({
autoFocus,
trigger = 'hover',
userId,
cloudId,
resourceClient,
actions = [],
position = 'bottom-start',
children,
testId,
addFlag,
onReportingLinesClick,
ariaLabel,
ariaLabelledBy,
prepopulatedData,
disabledAriaAttributes,
onVisibilityChange,
offset,
viewingUserId,
product,
agentActions,
hideAgentMoreActions,
hideAiDisclaimer,
hideAgentConversationStarters,
hideReportingLines,
ariaHideProfileTrigger = false,
isVisible: propsIsVisible,
isRenderedInPortal,
ssrPlaceholderId,
showDelay: customShowDelay,
hideDelay: customHideDelay
}) {
const {
formatMessage
} = useIntl();
const isMounted = useRef(false);
const [visible, setVisible] = useState(false);
const showTimer = useRef(0);
const hideTimer = useRef(0);
const isExternalControl = propsIsVisible !== undefined && propsIsVisible !== visible;
const showDelay = trigger === 'click' || isExternalControl && fg('fix_profilecard_trigger_isvisible') ? 0 : customShowDelay !== null && customShowDelay !== void 0 ? customShowDelay : DELAY_MS_SHOW;
const hideDelay = trigger === 'click' || isExternalControl && fg('fix_profilecard_trigger_isvisible') ? 0 : customHideDelay !== null && customHideDelay !== void 0 ? customHideDelay : DELAY_MS_HIDE;
const [isLoading, setIsLoading] = useState(undefined);
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const [reportingLinesData, setReportingLinesData] = useState(undefined);
const [shouldShowGiveKudos, setShouldShowGiveKudos] = useState(false);
const [teamCentralBaseUrl, setTeamCentralBaseUrl] = useState(undefined);
const [kudosDrawerOpen, setKudosDrawerOpen] = useState(false);
const [isTriggeredUsingKeyboard, setTriggeredUsingKeyboard] = useState(false);
const triggerRef = useRef(null);
const {
fireEvent
} = useAnalyticsEvents();
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
clearTimeout(showTimer.current);
clearTimeout(hideTimer.current);
};
}, []);
useEffect(() => {
// Reset state when the userId changes
setIsLoading(undefined);
setHasError(false);
setError(null);
setData(null);
setReportingLinesData(undefined);
setShouldShowGiveKudos(false);
setTeamCentralBaseUrl(undefined);
}, [userId]);
const fireAnalytics = useCallback((eventKey, ...attributes) => {
if (!isMounted.current) {
return;
}
fireEvent(eventKey, ...attributes);
}, [fireEvent]);
const hideProfilecard = useCallback(() => {
clearTimeout(showTimer.current);
clearTimeout(hideTimer.current);
if (!isTriggeredUsingKeyboard) {
hideTimer.current = window.setTimeout(() => {
setVisible(false);
onVisibilityChange && onVisibilityChange(false);
}, hideDelay);
}
}, [hideDelay, isTriggeredUsingKeyboard, onVisibilityChange]);
const handleKeyboardClose = useCallback(event => {
if (event.key && event.key !== 'Escape') {
return;
}
if (triggerRef.current) {
triggerRef.current.focus();
}
setTriggeredUsingKeyboard(false);
setVisible(false);
onVisibilityChange && onVisibilityChange(false);
}, [setTriggeredUsingKeyboard, setVisible, onVisibilityChange]);
const handleClientSuccess = useCallback((profileData, reportingLinesData, shouldShowGiveKudos, teamCentralBaseUrl) => {
if (!isMounted.current) {
return;
}
setIsLoading(false);
setHasError(false);
setData(profileData);
setReportingLinesData(reportingLinesData);
setTeamCentralBaseUrl(teamCentralBaseUrl);
setShouldShowGiveKudos(shouldShowGiveKudos);
}, [setHasError, setIsLoading, setData, setReportingLinesData, setShouldShowGiveKudos]);
const handleClientError = useCallback(err => {
if (!isMounted.current) {
return;
}
setIsLoading(false);
setHasError(true);
setError(err);
}, [setHasError, setIsLoading, setError]);
const clientFetchProfile = useCallback(async () => {
if (isLoading === true) {
// don't fetch data when fetching is in process
return;
}
setIsLoading(true);
setHasError(false);
setError(null);
setData(null);
try {
const shouldHideReportingLines = fg('jira_ai_profilecard_hide_reportinglines') && hideReportingLines;
const requests = Promise.all([resourceClient.getProfile(cloudId || '', userId, fireAnalytics), shouldHideReportingLines ? Promise.resolve({
managers: [],
reports: []
}) : resourceClient.getReportingLines(userId), resourceClient.shouldShowGiveKudos(), resourceClient.getTeamCentralBaseUrl({
withOrgContext: true,
withSiteContext: true
})]);
const responses = await requests;
handleClientSuccess(...responses);
} catch (err) {
handleClientError(err);
}
}, [cloudId, fireAnalytics, isLoading, resourceClient, userId, handleClientSuccess, handleClientError, hideReportingLines]);
const showProfilecard = useCallback(() => {
clearTimeout(hideTimer.current);
clearTimeout(showTimer.current);
showTimer.current = window.setTimeout(() => {
if (!visible) {
void clientFetchProfile();
setVisible(true);
onVisibilityChange && onVisibilityChange(true);
}
}, showDelay);
}, [showDelay, visible, clientFetchProfile, onVisibilityChange]);
const onClick = useCallback(event => {
// If the user clicks on the trigger then we don't want that click event to
// propagate out to parent containers. For example when clicking a mention
// lozenge in an inline-edit.
event.stopPropagation();
showProfilecard();
if (!visible) {
fireAnalytics('ui.profilecard.triggered', {
method: 'click',
firedAt: Math.round(getPageTime()),
...PACKAGE_META_DATA
});
}
}, [fireAnalytics, showProfilecard, visible]);
const onMouseEnter = useCallback(() => {
showProfilecard();
if (!visible) {
fireAnalytics('ui.profilecard.triggered', {
method: 'hover',
firedAt: Math.round(getPageTime()),
...PACKAGE_META_DATA
});
}
}, [fireAnalytics, showProfilecard, visible]);
const onKeyPress = useCallback(event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setTriggeredUsingKeyboard(true);
showProfilecard();
if (!visible) {
fireAnalytics('ui.profilecard.triggered', {
method: 'click',
firedAt: Math.round(getPageTime()),
...PACKAGE_META_DATA
});
}
}
}, [fireAnalytics, showProfilecard, visible]);
const onFocus = useCallback(() => {
showProfilecard();
}, [showProfilecard]);
useEffect(() => {
if (!fg('fix_profilecard_trigger_isvisible')) {
return;
}
// If the prop isVisible is not defined, we don't want to do anything
if (propsIsVisible === undefined) {
return;
}
// If the prop isVisible is defined, we want to show or hide the profile card based on the value
if (propsIsVisible) {
showProfilecard();
} else {
hideProfilecard();
}
}, [hideProfilecard, propsIsVisible, showProfilecard]);
const containerListeners = useMemo(() => trigger === 'hover' ? {
onMouseEnter: onMouseEnter,
onMouseLeave: hideProfilecard,
onBlur: hideProfilecard,
onKeyPress: onKeyPress
} : {
onClick: onClick,
onKeyPress: onKeyPress
}, [hideProfilecard, onClick, onKeyPress, onMouseEnter, trigger]);
const filterActions = useCallback(() => {
return filterActionsInner(actions, data);
}, [actions, data]);
const openKudosDrawer = () => {
hideProfilecard();
setKudosDrawerOpen(true);
};
const closeKudosDrawer = () => {
setKudosDrawerOpen(false);
};
const showLoading = isLoading === true || isLoading === undefined;
const wrapperProps = useMemo(() => trigger === 'hover' ? {
onMouseEnter: onMouseEnter,
onMouseLeave: hideProfilecard,
onFocus: onFocus
} : {}, [hideProfilecard, onFocus, onMouseEnter, trigger]);
const profilecardProps = {
userId: userId,
fullName: prepopulatedData === null || prepopulatedData === void 0 ? void 0 : prepopulatedData.fullName,
isCurrentUser: userId === viewingUserId,
clientFetchProfile: clientFetchProfile,
...data,
reportingLines: reportingLinesData,
onReportingLinesClick: onReportingLinesClick,
isKudosEnabled: shouldShowGiveKudos,
teamCentralBaseUrl: teamCentralBaseUrl,
cloudId: cloudId,
openKudosDrawer: openKudosDrawer,
isTriggeredUsingKeyboard: isTriggeredUsingKeyboard,
disabledAriaAttributes: disabledAriaAttributes,
hideReportingLines: fg('jira_ai_profilecard_hide_reportinglines') && hideReportingLines,
hideAgentConversationStarters: fg('jira_ai_hide_conversation_starters_profilecard') && hideAgentConversationStarters
};
const ssrPlaceholderProp = ssrPlaceholderId ? {
'data-ssr-placeholder-replace': ssrPlaceholderId
} : {};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Popup, {
isOpen: !!visible,
onClose: event => {
hideProfilecard();
handleKeyboardClose(event);
},
placement: position,
offset: offset !== null && offset !== void 0 ? offset : [0, 8],
content: () => /*#__PURE__*/React.createElement("div", wrapperProps, showLoading ? /*#__PURE__*/React.createElement(LoadingView, {
fireAnalytics: fireAnalytics
}) : visible && /*#__PURE__*/React.createElement(ProfileCardContent, {
profilecardProps: profilecardProps,
isAgent: !!(data !== null && data !== void 0 && data.isAgent),
userId: userId,
cloudId: cloudId,
resourceClient: resourceClient,
viewingUserId: viewingUserId,
trigger: trigger,
product: product,
profileCardAction: filterActions(),
errorType: error,
hasError: hasError,
agentActions: agentActions,
addFlag: addFlag,
hideAgentMoreActions: hideAgentMoreActions,
hideAiDisclaimer: hideAiDisclaimer,
isRenderedInPortal: isRenderedInPortal
})),
trigger: triggerProps => {
const {
ref: callbackRef,
...innerProps
} = triggerProps;
const ref = element => {
triggerRef.current = element;
if (typeof callbackRef === 'function') {
callbackRef(element);
}
};
const {
'aria-expanded': _,
'aria-haspopup': __,
...restInnerProps
} = innerProps;
return /*#__PURE__*/React.createElement("span", _extends({}, disabledAriaAttributes ? restInnerProps : triggerProps, containerListeners, {
ref: ref,
"data-testid": testId
}, !ariaHideProfileTrigger && {
'aria-labelledby': ariaLabelledBy
}, disabledAriaAttributes ? {} : {
role: 'button',
// aria-hidden cannot contain focusable elements: https://dequeuniversity.com/rules/axe/3.5/aria-hidden-focus
tabIndex: ariaHideProfileTrigger ? -1 : 0,
'aria-label': ariaHideProfileTrigger ? undefined : getLabelMessage(ariaLabel, profilecardProps.fullName, formatMessage)
}, ariaHideProfileTrigger && {
'aria-hidden': 'true'
}, ssrPlaceholderProp), children);
},
zIndex: layers.modal(),
shouldUseCaptureOnOutsideClick: true,
autoFocus: expValEquals('editor_a11y_7152_profile_card_tab_order', 'isEnabled', true) ? isRenderedInPortal && isTriggeredUsingKeyboard ? false : autoFocus !== null && autoFocus !== void 0 ? autoFocus : trigger === 'click' : autoFocus !== null && autoFocus !== void 0 ? autoFocus : trigger === 'click'
// This feature gate is currently enabled only for Jira_Web to avoid UI issues in Confluence_Web.
,
shouldRenderToParent: fg('enable_appropriate_reading_order_in_profile_card'),
shouldDisableFocusLock: fg('enable_appropriate_reading_order_in_profile_card')
}), shouldShowGiveKudos && teamCentralBaseUrl && /*#__PURE__*/React.createElement(Suspense, {
fallback: null
}, /*#__PURE__*/React.createElement(GiveKudosLauncherLazy, {
isOpen: kudosDrawerOpen,
recipient: {
type: KudosType.INDIVIDUAL,
recipientId: userId
},
analyticsSource: "profile-card",
teamCentralBaseUrl: teamCentralBaseUrl,
cloudId: cloudId,
addFlag: addFlag,
onClose: closeKudosDrawer
})));
}
const LoadingView = ({
fireAnalytics
}) => /*#__PURE__*/React.createElement(CardWrapper, {
testId: "profilecard.profilecardtrigger.loading"
}, /*#__PURE__*/React.createElement(UserLoadingState, {
fireAnalytics: fireAnalytics
}));