diff --git a/abstract/ActivityBlock.js b/abstract/ActivityBlock.js index 2dbbbc1b..517142fb 100644 --- a/abstract/ActivityBlock.js +++ b/abstract/ActivityBlock.js @@ -7,6 +7,13 @@ import { activityBlockCtx } from './CTX.js'; const ACTIVE_ATTR = 'active'; const ACTIVE_PROP = '___ACTIVITY_IS_ACTIVE___'; +/** + * @typedef {{ + * 'cloud-image-edit': import('../blocks/CloudImageEditorActivity/CloudImageEditorActivity.js').ActivityParams; + * external: import('../blocks/ExternalSource/ExternalSource.js').ActivityParams; + * }} ActivityParamsMap + */ + export class ActivityBlock extends Block { /** @protected */ historyTracked = false; @@ -54,10 +61,15 @@ export class ActivityBlock extends Block { this.setAttribute('activity', this.activityType); } this.sub('*currentActivity', (/** @type {String} */ val) => { - if (this.activityType !== val && this[ACTIVE_PROP]) { - this._deactivate(); - } else if (this.activityType === val && !this[ACTIVE_PROP]) { - this._activate(); + try { + if (this.activityType !== val && this[ACTIVE_PROP]) { + this._deactivate(); + } else if (this.activityType === val && !this[ACTIVE_PROP]) { + this._activate(); + } + } catch (err) { + console.error(`Error in activity "${this.activityType}". `, err); + this.$['*currentActivity'] = this.$['*history'][this.$['*history'].length - 1] ?? null; } if (!val) { @@ -156,6 +168,7 @@ export class ActivityBlock extends Block { return this.ctxName + this.activityType; } + /** @type {ActivityParamsMap[keyof ActivityParamsMap]} */ get activityParams() { return this.$['*currentActivityParams']; } @@ -201,7 +214,7 @@ ActivityBlock.activities = Object.freeze({ URL: 'url', CLOUD_IMG_EDIT: 'cloud-image-edit', EXTERNAL: 'external', - DETAILS: 'details', }); -/** @typedef {(typeof ActivityBlock)['activities'][keyof (typeof ActivityBlock)['activities']] | (string & {}) | null} ActivityType */ +/** @typedef {(typeof ActivityBlock)['activities'][keyof (typeof ActivityBlock)['activities']]} RegisteredActivityType */ +/** @typedef {RegisteredActivityType | (string & {}) | null} ActivityType */ diff --git a/abstract/Block.js b/abstract/Block.js index a69fa1df..61918f5c 100644 --- a/abstract/Block.js +++ b/abstract/Block.js @@ -209,6 +209,8 @@ export class Block extends BaseComponent { /** @protected */ destroyCallback() { + super.destroyCallback(); + let blocksRegistry = this.blocksRegistry; blocksRegistry?.delete(this); diff --git a/abstract/CTX.js b/abstract/CTX.js index 4462de40..840dbed3 100644 --- a/abstract/CTX.js +++ b/abstract/CTX.js @@ -23,7 +23,6 @@ export const uploaderBlockCtx = (fnCtx) => ({ ...activityBlockCtx(fnCtx), '*commonProgress': 0, '*uploadList': [], - '*focusedEntry': null, '*uploadQueue': new Queue(1), /** @type {ReturnType[]} */ '*collectionErrors': [], diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index efc04d29..93d4d40f 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -360,7 +360,9 @@ export class UploaderBlock extends ActivityBlock { this.cfg.useCloudImageEditor && this.hasBlockInCtx((block) => block.activityType === ActivityBlock.activities.CLOUD_IMG_EDIT) ) { - this.$['*focusedEntry'] = entry; + this.$['*currentActivityParams'] = { + internalId: entry.uid, + }; this.$['*currentActivity'] = ActivityBlock.activities.CLOUD_IMG_EDIT; } } diff --git a/abstract/UploaderPublicApi.js b/abstract/UploaderPublicApi.js index daef7e8a..a4785f52 100644 --- a/abstract/UploaderPublicApi.js +++ b/abstract/UploaderPublicApi.js @@ -278,13 +278,19 @@ export class UploaderPublicApi { }; /** - * @param {import('./ActivityBlock.js').ActivityType} activityType - * @param {import('../blocks/ExternalSource/ExternalSource.js').ActivityParams | {}} [params] + * @type {( + * activityType: T, + * ...params: T extends keyof import('./ActivityBlock.js').ActivityParamsMap + * ? [import('./ActivityBlock.js').ActivityParamsMap[T]] + * : T extends import('./ActivityBlock.js').RegisteredActivityType + * ? [undefined?] + * : [any?] + * ) => void} */ - setCurrentActivity = (activityType, params = {}) => { + setCurrentActivity = (activityType, params = undefined) => { if (this._ctx.hasBlockInCtx((b) => b.activityType === activityType)) { this._ctx.set$({ - '*currentActivityParams': params, + '*currentActivityParams': params ?? {}, '*currentActivity': activityType, }); return; @@ -292,6 +298,11 @@ export class UploaderPublicApi { console.warn(`Activity type "${activityType}" not found in the context`); }; + /** @returns {import('./ActivityBlock.js').ActivityType} */ + getCurrentActivity = () => { + return this._ctx.$['*currentActivity']; + }; + /** @param {boolean} opened */ setModalState = (opened) => { if (opened && !this._ctx.$['*currentActivity']) { diff --git a/blocks/CloudImageEditor/src/elements/slider/SliderUi.js b/blocks/CloudImageEditor/src/elements/slider/SliderUi.js index 88feacd6..811572d5 100644 --- a/blocks/CloudImageEditor/src/elements/slider/SliderUi.js +++ b/blocks/CloudImageEditor/src/elements/slider/SliderUi.js @@ -163,6 +163,7 @@ export class SliderUi extends Block { } destroyCallback() { + super.destroyCallback(); this._observer?.disconnect(); } } diff --git a/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js b/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js index 000e1755..ada20510 100644 --- a/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js +++ b/blocks/CloudImageEditorActivity/CloudImageEditorActivity.js @@ -3,17 +3,31 @@ import { ActivityBlock } from '../../abstract/ActivityBlock.js'; import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { CloudImageEditorBlock } from '../CloudImageEditor/index.js'; +/** @typedef {{ internalId: string }} ActivityParams */ + export class CloudImageEditorActivity extends UploaderBlock { couldBeCtxOwner = true; activityType = ActivityBlock.activities.CLOUD_IMG_EDIT; - constructor() { - super(); - - this.init$ = { - ...this.init$, - cdnUrl: null, - }; + /** + * @private + * @type {import('../../abstract/TypedData.js').TypedData | undefined} + */ + _entry; + + /** + * @private + * @type {CloudImageEditorBlock | undefined} + */ + _instance; + + /** @type {ActivityParams} */ + get activityParams() { + const params = super.activityParams; + if ('internalId' in params) { + return params; + } + throw new Error(`Cloud Image Editor activity params not found`); } initCallback() { @@ -24,19 +38,6 @@ export class CloudImageEditorActivity extends UploaderBlock { onDeactivate: () => this.unmountEditor(), }); - this.sub('*focusedEntry', (/** @type {import('../../abstract/TypedData.js').TypedData} */ entry) => { - if (!entry) { - return; - } - this.entry = entry; - - this.entry.subscribe('cdnUrl', (cdnUrl) => { - if (cdnUrl) { - this.$.cdnUrl = cdnUrl; - } - }); - }); - this.subConfigValue('cropPreset', (cropPreset) => { if (this._instance && this._instance.getAttribute('crop-preset') !== cropPreset) { this._instance.setAttribute('crop-preset', cropPreset); @@ -52,11 +53,11 @@ export class CloudImageEditorActivity extends UploaderBlock { /** @param {CustomEvent} e */ handleApply(e) { - if (!this.entry) { + if (!this._entry) { return; } let result = e.detail; - this.entry.setMultipleValues({ + this._entry.setMultipleValues({ cdnUrl: result.cdnUrl, cdnUrlModifiers: result.cdnUrlModifiers, }); @@ -68,8 +69,17 @@ export class CloudImageEditorActivity extends UploaderBlock { } mountEditor() { + const { internalId } = this.activityParams; + this._entry = this.uploadCollection.read(internalId); + if (!this._entry) { + throw new Error(`Entry with internalId "${internalId}" not found`); + } + const cdnUrl = this._entry.getValue('cdnUrl'); + if (!cdnUrl) { + throw new Error(`Entry with internalId "${internalId}" hasn't uploaded yet`); + } + const instance = new CloudImageEditorBlock(); - const cdnUrl = this.$.cdnUrl; const cropPreset = this.cfg.cropPreset; const tabs = this.cfg.cloudImageEditorTabs; @@ -100,14 +110,13 @@ export class CloudImageEditorActivity extends UploaderBlock { this.innerHTML = ''; this.appendChild(instance); - this._mounted = true; - /** @private */ this._instance = instance; } unmountEditor() { this._instance = undefined; + this._entry = undefined; this.innerHTML = ''; } } diff --git a/blocks/ExternalSource/ExternalSource.js b/blocks/ExternalSource/ExternalSource.js index 42903641..2556f542 100644 --- a/blocks/ExternalSource/ExternalSource.js +++ b/blocks/ExternalSource/ExternalSource.js @@ -60,6 +60,15 @@ export class ExternalSource extends UploaderBlock { }; } + /** @type {ActivityParams} */ + get activityParams() { + const params = super.activityParams; + if ('externalSourceType' in params) { + return params; + } + throw new Error(`External Source activity params not found`); + } + /** * @private * @type {HTMLIFrameElement | null} diff --git a/blocks/FileItem/FileItem.js b/blocks/FileItem/FileItem.js index 2c24568c..a293ee2b 100644 --- a/blocks/FileItem/FileItem.js +++ b/blocks/FileItem/FileItem.js @@ -56,14 +56,10 @@ export class FileItem extends UploaderBlock { isEditable: false, state: FileItemState.IDLE, onEdit: () => { - this.set$({ - '*focusedEntry': this._entry, - }); - if (this.hasBlockInCtx((b) => b.activityType === ActivityBlock.activities.DETAILS)) { - this.$['*currentActivity'] = ActivityBlock.activities.DETAILS; - } else { - this.$['*currentActivity'] = ActivityBlock.activities.CLOUD_IMG_EDIT; - } + this.$['*currentActivityParams'] = { + internalId: this._entry.uid, + }; + this.$['*currentActivity'] = ActivityBlock.activities.CLOUD_IMG_EDIT; }, onRemove: () => { this.uploadCollection.remove(this.$.uid); diff --git a/demo/upload-api.html b/demo/upload-api.html new file mode 100644 index 00000000..429383f8 --- /dev/null +++ b/demo/upload-api.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + +
+ Please select behaviour: +
+ + +
+
diff --git a/types/test/public-upload-api.test-d.tsx b/types/test/public-upload-api.test-d.tsx new file mode 100644 index 00000000..03507965 --- /dev/null +++ b/types/test/public-upload-api.test-d.tsx @@ -0,0 +1,34 @@ +import { UploadCtxProvider } from '../../index.js'; + +const instance = new UploadCtxProvider(); +const api = instance.getAPI(); + +api.addFileFromUrl('https://example.com/image.png'); + +api.setCurrentActivity('camera'); +api.setCurrentActivity('cloud-image-edit', { internalId: 'id' }); +api.setCurrentActivity('external', { + externalSourceType: 'type', +}); + +// @ts-expect-error - should not allow to set activity without params +api.setCurrentActivity('cloud-image-edit'); +// @ts-expect-error - should not allow to set activity without params +api.setCurrentActivity('external'); + +// @ts-expect-error - should not allow to set activity with invalid params +api.setCurrentActivity('camera', { + invalidParam: 'value', +}); +api.setCurrentActivity('cloud-image-edit', { + // @ts-expect-error - should not allow to set activity with invalid params + invalidParam: 'value', +}); +api.setCurrentActivity('external', { + // @ts-expect-error - should not allow to set activity with invalid params + invalidParam: 'value', +}); + +// should allow to set some custom activity +api.setCurrentActivity('my-custom-activity'); +api.setCurrentActivity('my-custom-activity', { myCustomParam: 'value' }); diff --git a/types/test/uc-upload-ctx-provider.test-d.tsx b/types/test/uc-upload-ctx-provider.test-d.tsx index 9b9903bb..53a74112 100644 --- a/types/test/uc-upload-ctx-provider.test-d.tsx +++ b/types/test/uc-upload-ctx-provider.test-d.tsx @@ -1,17 +1,16 @@ -import { expectNotType, expectType } from 'tsd'; +import { UploadcareFile, UploadcareGroup } from '@uploadcare/upload-client'; +import { useRef } from 'react'; +import { expectType } from 'tsd'; import { ActivityBlock, EventMap, OutputCollectionErrorType, - OutputCollectionState, OutputCollectionStatus, OutputError, OutputFileEntry, OutputFileErrorType, - UploadCtxProvider, + UploadCtxProvider } from '../../index.js'; -import { useRef } from 'react'; -import { UploadcareFile, UploadcareGroup } from '@uploadcare/upload-client'; const instance = new UploadCtxProvider(); instance.uploadCollection.size;