discourse/app/assets/javascripts/admin/addon/models/theme.js
Penar Musaraj a86112fc25
FEATURE: Allow embedded view to include a header (#20150)
This commits adds the ability to add a header to the embedded comments
view. One use case for this is to allow `postMessage` communication
between the comments iframe and the parent frame, for example, when
toggling the theme of the parent webpage.
2023-02-06 11:10:50 -05:00

325 lines
8.0 KiB
JavaScript

import { gt, or } from "@ember/object/computed";
import { isBlank, isEmpty } from "@ember/utils";
import I18n from "I18n";
import RestModel from "discourse/models/rest";
import discourseComputed from "discourse-common/utils/decorators";
import { get } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
const THEME_UPLOAD_VAR = 2;
const FIELDS_IDS = [0, 1, 5];
export const THEMES = "themes";
export const COMPONENTS = "components";
const SETTINGS_TYPE_ID = 5;
const Theme = RestModel.extend({
isActive: or("default", "user_selectable"),
isPendingUpdates: gt("remote_theme.commits_behind", 0),
hasEditedFields: gt("editedFields.length", 0),
hasParents: gt("parent_themes.length", 0),
@discourseComputed("theme_fields.[]")
targets() {
return [
{ id: 0, name: "common" },
{ id: 1, name: "desktop", icon: "desktop" },
{ id: 2, name: "mobile", icon: "mobile-alt" },
{ id: 3, name: "settings", icon: "cog", advanced: true },
{
id: 4,
name: "translations",
icon: "globe",
advanced: true,
customNames: true,
},
{
id: 5,
name: "extra_scss",
icon: "paint-brush",
advanced: true,
customNames: true,
},
].map((target) => {
target["edited"] = this.hasEdited(target.name);
target["error"] = this.hasError(target.name);
return target;
});
},
@discourseComputed("theme_fields.[]")
fieldNames() {
const common = [
"scss",
"head_tag",
"header",
"after_header",
"body_tag",
"footer",
];
const scss_fields = (this.theme_fields || [])
.filter((f) => f.target === "extra_scss" && f.name !== "")
.map((f) => f.name);
if (scss_fields.length < 1) {
scss_fields.push("importable_scss");
}
return {
common: [
...common,
"color_definitions",
"embedded_scss",
"embedded_header",
],
desktop: common,
mobile: common,
settings: ["yaml"],
translations: [
"en",
...(this.theme_fields || [])
.filter((f) => f.target === "translations" && f.name !== "en")
.map((f) => f.name),
],
extra_scss: scss_fields,
};
},
@discourseComputed(
"fieldNames",
"theme_fields.[]",
"theme_fields.@each.error"
)
fields(fieldNames) {
const hash = {};
Object.keys(fieldNames).forEach((target) => {
hash[target] = fieldNames[target].map((fieldName) => {
const field = {
name: fieldName,
edited: this.hasEdited(target, fieldName),
error: this.hasError(target, fieldName),
};
if (target === "translations" || target === "extra_scss") {
field.translatedName = fieldName;
} else {
field.translatedName = I18n.t(
`admin.customize.theme.${fieldName}.text`
);
field.title = I18n.t(`admin.customize.theme.${fieldName}.title`);
}
if (fieldName.indexOf("_tag") > 0) {
field.icon = "far-file-alt";
}
return field;
});
});
return hash;
},
@discourseComputed("theme_fields")
themeFields(fields) {
if (!fields) {
this.set("theme_fields", []);
return {};
}
let hash = {};
fields.forEach((field) => {
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
hash[this.getKey(field)] = field;
}
});
return hash;
},
@discourseComputed("theme_fields", "theme_fields.[]")
uploads(fields) {
if (!fields) {
return [];
}
return fields.filter(
(f) => f.target === "common" && f.type_id === THEME_UPLOAD_VAR
);
},
@discourseComputed("theme_fields", "theme_fields.@each.error")
isBroken(fields) {
return (
fields && fields.any((field) => field.error && field.error.length > 0)
);
},
@discourseComputed("theme_fields.[]")
editedFields(fields) {
return fields.filter(
(field) => !isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
);
},
@discourseComputed("remote_theme.last_error_text")
remoteError(errorText) {
if (errorText && errorText.length > 0) {
return errorText;
}
},
getKey(field) {
return `${field.target} ${field.name}`;
},
hasEdited(target, name) {
if (name) {
return !isEmpty(this.getField(target, name));
} else {
let fields = this.theme_fields || [];
return fields.any(
(field) => field.target === target && !isEmpty(field.value)
);
}
},
hasError(target, name) {
return this.theme_fields
.filter((f) => f.target === target && (!name || name === f.name))
.any((f) => f.error);
},
getError(target, name) {
let themeFields = this.themeFields;
let key = this.getKey({ target, name });
let field = themeFields[key];
return field ? field.error : "";
},
getField(target, name) {
let themeFields = this.themeFields;
let key = this.getKey({ target, name });
let field = themeFields[key];
return field ? field.value : "";
},
removeField(field) {
this.set("changed", true);
field.upload_id = null;
field.value = null;
return this.saveChanges("theme_fields");
},
setField(target, name, value, upload_id, type_id) {
this.set("changed", true);
let themeFields = this.themeFields;
let field = { name, target, value, upload_id, type_id };
// slow path for uploads and so on
if (type_id && type_id > 1) {
let fields = this.theme_fields;
let existing = fields.find(
(f) => f.target === target && f.name === name && f.type_id === type_id
);
if (existing) {
existing.value = value;
existing.upload_id = upload_id;
} else {
fields.pushObject(field);
}
return;
}
// fast path
let key = this.getKey({ target, name });
let existingField = themeFields[key];
if (!existingField) {
this.theme_fields.pushObject(field);
themeFields[key] = field;
} else {
const changed =
(isEmpty(existingField.value) && !isEmpty(value)) ||
(isEmpty(value) && !isEmpty(existingField.value));
existingField.value = value;
if (changed) {
// Observing theme_fields.@each.value is too slow, so manually notify
// if the value goes to/from blank
this.notifyPropertyChange("theme_fields.[]");
}
}
},
@discourseComputed("childThemes.[]")
child_theme_ids(childThemes) {
if (childThemes) {
return childThemes.map((theme) => get(theme, "id"));
}
},
@discourseComputed("recentlyInstalled", "component", "hasParents")
warnUnassignedComponent(recent, component, hasParents) {
return recent && component && !hasParents;
},
removeChildTheme(theme) {
const childThemes = this.childThemes;
childThemes.removeObject(theme);
return this.saveChanges("child_theme_ids");
},
addChildTheme(theme) {
let childThemes = this.childThemes;
if (!childThemes) {
childThemes = [];
this.set("childThemes", childThemes);
}
childThemes.removeObject(theme);
childThemes.pushObject(theme);
return this.saveChanges("child_theme_ids");
},
addParentTheme(theme) {
let parentThemes = this.parentThemes;
if (!parentThemes) {
parentThemes = [];
this.set("parentThemes", parentThemes);
}
parentThemes.addObject(theme);
},
checkForUpdates() {
return this.save({ remote_check: true }).then(() =>
this.set("changed", false)
);
},
updateToLatest() {
return this.save({ remote_update: true }).then(() =>
this.set("changed", false)
);
},
changed: false,
saveChanges() {
const hash = this.getProperties.apply(this, arguments);
return this.save(hash)
.finally(() => this.set("changed", false))
.catch(popupAjaxError);
},
saveSettings(name, value) {
const settings = {};
settings[name] = value;
return this.save({ settings });
},
saveTranslation(name, value) {
return this.save({ translations: { [name]: value } });
},
});
export default Theme;