Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(public-upload-api): allow to switch activity to the cloud image editor with predefined file opened #719

Merged
merged 4 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions abstract/ActivityBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -156,6 +168,7 @@ export class ActivityBlock extends Block {
return this.ctxName + this.activityType;
}

/** @type {ActivityParamsMap[keyof ActivityParamsMap]} */
get activityParams() {
return this.$['*currentActivityParams'];
}
Expand Down Expand Up @@ -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 */
2 changes: 2 additions & 0 deletions abstract/Block.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export class Block extends BaseComponent {

/** @protected */
destroyCallback() {
super.destroyCallback();

let blocksRegistry = this.blocksRegistry;
blocksRegistry?.delete(this);

Expand Down
1 change: 0 additions & 1 deletion abstract/CTX.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const uploaderBlockCtx = (fnCtx) => ({
...activityBlockCtx(fnCtx),
'*commonProgress': 0,
'*uploadList': [],
'*focusedEntry': null,
'*uploadQueue': new Queue(1),
/** @type {ReturnType<import('../types').OutputErrorCollection>[]} */
'*collectionErrors': [],
Expand Down
4 changes: 3 additions & 1 deletion abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
19 changes: 15 additions & 4 deletions abstract/UploaderPublicApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,20 +278,31 @@ export class UploaderPublicApi {
};

/**
* @param {import('./ActivityBlock.js').ActivityType} activityType
* @param {import('../blocks/ExternalSource/ExternalSource.js').ActivityParams | {}} [params]
* @type {<T extends import('./ActivityBlock.js').ActivityType>(
* 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;
}
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']) {
Expand Down
1 change: 1 addition & 0 deletions blocks/CloudImageEditor/src/elements/slider/SliderUi.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class SliderUi extends Block {
}

destroyCallback() {
super.destroyCallback();
this._observer?.disconnect();
}
}
Expand Down
59 changes: 34 additions & 25 deletions blocks/CloudImageEditorActivity/CloudImageEditorActivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
Expand All @@ -52,11 +53,11 @@ export class CloudImageEditorActivity extends UploaderBlock {

/** @param {CustomEvent<import('../CloudImageEditor/src/types.js').ApplyResult>} 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,
});
Expand All @@ -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;

Expand Down Expand Up @@ -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 = '';
}
}
9 changes: 9 additions & 0 deletions blocks/ExternalSource/ExternalSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
};
}

/** @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}
Expand Down Expand Up @@ -87,7 +96,7 @@
this.mountIframe();
},
});
this.sub('*currentActivityParams', (val) => {

Check warning on line 99 in blocks/ExternalSource/ExternalSource.js

View workflow job for this annotation

GitHub Actions / build

'val' is defined but never used
if (!this.isActivityActive) {
return;
}
Expand Down
12 changes: 4 additions & 8 deletions blocks/FileItem/FileItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
63 changes: 63 additions & 0 deletions demo/upload-api.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!doctype html>
<base href="../../" />

<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="./solutions/file-uploader/regular/index.css" />
<script
async=""
src="https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=raw,min/dist/es-module-shims.js"
></script>
<script type="importmap">
{
"imports": {
"@symbiotejs/symbiote": "./node_modules/@symbiotejs/symbiote/build/symbiote.js",
"@uploadcare/upload-client": "./node_modules/@uploadcare/upload-client/dist/esm/index.browser.mjs",
"@uploadcare/image-shrink": "./node_modules/@uploadcare/image-shrink/dist/esm/index.browser.mjs",
"keyux": "./node_modules/keyux/index.js"
}
}
</script>
<script src="./index.js" type="module"></script>
<script type="module">
import * as UC from './index.js';

UC.defineComponents(UC);

const config = document.querySelector('uc-');
const ctx = document.querySelector('uc-upload-ctx-provider');
const api = ctx.getAPI();
const radio = document.querySelector('input[name="choice"]');

const handleRadioChange = (choice) => {
switch (choice) {
case 'auto-editor-open':
ctx.addEventListener('file-upload-success', (e) => {
const { internalId } = e.detail;
if (api.getCurrentActivity() !== 'cloud-image-edit') {
api.setCurrentActivity('cloud-image-edit', { internalId });
}
});
break;
default:
break;
}
};

radio.addEventListener('change', (e) => {
handleRadioChange(e.target.value);
});
</script>
</head>

<uc-file-uploader-regular ctx-name="my-uploader" class="uc-light"></uc-file-uploader-regular>
<uc-config ctx-name="my-uploader" pubkey="demopublickey" debug></uc-config>
<uc-upload-ctx-provider ctx-name="my-uploader"></uc-upload-ctx-provider>

<fieldset>
<legend>Please select behaviour:</legend>
<div>
<input type="radio" id="choice1" name="choice" value="auto-editor-open" />
<label for="choice1">Auto editor open</label>
</div>
</fieldset>
34 changes: 34 additions & 0 deletions types/test/public-upload-api.test-d.tsx
Original file line number Diff line number Diff line change
@@ -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' });
9 changes: 4 additions & 5 deletions types/test/uc-upload-ctx-provider.test-d.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading