mirror of
https://github.com/discourse/discourse.git
synced 2025-02-23 04:33:43 +08:00
DEV: Convert UppyImageUploader to gjs (#31310)
This commit is contained in:
parent
a585fc5a24
commit
310cd513d8
@ -1,6 +1,8 @@
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.value}}
|
||||
@placeholderUrl={{this.setting.placeholder}}
|
||||
@onUploadDone={{fn (mut this.value)}}
|
||||
@onUploadDeleted={{fn (mut this.value) null}}
|
||||
@additionalParams={{hash for_site_setting=true}}
|
||||
@type="site_setting"
|
||||
@id={{concat "site-setting-image-uploader-" this.setting.setting}}
|
||||
|
@ -0,0 +1,216 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { modifier } from "ember-modifier";
|
||||
import $ from "jquery";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PickFilesButton from "discourse/components/pick-files-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { getURLWithCDN } from "discourse/lib/get-url";
|
||||
import lightbox from "discourse/lib/lightbox";
|
||||
import { authorizesOneOrMoreExtensions } from "discourse/lib/uploads";
|
||||
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
// Args: id, type, imageUrl, placeholderUrl, additionalParams, onUploadDone, onUploadDeleted,
|
||||
export default class UppyImageUploader extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked imageFilesize;
|
||||
@tracked imageFilename;
|
||||
@tracked imageWidth;
|
||||
@tracked imageHeight;
|
||||
|
||||
uppyUpload = new UppyUpload(getOwner(this), {
|
||||
id: this.args.id,
|
||||
type: this.args.type,
|
||||
additionalParams: this.args.additionalParams,
|
||||
validateUploadedFilesOptions: { imagesOnly: true },
|
||||
uploadDropTargetOptions: () => ({
|
||||
target: document.querySelector(
|
||||
`#${this.args.id} .uploaded-image-preview`
|
||||
),
|
||||
}),
|
||||
uploadDone: (upload) => {
|
||||
this.imageFilesize = upload.human_filesize;
|
||||
this.imageFilename = upload.original_filename;
|
||||
this.imageWidth = upload.width;
|
||||
this.imageHeight = upload.height;
|
||||
|
||||
this.args.onUploadDone(upload);
|
||||
},
|
||||
});
|
||||
|
||||
applyLightbox = modifier((element) => lightbox(element, this.siteSettings));
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
$.magnificPopup?.instance.close();
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return (
|
||||
this.notAllowed ||
|
||||
this.uppyUpload?.uploading ||
|
||||
this.uppyUpload?.processing
|
||||
);
|
||||
}
|
||||
|
||||
get computedId() {
|
||||
// without a fallback ID this will not be accessible
|
||||
return this.args.id ? `${this.args.id}__input` : `${guidFor(this)}__input`;
|
||||
}
|
||||
|
||||
get disabledReason() {
|
||||
if (this.disabled && this.notAllowed) {
|
||||
return i18n("post.errors.no_uploads_authorized");
|
||||
}
|
||||
}
|
||||
|
||||
get notAllowed() {
|
||||
return !authorizesOneOrMoreExtensions(
|
||||
this.currentUser?.staff,
|
||||
this.siteSettings
|
||||
);
|
||||
}
|
||||
|
||||
get showingPlaceholder() {
|
||||
return !this.args.imageUrl && this.args.placeholderUrl;
|
||||
}
|
||||
|
||||
get placeholderStyle() {
|
||||
if (isEmpty(this.args.placeholderUrl)) {
|
||||
return htmlSafe("");
|
||||
}
|
||||
return htmlSafe(`background-image: url(${this.args.placeholderUrl})`);
|
||||
}
|
||||
|
||||
get imageCdnUrl() {
|
||||
if (isEmpty(this.args.imageUrl)) {
|
||||
return htmlSafe("");
|
||||
}
|
||||
|
||||
return getURLWithCDN(this.args.imageUrl);
|
||||
}
|
||||
|
||||
get backgroundStyle() {
|
||||
return htmlSafe(`background-image: url(${this.imageCdnUrl})`);
|
||||
}
|
||||
|
||||
get imageBaseName() {
|
||||
if (!isEmpty(this.args.imageUrl)) {
|
||||
return this.args.imageUrl.split("/").slice(-1)[0];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleLightbox() {
|
||||
const lightboxElement = document.querySelector(
|
||||
`#${this.args.id} a.lightbox`
|
||||
);
|
||||
|
||||
if (lightboxElement) {
|
||||
$(lightboxElement).magnificPopup("open");
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeyboardActivation(event) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault(); // avoid space scrolling the page
|
||||
const input = document.getElementById(this.computedId);
|
||||
if (input && !this.disabled) {
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div {{this.applyLightbox}} id={{@id}} class="image-uploader" ...attributes>
|
||||
<div
|
||||
class="uploaded-image-preview input-xxlarge"
|
||||
style={{this.backgroundStyle}}
|
||||
>
|
||||
{{#if this.showingPlaceholder}}
|
||||
<div
|
||||
class="placeholder-overlay"
|
||||
style={{this.placeholderStyle}}
|
||||
></div>
|
||||
{{/if}}
|
||||
<div class="image-upload-controls">
|
||||
<label
|
||||
class="btn btn-default pad-left no-text
|
||||
{{if this.disabled 'disabled'}}"
|
||||
title={{this.disabledReason}}
|
||||
for={{this.computedId}}
|
||||
tabindex="0"
|
||||
{{on "keydown" this.handleKeyboardActivation}}
|
||||
>
|
||||
{{icon "far-image"}}
|
||||
<PickFilesButton
|
||||
@registerFileInput={{this.uppyUpload.setup}}
|
||||
@fileInputDisabled={{this.disabled}}
|
||||
@acceptedFormatsOverride="image/*"
|
||||
@fileInputId={{this.computedId}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{{#if @imageUrl}}
|
||||
<DButton
|
||||
@action={{@onUploadDeleted}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger pad-left no-text"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.toggleLightbox}}
|
||||
@icon="discourse-expand"
|
||||
@title="expand"
|
||||
class="btn-default image-uploader-lightbox-btn no-text"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<span
|
||||
class={{concatClass
|
||||
"btn"
|
||||
(unless this.uppyUpload.uploading "hidden")
|
||||
}}
|
||||
>
|
||||
{{i18n "upload_selector.uploading"}}
|
||||
{{this.uppyUpload.uploadProgress}}%
|
||||
</span>
|
||||
<span
|
||||
class={{concatClass
|
||||
"btn"
|
||||
(unless this.uppyUpload.processing "hidden")
|
||||
}}
|
||||
>{{i18n "upload_selector.processing"}}</span>
|
||||
</div>
|
||||
|
||||
{{#if @imageUrl}}
|
||||
<a
|
||||
href={{this.imageCdnUrl}}
|
||||
title={{this.imageFilename}}
|
||||
rel="nofollow ugc noopener"
|
||||
class="lightbox"
|
||||
>
|
||||
|
||||
<div class="meta">
|
||||
<span class="informations">
|
||||
{{this.imageWidth}}x{{this.imageHeight}}
|
||||
{{this.imageFilesize}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
<div
|
||||
class="uploaded-image-preview input-xxlarge"
|
||||
style={{this.backgroundStyle}}
|
||||
>
|
||||
{{#if this.showingPlaceholder}}
|
||||
<div class="placeholder-overlay" style={{this.placeholderStyle}}></div>
|
||||
{{/if}}
|
||||
<div class="image-upload-controls">
|
||||
<label
|
||||
class="btn btn-default pad-left no-text {{if this.disabled 'disabled'}}"
|
||||
title={{this.disabledReason}}
|
||||
for={{this.computedId}}
|
||||
tabindex="0"
|
||||
{{on "keydown" this.handleKeyboardActivation}}
|
||||
>
|
||||
{{d-icon "far-image"}}
|
||||
<PickFilesButton
|
||||
@registerFileInput={{this.uppyUpload.setup}}
|
||||
@fileInputDisabled={{this.disabled}}
|
||||
@acceptedFormatsOverride="image/*"
|
||||
@fileInputId={{this.computedId}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{{#if this.imageUrl}}
|
||||
<DButton
|
||||
@action={{action "trash"}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger pad-left no-text"
|
||||
/>
|
||||
<DButton
|
||||
@icon="discourse-expand"
|
||||
@title="expand"
|
||||
@disabled={{this.loadingLightbox}}
|
||||
@action={{this.toggleLightbox}}
|
||||
class="btn-default image-uploader-lightbox-btn no-text"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<span class="btn {{unless this.uppyUpload.uploading 'hidden'}}">{{i18n
|
||||
"upload_selector.uploading"
|
||||
}}
|
||||
{{this.uppyUpload.uploadProgress}}%</span>
|
||||
<span class="btn {{unless this.uppyUpload.processing 'hidden'}}">{{i18n
|
||||
"upload_selector.processing"
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
{{#if this.imageUrl}}
|
||||
<a
|
||||
class="lightbox"
|
||||
href={{this.imageCDNURL}}
|
||||
title={{this.imageFilename}}
|
||||
rel="nofollow ugc noopener"
|
||||
>
|
||||
|
||||
<div class="meta">
|
||||
<span class="informations">
|
||||
{{this.imageWidth}}x{{this.imageHeight}}
|
||||
{{this.imageFilesize}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
@ -1,157 +0,0 @@
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { or } from "@ember/object/computed";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { next } from "@ember/runloop";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import { on } from "@ember-decorators/object";
|
||||
import $ from "jquery";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { getURLWithCDN } from "discourse/lib/get-url";
|
||||
import lightbox from "discourse/lib/lightbox";
|
||||
import { authorizesOneOrMoreExtensions } from "discourse/lib/uploads";
|
||||
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@classNames("image-uploader")
|
||||
export default class UppyImageUploader extends Component {
|
||||
@or("notAllowed", "uppyUpload.uploading", "uppyUpload.processing") disabled;
|
||||
|
||||
uppyUpload = null;
|
||||
|
||||
@on("init")
|
||||
setupUppyUpload() {
|
||||
// The uppyUpload configuration depends on arguments. In classic components like
|
||||
// this one, the arguments are not available during field initialization, so we have to
|
||||
// defer until init(). When this component is glimmer-ified in future, this can be turned
|
||||
// into a simple field initializer.
|
||||
this.uppyUpload = new UppyUpload(getOwner(this), {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
additionalParams: this.additionalParams,
|
||||
validateUploadedFilesOptions: { imagesOnly: true },
|
||||
uploadDropTargetOptions: () => ({
|
||||
target: document.querySelector(`#${this.id} .uploaded-image-preview`),
|
||||
}),
|
||||
uploadDone: (upload) => {
|
||||
this.setProperties({
|
||||
imageFilesize: upload.human_filesize,
|
||||
imageFilename: upload.original_filename,
|
||||
imageWidth: upload.width,
|
||||
imageHeight: upload.height,
|
||||
});
|
||||
|
||||
// the value of the property used for imageUrl should be set
|
||||
// in this callback. this should be done in cases where imageUrl
|
||||
// is bound to a computed property of the parent component.
|
||||
if (this.onUploadDone) {
|
||||
this.onUploadDone(upload);
|
||||
} else {
|
||||
this.set("imageUrl", upload.url);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@discourseComputed("id")
|
||||
computedId(id) {
|
||||
// without a fallback ID this will not be accessible
|
||||
return id ? `${id}__input` : `${guidFor(this)}__input`;
|
||||
}
|
||||
|
||||
@discourseComputed("disabled", "notAllowed")
|
||||
disabledReason(disabled, notAllowed) {
|
||||
if (disabled && notAllowed) {
|
||||
return i18n("post.errors.no_uploads_authorized");
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"currentUser.staff",
|
||||
"siteSettings.{authorized_extensions,authorized_extensions_for_staff}"
|
||||
)
|
||||
notAllowed() {
|
||||
return !authorizesOneOrMoreExtensions(
|
||||
this.currentUser?.staff,
|
||||
this.siteSettings
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("imageUrl", "placeholderUrl")
|
||||
showingPlaceholder(imageUrl, placeholderUrl) {
|
||||
return !imageUrl && placeholderUrl;
|
||||
}
|
||||
|
||||
@discourseComputed("placeholderUrl")
|
||||
placeholderStyle(url) {
|
||||
if (isEmpty(url)) {
|
||||
return htmlSafe("");
|
||||
}
|
||||
return htmlSafe(`background-image: url(${url})`);
|
||||
}
|
||||
|
||||
@discourseComputed("imageUrl")
|
||||
imageCDNURL(url) {
|
||||
if (isEmpty(url)) {
|
||||
return htmlSafe("");
|
||||
}
|
||||
|
||||
return getURLWithCDN(url);
|
||||
}
|
||||
|
||||
@discourseComputed("imageCDNURL")
|
||||
backgroundStyle(url) {
|
||||
return htmlSafe(`background-image: url(${url})`);
|
||||
}
|
||||
|
||||
@discourseComputed("imageUrl")
|
||||
imageBaseName(imageUrl) {
|
||||
if (isEmpty(imageUrl)) {
|
||||
return;
|
||||
}
|
||||
return imageUrl.split("/").slice(-1)[0];
|
||||
}
|
||||
|
||||
@on("didRender")
|
||||
_applyLightbox() {
|
||||
next(() => lightbox(this.element, this.siteSettings));
|
||||
}
|
||||
|
||||
@on("willDestroyElement")
|
||||
_closeOnRemoval() {
|
||||
if ($.magnificPopup?.instance) {
|
||||
$.magnificPopup.instance.close();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleLightbox() {
|
||||
$(this.element.querySelector("a.lightbox"))?.magnificPopup("open");
|
||||
}
|
||||
|
||||
@action
|
||||
trash() {
|
||||
// the value of the property used for imageUrl should be cleared
|
||||
// in this callback. this should be done in cases where imageUrl
|
||||
// is bound to a computed property of the parent component.
|
||||
if (this.onUploadDeleted) {
|
||||
this.onUploadDeleted();
|
||||
} else {
|
||||
this.setProperties({ imageUrl: null });
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeyboardActivation(event) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault(); // avoid space scrolling the page
|
||||
const input = document.getElementById(this.computedId);
|
||||
if (input && !this.disabled) {
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { isBlank } from "@ember/utils";
|
||||
import UppyImageUploader from "discourse/components/uppy-image-uploader";
|
||||
@ -23,7 +22,7 @@ export default class FKControlImage extends Component {
|
||||
|
||||
<template>
|
||||
<UppyImageUploader
|
||||
@id={{concat @field.id "-" @field.name}}
|
||||
@id="{{@field.id}}-{{@field.name}}"
|
||||
@imageUrl={{this.imageUrl}}
|
||||
@onUploadDone={{this.setImage}}
|
||||
@onUploadDeleted={{this.removeImage}}
|
||||
|
@ -96,6 +96,11 @@
|
||||
<div class="controls">
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.model.profile_background_upload_url}}
|
||||
@onUploadDone={{fn (mut this.model.profile_background_upload_url)}}
|
||||
@onUploadDeleted={{fn
|
||||
(mut this.model.profile_background_upload_url)
|
||||
null
|
||||
}}
|
||||
@type="profile_background"
|
||||
@id="profile-background-uploader"
|
||||
/>
|
||||
@ -116,6 +121,11 @@
|
||||
<div class="controls">
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.model.card_background_upload_url}}
|
||||
@onUploadDone={{fn (mut this.model.card_background_upload_url)}}
|
||||
@onUploadDeleted={{fn
|
||||
(mut this.model.card_background_upload_url)
|
||||
null
|
||||
}}
|
||||
@type="card_background"
|
||||
@id="profile-card-background-uploader"
|
||||
/>
|
||||
|
@ -43,9 +43,7 @@ class FieldHelper {
|
||||
}
|
||||
|
||||
get value() {
|
||||
this.context
|
||||
.dom(this.element)
|
||||
.exists(`Could not find element (name: ${this.name}).`);
|
||||
this.context.dom(this.element).exists(`field '${this.name}' exists`);
|
||||
|
||||
switch (this.element.dataset.controlType) {
|
||||
case "image": {
|
||||
|
@ -1,67 +1,70 @@
|
||||
import { click, render, triggerEvent } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import UppyImageUploader from "discourse/components/uppy-image-uploader";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | uppy-image-uploader", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("with image", async function (assert) {
|
||||
await render(hbs`
|
||||
<UppyImageUploader @type="avatar" @id="uploader" @imageUrl="/images/avatar.png" @placeholderUrl="/not/used.png" />
|
||||
`);
|
||||
await render(<template>
|
||||
<UppyImageUploader
|
||||
@type="avatar"
|
||||
@id="uploader"
|
||||
@imageUrl="/images/avatar.png"
|
||||
@placeholderUrl="/not/used.png"
|
||||
/>
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-icon-far-image").exists("displays the upload icon");
|
||||
assert.dom(".d-icon-trash-can").exists("displays the trash icon");
|
||||
|
||||
assert
|
||||
.dom(".placeholder-overlay")
|
||||
.doesNotExist("it does not display the placeholder image");
|
||||
.doesNotExist("does not display the placeholder image");
|
||||
|
||||
await click(".image-uploader-lightbox-btn");
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".mfp-container").length,
|
||||
1,
|
||||
"it displays the image lightbox"
|
||||
);
|
||||
assert.dom(".mfp-container").exists("displays the image lightbox");
|
||||
});
|
||||
|
||||
test("without image", async function (assert) {
|
||||
await render(
|
||||
hbs`<UppyImageUploader @type="site_setting" @id="uploader" />`
|
||||
);
|
||||
await render(<template>
|
||||
<UppyImageUploader @type="site_setting" @id="uploader" />
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-icon-far-image").exists("displays the upload icon");
|
||||
assert.dom(".d-icon-trash-can").doesNotExist("does not display trash icon");
|
||||
|
||||
assert
|
||||
.dom(".image-uploader-lightbox-btn")
|
||||
.doesNotExist("it does not display the button to open image lightbox");
|
||||
.doesNotExist("does not display the button to open image lightbox");
|
||||
});
|
||||
|
||||
test("with placeholder", async function (assert) {
|
||||
await render(
|
||||
hbs`<UppyImageUploader @type="composer" @id="uploader" @placeholderUrl="/images/avatar.png" />`
|
||||
);
|
||||
await render(<template>
|
||||
<UppyImageUploader
|
||||
@type="composer"
|
||||
@id="uploader"
|
||||
@placeholderUrl="/images/avatar.png"
|
||||
/>
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-icon-far-image").exists("displays the upload icon");
|
||||
assert.dom(".d-icon-trash-can").doesNotExist("does not display trash icon");
|
||||
|
||||
assert
|
||||
.dom(".image-uploader-lightbox-btn")
|
||||
.doesNotExist("it does not display the button to open image lightbox");
|
||||
.doesNotExist("does not display the button to open image lightbox");
|
||||
|
||||
assert.dom(".placeholder-overlay").exists("displays the placeholder image");
|
||||
});
|
||||
|
||||
test("when dragging image", async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
await render(<template>
|
||||
<UppyImageUploader @type="composer" @id="uploader1" />
|
||||
<UppyImageUploader @type="composer" @id="uploader2" />
|
||||
`
|
||||
);
|
||||
</template>);
|
||||
|
||||
const dropImage = async (target) => {
|
||||
const dataTransfer = new DataTransfer();
|
Loading…
x
Reference in New Issue
Block a user