diff --git a/framework/core/js/src/admin/components/AdminPage.tsx b/framework/core/js/src/admin/components/AdminPage.tsx
index d3b1c9861d..a705c6b047 100644
--- a/framework/core/js/src/admin/components/AdminPage.tsx
+++ b/framework/core/js/src/admin/components/AdminPage.tsx
@@ -12,6 +12,8 @@ import AdminHeader from './AdminHeader';
import generateElementId from '../utils/generateElementId';
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
import ItemList from '../../common/utils/ItemList';
+import type { IUploadImageButtonAttrs } from './UploadImageButton';
+import UploadImageButton from './UploadImageButton';
export interface AdminHeaderOptions {
title: Mithril.Children;
@@ -79,6 +81,7 @@ const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
const TextareaSettingTypes = ['textarea'] as const;
const ColorPreviewSettingType = 'color-preview' as const;
+const ImageUploadSettingType = 'image-upload' as const;
/**
* Valid options for the setting component builder to generate a Switch.
@@ -113,6 +116,10 @@ export interface ColorPreviewSettingComponentOptions extends CommonSettingsItemO
type: typeof ColorPreviewSettingType;
}
+export interface ImageUploadSettingComponentOptions extends CommonSettingsItemOptions, IUploadImageButtonAttrs {
+ type: typeof ImageUploadSettingType;
+}
+
export interface CustomSettingComponentOptions extends CommonSettingsItemOptions {
type: string;
[key: string]: unknown;
@@ -127,6 +134,7 @@ export type SettingsComponentOptions =
| SelectSettingComponentOptions
| TextareaSettingComponentOptions
| ColorPreviewSettingComponentOptions
+ | ImageUploadSettingComponentOptions
| CustomSettingComponentOptions;
/**
@@ -311,6 +319,10 @@ export default abstract class AdminPage
);
+ } else if (type === ImageUploadSettingType) {
+ const { value, ...otherAttrs } = componentAttrs;
+
+ settingElement = ;
} else if (customSettingComponents.has(type)) {
return customSettingComponents.get(type)({ setting, help, label, ...componentAttrs });
} else {
diff --git a/framework/core/js/src/admin/components/AppearancePage.tsx b/framework/core/js/src/admin/components/AppearancePage.tsx
index f5f44cba73..54c2c3a60a 100644
--- a/framework/core/js/src/admin/components/AppearancePage.tsx
+++ b/framework/core/js/src/admin/components/AppearancePage.tsx
@@ -37,13 +37,13 @@ export default class AppearancePage extends AdminPage {
{app.translator.trans('core.admin.appearance.logo_text')}
-
+
{app.translator.trans('core.admin.appearance.favicon_text')}
-
+
diff --git a/framework/core/js/src/admin/components/UploadImageButton.js b/framework/core/js/src/admin/components/UploadImageButton.js
deleted file mode 100644
index 1a7a1a7210..0000000000
--- a/framework/core/js/src/admin/components/UploadImageButton.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import app from '../../admin/app';
-import Button from '../../common/components/Button';
-import classList from '../../common/utils/classList';
-
-export default class UploadImageButton extends Button {
- loading = false;
-
- view(vnode) {
- this.attrs.loading = this.loading;
- this.attrs.className = classList(this.attrs.className, 'Button');
-
- if (app.data.settings[this.attrs.name + '_path']) {
- this.attrs.onclick = this.remove.bind(this);
-
- return (
-
-
-
-
-
{super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.remove_button') })}
-
- );
- } else {
- this.attrs.onclick = this.upload.bind(this);
- }
-
- return super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.upload_button') });
- }
-
- /**
- * Prompt the user to upload an image.
- */
- upload() {
- if (this.loading) return;
-
- const $input = $('
');
-
- $input
- .appendTo('body')
- .hide()
- .trigger('click')
- .on('change', (e) => {
- const body = new FormData();
- body.append(this.attrs.name, $(e.target)[0].files[0]);
-
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'POST',
- url: this.resourceUrl(),
- serialize: (raw) => raw,
- body,
- })
- .then(this.success.bind(this), this.failure.bind(this));
- });
- }
-
- /**
- * Remove the logo.
- */
- remove() {
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'DELETE',
- url: this.resourceUrl(),
- })
- .then(this.success.bind(this), this.failure.bind(this));
- }
-
- resourceUrl() {
- return app.forum.attribute('apiUrl') + '/' + this.attrs.name;
- }
-
- /**
- * After a successful upload/removal, reload the page.
- *
- * @param {object} response
- * @protected
- */
- success(response) {
- window.location.reload();
- }
-
- /**
- * If upload/removal fails, stop loading.
- *
- * @param {object} response
- * @protected
- */
- failure(response) {
- this.loading = false;
- m.redraw();
- }
-}
diff --git a/framework/core/js/src/admin/components/UploadImageButton.tsx b/framework/core/js/src/admin/components/UploadImageButton.tsx
new file mode 100644
index 0000000000..f1e4ab452e
--- /dev/null
+++ b/framework/core/js/src/admin/components/UploadImageButton.tsx
@@ -0,0 +1,110 @@
+import app from '../../admin/app';
+import Button from '../../common/components/Button';
+import type { IButtonAttrs } from '../../common/components/Button';
+import classList from '../../common/utils/classList';
+import type Mithril from 'mithril';
+import Component from '../../common/Component';
+
+export interface IUploadImageButtonAttrs extends IButtonAttrs {
+ name: string;
+ routePath: string;
+ value?: string | null | (() => string | null);
+ url?: string | null | (() => string | null);
+}
+
+export default class UploadImageButton
extends Component {
+ loading = false;
+
+ view(vnode: Mithril.Vnode) {
+ let { name, value, url, ...attrs } = vnode.attrs as IButtonAttrs;
+
+ attrs.loading = this.loading;
+ attrs.className = classList(attrs.className, 'Button');
+
+ if (typeof value === 'function') {
+ value = value();
+ }
+
+ if (typeof url === 'function') {
+ url = url();
+ }
+
+ return (
+
+ {value ? (
+ <>
+
+

+
+
+ >
+ ) : (
+
+ )}
+
+ );
+ }
+
+ upload() {
+ if (this.loading) return;
+
+ const $input = $('');
+
+ $input
+ .appendTo('body')
+ .hide()
+ .trigger('click')
+ .on('change', (e) => {
+ const body = new FormData();
+ // @ts-ignore
+ body.append(this.attrs.name, $(e.target)[0].files[0]);
+
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'POST',
+ url: this.resourceUrl(),
+ serialize: (raw) => raw,
+ body,
+ })
+ .then(this.success.bind(this), this.failure.bind(this));
+ });
+ }
+
+ remove() {
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'DELETE',
+ url: this.resourceUrl(),
+ })
+ .then(this.success.bind(this), this.failure.bind(this));
+ }
+
+ resourceUrl() {
+ return app.forum.attribute('apiUrl') + '/' + this.attrs.routePath;
+ }
+
+ /**
+ * After a successful upload/removal, reload the page.
+ */
+ protected success(response: any) {
+ window.location.reload();
+ }
+
+ /**
+ * If upload/removal fails, stop loading.
+ */
+ protected failure(response: any) {
+ this.loading = false;
+ m.redraw();
+ }
+}