@atlaskit/editor-plugin-mentions
Version:
Mentions plugin for @atlaskit/editor-core
313 lines (312 loc) • 14.2 kB
JavaScript
import React, { useEffect, useMemo } from 'react';
import { useIntl } from 'react-intl';
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { toolbarInsertBlockMessages as messages, mentionMessages } from '@atlaskit/editor-common/messages';
import { WithProviders } from '@atlaskit/editor-common/provider-factory';
import { IconMention } from '@atlaskit/editor-common/assets';
import { isResolvingMentionProvider } from '@atlaskit/mention/resource';
import { MentionNameStatus, isPromise } from '@atlaskit/mention/types';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { insertMention } from './editor-commands';
import { mentionNodeSpec } from './nodeviews/mentionNodeSpec';
import { agentMentionPluginKey, createAgentMentionPlugin } from './pm-plugins/agent';
import { mentionPluginKey } from './pm-plugins/key';
import { ACTIONS, createMentionPlugin } from './pm-plugins/main';
import { InlineInvitePopupContainer } from './ui/InlineInvitePopupContainer';
import { SecondaryToolbarComponent } from './ui/SecondaryToolbarComponent';
import { createTypeAheadConfig } from './ui/type-ahead';
import { getMentionQuickInsertComponents } from './ui/quick-insert/getMentionQuickInsertComponents';
const processName = (name, intl) => {
const unknownLabel = intl.formatMessage(mentionMessages.unknownLabel);
if (name.status === MentionNameStatus.OK) {
return `@${name.name || unknownLabel}`;
} else {
return `@${unknownLabel}`;
}
};
/**
* We will need to clean this up once mentionProvider is
* put inside mention plugin.
* See: https://product-fabric.atlassian.net/browse/ED-26011
*/
function Component({
mentionProvider,
api
}) {
const mentionProviderMemo = useMemo(() => {
return mentionProvider;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const intl = useIntl();
useEffect(() => {
mentionProviderMemo === null || mentionProviderMemo === void 0 ? void 0 : mentionProviderMemo.then(mentionProviderSync => {
var _api$base, _api$base$actions;
api === null || api === void 0 ? void 0 : (_api$base = api.base) === null || _api$base === void 0 ? void 0 : (_api$base$actions = _api$base.actions) === null || _api$base$actions === void 0 ? void 0 : _api$base$actions.registerMarks(({
tr,
node,
pos
}) => {
const {
doc
} = tr;
const {
schema
} = doc.type;
const {
mention: mentionNodeType
} = schema.nodes;
const {
id
} = node.attrs;
if (node.type === mentionNodeType) {
if (isResolvingMentionProvider(mentionProviderSync)) {
const nameDetail = mentionProviderSync === null || mentionProviderSync === void 0 ? void 0 : mentionProviderSync.resolveMentionName(id);
let newText;
if (isPromise(nameDetail)) {
newText = `@${intl.formatMessage(mentionMessages.unknownLabel)}`;
} else {
newText = processName(nameDetail, intl);
}
const currentPos = tr.mapping.map(pos);
tr.replaceWith(currentPos, currentPos + node.nodeSize, schema.text(newText, node.marks));
}
}
});
});
}, [mentionProviderMemo, api, intl]);
return null;
}
const mentionsPlugin = ({
config: options,
api
}) => {
var _options$sanitizePriv, _options$insertDispla;
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
const sessionId = uuid();
let previousMediaProvider;
const fireEvent = (payload, channel) => {
var _api$analytics, _api$analytics$action;
const fireAnalyticsEvent = 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;
if (!fireAnalyticsEvent) {
return;
}
if (payload.attributes && !payload.attributes.sessionId) {
payload.attributes.sessionId = sessionId;
}
fireAnalyticsEvent(payload, channel);
};
const typeAhead = createTypeAheadConfig({
sanitizePrivateContent: options === null || options === void 0 ? void 0 : options.sanitizePrivateContent,
mentionInsertDisplayName: options === null || options === void 0 ? void 0 : options.insertDisplayName,
HighlightComponent: options === null || options === void 0 ? void 0 : options.HighlightComponent,
handleMentionsChanged: options === null || options === void 0 ? void 0 : options.handleMentionsChanged,
enableAgentSectioning: options === null || options === void 0 ? void 0 : options.enableAgentSectioning,
showAgentMentionsLabsLozenge: options === null || options === void 0 ? void 0 : options.showAgentMentionsLabsLozenge,
profilecardProvider: options === null || options === void 0 ? void 0 : options.profilecardProvider,
fireEvent,
api
});
const isRegisteredSlashCommandEnabled = isExperimentEnabled('platform_editor_slash_command');
if (isRegisteredSlashCommandEnabled) {
var _api$uiControlRegistr;
api === null || api === void 0 ? void 0 : (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 ? void 0 : _api$uiControlRegistr.actions.register(getMentionQuickInsertComponents({
api,
typeAhead
}));
}
return {
name: 'mention',
nodes() {
return [{
name: 'mention',
node: mentionNodeSpec()
}];
},
pmPlugins() {
const plugins = [{
name: 'mention',
plugin: pmPluginFactoryParams => createMentionPlugin({
pmPluginFactoryParams,
fireEvent,
options,
api
})
}];
if (editorExperiment('platform_editor_agent_mentions', true) && isExperimentEnabled('platform_editor_agent_mentions_separate_pm_plugin')) {
plugins.push({
name: 'agentMention',
plugin: pmPluginFactoryParams => createAgentMentionPlugin({
pmPluginFactoryParams,
options
})
});
}
return plugins;
},
contentComponent({
editorView,
providerFactory
}) {
if (!editorView) {
return null;
}
return /*#__PURE__*/React.createElement(WithProviders
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
providers: ['mentionProvider'],
providerFactory: providerFactory
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
renderNode: ({
mentionProvider
}) => {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Component, {
mentionProvider: mentionProvider,
api: api
}), fg('inline_invite_from_mentions_kill_switch') && /*#__PURE__*/React.createElement(InlineInvitePopupContainer, {
mentionProvider: mentionProvider,
api: api,
editorView: editorView
}));
}
});
},
secondaryToolbarComponent({
editorView,
disabled
}) {
if (!editorView) {
return null;
}
return /*#__PURE__*/React.createElement(SecondaryToolbarComponent, {
editorView: editorView,
api: api,
disabled: disabled,
typeAhead: typeAhead
});
},
commands: {
insertMention: insertMention({
sanitizePrivateContent: (_options$sanitizePriv = options === null || options === void 0 ? void 0 : options.sanitizePrivateContent) !== null && _options$sanitizePriv !== void 0 ? _options$sanitizePriv : false,
mentionInsertDisplayName: (_options$insertDispla = options === null || options === void 0 ? void 0 : options.insertDisplayName) !== null && _options$insertDispla !== void 0 ? _options$insertDispla : false,
api
})
},
actions: {
openTypeAhead(inputMethod) {
var _api$typeAhead, _api$typeAhead$action;
return Boolean(api === null || api === void 0 ? void 0 : (_api$typeAhead = api.typeAhead) === null || _api$typeAhead === void 0 ? void 0 : (_api$typeAhead$action = _api$typeAhead.actions) === null || _api$typeAhead$action === void 0 ? void 0 : _api$typeAhead$action.open({
triggerHandler: typeAhead,
inputMethod
}));
},
announceMentionsInsertion: mentionChanges => {
if (options !== null && options !== void 0 && options.handleMentionsChanged) {
options.handleMentionsChanged(mentionChanges);
}
},
setAgentMentionRunStates: runStateByLocalId => {
var _api$core$actions$exe;
if (!(editorExperiment('platform_editor_agent_mentions', true) && isExperimentEnabled('platform_editor_agent_mention_state_anim'))) {
return false;
}
return (_api$core$actions$exe = api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(mentionPluginKey, {
action: ACTIONS.SET_AGENT_RUN_STATES,
params: {
runStateByLocalId
}
}))) !== null && _api$core$actions$exe !== void 0 ? _api$core$actions$exe : false;
},
updateSectionTitle: props => {
var _api$typeAhead$action2, _api$typeAhead2, _api$typeAhead2$actio, _api$typeAhead2$actio2;
if (!(options !== null && options !== void 0 && options.enableAgentSectioning)) {
return false;
}
return (_api$typeAhead$action2 = api === null || api === void 0 ? void 0 : (_api$typeAhead2 = api.typeAhead) === null || _api$typeAhead2 === void 0 ? void 0 : (_api$typeAhead2$actio = _api$typeAhead2.actions) === null || _api$typeAhead2$actio === void 0 ? void 0 : (_api$typeAhead2$actio2 = _api$typeAhead2$actio.updateSectionTitle) === null || _api$typeAhead2$actio2 === void 0 ? void 0 : _api$typeAhead2$actio2.call(_api$typeAhead2$actio, props)) !== null && _api$typeAhead$action2 !== void 0 ? _api$typeAhead$action2 : false;
},
setProvider: async providerPromise => {
var _api$core$actions$exe2;
if (!fg('platform_editor_mention_provider_via_plugin_config')) {
return false;
}
const provider = await providerPromise;
// Prevent someone trying to set the exact same provider twice for performance reasons
if (previousMediaProvider === provider) {
return false;
}
previousMediaProvider = provider;
return (_api$core$actions$exe2 = api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(mentionPluginKey, {
action: ACTIONS.SET_PROVIDER,
params: {
provider
}
}))) !== null && _api$core$actions$exe2 !== void 0 ? _api$core$actions$exe2 : false;
}
},
getSharedState(editorState) {
if (!editorState) {
return undefined;
}
const mentionPluginState = mentionPluginKey.getState(editorState);
const agentMentionPluginState = isExperimentEnabled('platform_editor_agent_mentions_separate_pm_plugin') ? agentMentionPluginKey.getState(editorState) : undefined;
// Exclude pendingPastedAgentMention — it is an @internal transient field and
// should not be part of the public shared state API. Exposing it would cause
// unnecessary re-renders in subscribers and leak implementation details.
const {
pendingPastedAgentMention: _excluded,
// Internal decoration-only state, kept out of the public shared state.
runStateDecorations: _excludedRunState,
...publicPluginState
} = mentionPluginState !== null && mentionPluginState !== void 0 ? mentionPluginState : {};
return {
...publicPluginState,
...(agentMentionPluginState ? {
lastAgentMentionInsertionCount: agentMentionPluginState.lastAgentMentionInsertionCount,
lastInsertedAgentMentionContext: agentMentionPluginState.lastInsertedAgentMentionContext,
lastInsertedAgentMentionId: agentMentionPluginState.lastInsertedAgentMentionId,
lastInsertedAgentMentionLocalId: agentMentionPluginState.lastInsertedAgentMentionLocalId,
lastInsertedAgentMentionName: agentMentionPluginState.lastInsertedAgentMentionName,
lastInsertedAgentMentionParentNodeType: agentMentionPluginState.lastInsertedAgentMentionParentNodeType,
lastInsertedAgentMentionPrompt: agentMentionPluginState.lastInsertedAgentMentionPrompt
} : {}),
typeAheadHandler: typeAhead
};
},
pluginsOptions: {
quickInsert: isRegisteredSlashCommandEnabled ? undefined : ({
formatMessage
}) => [{
id: 'mention',
title: formatMessage(messages.mention),
description: formatMessage(messages.mentionDescription),
keywords: ['team', 'user'],
priority: 400,
keyshortcut: '@',
icon: () => /*#__PURE__*/React.createElement(IconMention, null),
action(insert, state) {
var _api$typeAhead3;
const tr = insert(undefined);
const pluginState = mentionPluginKey.getState(state);
if (pluginState && pluginState.canInsertMention === false) {
return false;
}
api === null || api === void 0 ? void 0 : (_api$typeAhead3 = api.typeAhead) === null || _api$typeAhead3 === void 0 ? void 0 : _api$typeAhead3.actions.openAtTransaction({
triggerHandler: typeAhead,
inputMethod: INPUT_METHOD.QUICK_INSERT
})(tr);
return tr;
}
}],
typeAhead
}
};
};
export { mentionsPlugin };