discourse/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.gjs
Sérgio Saquetim 1654d529ef
DEV: Consolidate reusable components into ui-kit (#38703)
Consolidates reusable UI primitives from `app/components/`,
`app/helpers/`, and `app/modifiers/` into a dedicated `app/ui-kit/`
directory under a unified `d-` prefix naming convention. This gives
Discourse a single, discoverable home for the building-block parts of
the UI: generic primitives (buttons, inputs, modals, selects), layout
pieces (page headers, breadcrumbs, stat tiles), and lightly
Discourse-flavored display widgets (user info, badges, cook-text).

Helpers and modifiers live in `app/ui-kit/helpers/` and
`app/ui-kit/modifiers/` respectively.

### Backward compatibility

No existing imports or template invocations need to change. Old import
paths (e.g. `discourse/components/d-button`, `discourse/helpers/d-icon`)
keep working via runtime AMD `loaderShim` entries in
`app/ui-kit-shims.js`, so plugins and themes need zero changes.

An ESLint rule (shipped via `@discourse/lint-configs` 2.46.0) auto-fixes
imports in the codebase to use the new `discourse/ui-kit/...` paths.
Existing consumer files in `app/`, `admin/`, `plugins/`, and `frontend/`
have been re-imported through that rule in a single sweep, so the
codebase stays consistent.

### Tests

Test module identifiers and file paths now match the ui-kit directory
layout (`tests/integration/ui-kit/...`, `module("Integration | ui-kit |
...")`), keeping the test tree symmetric with `app/ui-kit/`.
2026-05-11 18:07:36 -03:00

178 lines
5.1 KiB
Text
Vendored

/* eslint-disable ember/no-classic-components */
import Component from "@ember/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import { service } from "@ember/service";
import { tagName } from "@ember-decorators/component";
import { removeValueFromArray } from "discourse/lib/array-tools";
import { bind } from "discourse/lib/decorators";
import { cloneJSON } from "discourse/lib/object";
import { autoTrackedArray } from "discourse/lib/tracked-tools";
import UppyUpload from "discourse/lib/uppy/uppy-upload";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
import { clipboardHelpers } from "discourse/lib/utilities";
import DPickFilesButton from "discourse/ui-kit/d-pick-files-button";
import ChatComposerUpload from "discourse/plugins/chat/discourse/components/chat-composer-upload";
@tagName("")
export default class ChatComposerUploads extends Component {
@service capabilities;
@service mediaOptimizationWorker;
@autoTrackedArray uploads = null;
uppyUpload = new UppyUpload(getOwner(this), {
id: "chat-composer-uploader",
type: "chat-composer",
useMultipartUploadsIfAvailable: true,
uppyReady: () => {
if (this.siteSettings.composer_media_optimization_image_enabled) {
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
optimizeFn: (data, opts) =>
this.mediaOptimizationWorker.optimizeImage(data, opts),
runParallel: !this.capabilities.isMobileDevice,
});
}
this.uppyUpload.uppyWrapper.onPreProcessProgress((file) => {
const inProgressUpload = this.inProgressUploads.find(
(item) => item.id === file.id
);
if (!inProgressUpload?.processing) {
inProgressUpload?.set("processing", true);
}
});
this.uppyUpload.uppyWrapper.onPreProcessComplete((file) => {
const inProgressUpload = this.inProgressUploads.find(
(item) => item.id === file.id
);
inProgressUpload?.set("processing", false);
});
},
uploadDone: (upload) => {
this.uploads.push(upload);
this._triggerUploadsChanged();
},
uploadDropTargetOptions: () => ({
target: this.uploadDropZone || document.body,
}),
onProgressUploadsChanged: () => {
this._triggerUploadsChanged(this.uploads, {
inProgressUploadsCount: this.inProgressUploads?.length,
});
},
});
existingUploads = null;
uploadDropZone = null;
get inProgressUploads() {
return this.uppyUpload.inProgressUploads;
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
if (this.inProgressUploads?.length > 0) {
this.uppyUpload.uppyWrapper.uppyInstance?.cancelAll();
}
this.uploads = this.existingUploads ? cloneJSON(this.existingUploads) : [];
}
didInsertElement() {
super.didInsertElement(...arguments);
this.composerInputEl?.addEventListener("paste", this._pasteEventListener);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
this.composerInputEl?.removeEventListener(
"paste",
this._pasteEventListener
);
}
get showUploadsContainer() {
return this.uploads?.length > 0 || this.inProgressUploads.length > 0;
}
@action
cancelUploading(upload) {
this.uppyUpload.cancelSingleUpload({
fileId: upload.id,
});
this.removeUpload(upload);
}
@action
removeUpload(upload) {
removeValueFromArray(this.uploads, upload);
this._triggerUploadsChanged();
}
@bind
_pasteEventListener(event) {
if (document.activeElement !== this.composerInputEl) {
return;
}
const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
siteSettings: this.siteSettings,
canUpload: true,
});
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
return;
}
if (event && event.clipboardData && event.clipboardData.files) {
this.uppyUpload.addFiles([...event.clipboardData.files], {
pasted: true,
});
}
}
_triggerUploadsChanged() {
this.onUploadChanged?.(this.uploads, {
inProgressUploadsCount: this.inProgressUploads?.length,
});
}
<template>
<div class="chat-composer-uploads" ...attributes>
{{#if this.showUploadsContainer}}
<div class="chat-composer-uploads-container">
{{#each this.uploads as |upload|}}
<ChatComposerUpload
@upload={{upload}}
@isDone={{true}}
@onCancel={{fn this.removeUpload upload}}
/>
{{/each}}
{{#each this.inProgressUploads as |upload|}}
<ChatComposerUpload
@upload={{upload}}
@onCancel={{fn this.cancelUploading upload}}
/>
{{/each}}
</div>
{{/if}}
<DPickFilesButton
@allowMultiple={{true}}
@fileInputId={{this.fileUploadElementId}}
@fileInputClass="hidden-upload-field"
@registerFileInput={{this.uppyUpload.setup}}
/>
</div>
</template>
}