@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
481 lines (480 loc) • 17.3 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuidV4 from 'uuid/v4';
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-single';
import { getAttrsFromUrl, isImageRepresentationReady, isMediaBlobUrl } from '@atlaskit/media-client';
import { getMediaClient } from '@atlaskit/media-client-react';
import { getClientIdForFile } from '@atlaskit/media-common';
import { fg } from '@atlaskit/platform-feature-flags';
import { replaceExternalMedia, updateCurrentMediaNodeAttrs, updateMediaNodeAttrs } from '../pm-plugins/commands/helpers';
import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key';
import { batchMediaNodeAttrsUpdate } from '../pm-plugins/utils/batchMediaNodeAttrs';
import { getIdentifier } from '../pm-plugins/utils/media-common';
const isMediaTypeSupported = type => {
if (type) {
return ['image', 'file'].includes(type);
}
return false;
};
export class MediaNodeUpdater {
constructor(props) {
// Updates the node with contextId if it doesn't have one already
_defineProperty(this, "updateContextId", async () => {
const attrs = this.getAttrs();
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return;
}
const {
id
} = attrs;
const objectId = await this.getObjectId();
batchMediaNodeAttrsUpdate(this.props.view, {
id: id,
nextAttributes: {
__contextId: objectId
}
});
});
_defineProperty(this, "updateNodeContextId", async getPos => {
const attrs = this.getAttrs();
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return;
}
const objectId = await this.getObjectId();
updateCurrentMediaNodeAttrs({
__contextId: objectId
}, {
node: this.props.node,
getPos
})(this.props.view.state, this.props.view.dispatch);
});
_defineProperty(this, "hasFileAttributesDefined", attrs => {
return attrs && attrs.type === 'file' && attrs.__fileName && attrs.__fileMimeType && attrs.__fileSize && attrs.__contextId;
});
_defineProperty(this, "getNewFileAttrsForNode", async () => {
const attrs = this.getAttrs();
const mediaProvider = await this.props.mediaProvider;
if (!mediaProvider || !mediaProvider.uploadParams || !attrs || !isMediaTypeSupported(attrs.type) || this.hasFileAttributesDefined(attrs)) {
return;
}
const mediaClientConfig = mediaProvider.viewMediaClientConfig;
const mediaClient = getMediaClient(mediaClientConfig);
let fileState;
const {
id,
collection: collectionName
} = attrs;
try {
fileState = await mediaClient.file.getCurrentState(id, {
collectionName
});
if (fileState.status === 'error') {
return;
}
} catch {
return;
}
const contextId = this.getNodeContextId() || (await this.getObjectId());
const {
name,
mimeType,
size
} = fileState;
const newAttrs = {
__fileName: name,
__fileMimeType: mimeType,
__fileSize: size,
__contextId: contextId
};
if (!hasPrivateAttrsChanged(attrs, newAttrs)) {
return;
}
return newAttrs;
});
_defineProperty(this, "updateMediaSingleFileAttrs", async () => {
const newAttrs = await this.getNewFileAttrsForNode();
const {
id
} = this.getAttrs();
if (id && newAttrs) {
batchMediaNodeAttrsUpdate(this.props.view, {
id: id,
nextAttributes: newAttrs
});
}
});
_defineProperty(this, "updateNodeAttrs", async getPos => {
const newAttrs = await this.getNewFileAttrsForNode();
if (newAttrs) {
updateCurrentMediaNodeAttrs(newAttrs, {
node: this.props.node,
getPos
})(this.props.view.state, this.props.view.dispatch);
}
});
_defineProperty(this, "getAttrs", () => {
const {
attrs
} = this.props.node;
if (attrs) {
return attrs;
}
return undefined;
});
_defineProperty(this, "getObjectId", async () => {
const contextIdentifierProvider = await this.props.contextIdentifierProvider;
return (contextIdentifierProvider === null || contextIdentifierProvider === void 0 ? void 0 : contextIdentifierProvider.objectId) || null;
});
_defineProperty(this, "uploadExternalMedia", async getPos => {
const {
node,
mediaOptions
} = this.props;
if (mediaOptions !== null && mediaOptions !== void 0 && mediaOptions.isExternalMediaUploadDisabled) {
return;
}
const mediaProvider = await this.props.mediaProvider;
if (node && mediaProvider) {
const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig;
if (!uploadMediaClientConfig || !node.attrs.url) {
return;
}
const mediaClient = getMediaClient(uploadMediaClientConfig);
const collection = mediaProvider.uploadParams && mediaProvider.uploadParams.collection;
try {
const uploader = await mediaClient.file.uploadExternal(node.attrs.url, collection);
const {
uploadableFileUpfrontIds,
dimensions
} = uploader;
const pos = getPos();
if (typeof pos !== 'number') {
return;
}
replaceExternalMedia(pos + 1, {
id: uploadableFileUpfrontIds.id,
collection,
height: dimensions.height,
width: dimensions.width,
occurrenceKey: uploadableFileUpfrontIds.occurrenceKey
})(this.props.view.state, this.props.view.dispatch);
} catch {
//keep it as external media
if (this.props.dispatchAnalyticsEvent) {
this.props.dispatchAnalyticsEvent({
action: ACTION.UPLOAD_EXTERNAL_FAIL,
actionSubject: ACTION_SUBJECT.EDITOR,
eventType: EVENT_TYPE.OPERATIONAL
});
}
}
}
});
_defineProperty(this, "getNodeContextId", () => {
const attrs = this.getAttrs();
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return null;
}
return attrs.__contextId || null;
});
_defineProperty(this, "updateDimensions", dimensions => {
batchMediaNodeAttrsUpdate(this.props.view, {
id: dimensions.id,
nextAttributes: {
height: dimensions.height,
width: dimensions.width
}
});
});
_defineProperty(this, "shouldNodeBeDeepCopied", async () => {
var _ref, _this$props$mediaOpti, _this$props$mediaOpti2, _this$mediaPluginStat, _this$mediaPluginStat2;
const scope = (_ref = (_this$props$mediaOpti = (_this$props$mediaOpti2 = this.props.mediaOptions) === null || _this$props$mediaOpti2 === void 0 ? void 0 : _this$props$mediaOpti2.mediaShallowCopyScope) !== null && _this$props$mediaOpti !== void 0 ? _this$props$mediaOpti : (_this$mediaPluginStat = this.mediaPluginState) === null || _this$mediaPluginStat === void 0 ? void 0 : (_this$mediaPluginStat2 = _this$mediaPluginStat.mediaOptions) === null || _this$mediaPluginStat2 === void 0 ? void 0 : _this$mediaPluginStat2.mediaShallowCopyScope) !== null && _ref !== void 0 ? _ref : 'context';
if (scope === 'context') {
return await this.hasDifferentContextId();
} else {
const attrs = this.getAttrs();
if (!attrs || !this.mediaPluginState) {
return false;
}
const id = getIdentifier(attrs);
const isIdentifierOutsideEditorScope = !this.mediaPluginState.isIdentifierInEditorScope(id);
return isIdentifierOutsideEditorScope;
}
});
_defineProperty(this, "hasDifferentContextId", async () => {
const nodeContextId = this.getNodeContextId();
const currentContextId = await this.getObjectId();
if (nodeContextId && currentContextId && nodeContextId !== currentContextId) {
return true;
}
return false;
});
_defineProperty(this, "isNodeFromDifferentCollection", async () => {
const mediaProvider = await this.props.mediaProvider;
if (!mediaProvider || !mediaProvider.uploadParams) {
return false;
}
const currentCollectionName = mediaProvider.uploadParams.collection;
const attrs = this.getAttrs();
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return false;
}
const {
collection: nodeCollection,
__contextId
} = attrs;
const contextId = __contextId || (await this.getObjectId());
if (contextId && currentCollectionName !== nodeCollection) {
return true;
}
return false;
});
_defineProperty(this, "handleCopyFileSwitcher", async attrs => {
const {
mediaClient,
source,
destination,
traceContext
} = attrs;
try {
// calling copyWithToken by passing the auth providers
const {
id
} = await mediaClient.file.copyFile(source, destination, undefined, traceContext);
return id;
} catch (err) {
if (fg('platform_media_cross_client_copy')) {
// calling /v2/file/copy by removing the auth tokens to make cross product copy and pastes
const {
authProvider: _sourceAP,
...copyV2Source
} = source;
const {
authProvider: _destAP,
...copyV2Destination
} = destination;
const {
id
} = await mediaClient.file.copyFile(copyV2Source, copyV2Destination, undefined, traceContext);
return id;
} else {
throw err;
}
}
});
_defineProperty(this, "copyNodeFromBlobUrl", async getPos => {
const attrs = this.getAttrs();
if (!attrs || attrs.type !== 'external') {
return;
}
const {
url
} = attrs;
const mediaAttrs = getAttrsFromUrl(url);
if (!mediaAttrs) {
return;
}
const mediaProvider = await this.props.mediaProvider;
if (!mediaProvider || !mediaProvider.uploadParams) {
return;
}
const currentCollectionName = mediaProvider.uploadParams.collection;
const {
contextId,
clientId,
id,
collection,
height,
width,
mimeType,
name,
size
} = mediaAttrs;
const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig;
if (!uploadMediaClientConfig || !uploadMediaClientConfig.getAuthFromContext) {
return;
}
const mediaClient = getMediaClient(uploadMediaClientConfig);
const getAuthFromContext = uploadMediaClientConfig.getAuthFromContext;
const mediaFileId = await this.handleCopyFileSwitcher({
mediaClient,
source: {
id,
collection,
authProvider: () => getAuthFromContext(contextId),
clientId: fg('platform_media_cross_client_copy_with_auth') ? clientId : undefined
},
destination: {
collection: currentCollectionName,
authProvider: uploadMediaClientConfig.authProvider,
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
occurrenceKey: uuidV4()
}
});
const pos = getPos();
if (typeof pos !== 'number') {
return;
}
replaceExternalMedia(pos + 1, {
id: mediaFileId,
collection: currentCollectionName,
height,
width,
__fileName: name,
__fileMimeType: mimeType,
__fileSize: size
})(this.props.view.state, this.props.view.dispatch);
});
// Copies the pasted node into the current collection using a getPos handler
_defineProperty(this, "copyNodeFromPos", async (getPos, traceContext) => {
const attrs = this.getAttrs();
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return;
}
const copiedAttrs = await this.copyFile(attrs.id, attrs.collection, traceContext);
if (!copiedAttrs) {
return;
}
updateCurrentMediaNodeAttrs(copiedAttrs, {
node: this.props.node,
getPos
})(this.props.view.state, this.props.view.dispatch);
});
// Copies the pasted node into the current collection
_defineProperty(this, "copyNode", async traceContext => {
const attrs = this.getAttrs();
const {
view
} = this.props;
if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) {
return;
}
const copiedAttrs = await this.copyFile(attrs.id, attrs.collection, traceContext);
if (!copiedAttrs) {
return;
}
updateMediaNodeAttrs(attrs.id, copiedAttrs)(view.state, view.dispatch);
});
_defineProperty(this, "copyFile", async (id, collection, traceContext) => {
const mediaProvider = await this.props.mediaProvider;
if (!(mediaProvider !== null && mediaProvider !== void 0 && mediaProvider.uploadParams)) {
return;
}
const nodeContextId = this.getNodeContextId();
const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig;
if (!(uploadMediaClientConfig !== null && uploadMediaClientConfig !== void 0 && uploadMediaClientConfig.getAuthFromContext) || !nodeContextId) {
return;
}
const mediaClient = getMediaClient(uploadMediaClientConfig);
const clientId = fg('platform_media_cross_client_copy_with_auth') ? getClientIdForFile(id) : undefined;
const currentCollectionName = mediaProvider.uploadParams.collection;
const objectId = await this.getObjectId();
const getAuthFromContext = uploadMediaClientConfig.getAuthFromContext;
const mediaFileId = await this.handleCopyFileSwitcher({
mediaClient,
source: {
id,
collection,
authProvider: () => getAuthFromContext(nodeContextId),
clientId
},
destination: {
collection: currentCollectionName,
authProvider: uploadMediaClientConfig.authProvider,
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
occurrenceKey: uuidV4()
},
traceContext
});
return {
id: mediaFileId,
collection: currentCollectionName,
__contextId: objectId
};
});
this.props = props;
this.mediaPluginState = mediaStateKey.getState(props.view.state);
}
setProps(newComponentProps) {
this.props = {
...this.props,
...newComponentProps
};
}
isMediaBlobUrl() {
const attrs = this.getAttrs();
return !!(attrs && attrs.type === 'external' && isMediaBlobUrl(attrs.url));
}
async getRemoteDimensions() {
const mediaProvider = await this.props.mediaProvider;
const {
mediaOptions
} = this.props;
const attrs = this.getAttrs();
if (!mediaProvider || !attrs) {
return false;
}
const {
height,
width
} = attrs;
if (attrs.type === 'external' || !attrs.id) {
return false;
}
const {
id,
collection
} = attrs;
if (height && width) {
return false;
}
// can't fetch remote dimensions on mobile, so we'll default them
if (mediaOptions && !mediaOptions.allowRemoteDimensionsFetch) {
return {
id,
height: DEFAULT_IMAGE_HEIGHT,
width: DEFAULT_IMAGE_WIDTH
};
}
const viewMediaClientConfig = mediaProvider.viewMediaClientConfig;
const mediaClient = getMediaClient(viewMediaClientConfig);
const currentState = await mediaClient.file.getCurrentState(id, {
collectionName: collection
});
if (!isImageRepresentationReady(currentState)) {
return false;
}
const imageMetadata = await mediaClient.getImageMetadata(id, {
collection
});
if (!imageMetadata || !imageMetadata.original) {
return false;
}
return {
id,
height: imageMetadata.original.height || DEFAULT_IMAGE_HEIGHT,
width: imageMetadata.original.width || DEFAULT_IMAGE_WIDTH
};
}
async handleExternalMedia(getPos) {
if (this.isMediaBlobUrl()) {
try {
await this.copyNodeFromBlobUrl(getPos);
} catch {
await this.uploadExternalMedia(getPos);
}
} else {
await this.uploadExternalMedia(getPos);
}
}
}
const hasPrivateAttrsChanged = (currentAttrs, newAttrs) => {
return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId;
};
export const createMediaNodeUpdater = props => {
const updaterProps = {
...props
};
return new MediaNodeUpdater(updaterProps);
};