discourse/app/assets/javascripts/admin/addon/models/theme.js
Osama Sayegh 422f395042
FIX: Show unassigned component warning when installing multiple components successively (#11675)
A while ago we made a change to display a warning after installing a theme component when the admin tries to leave the page without adding the new installed component to any themes (see 5e29ae3ef5).

However there is an edge case that we forgot to address, and that's when an admin installs a component and then immediately opens the install modal again to install another one which can result in the warning being shown twice at the same time.

This PR prevents that by showing the warning when opening the install modal if the conditions are met (new component and not added to any themes) instead of showing it after installing the second component.
2021-01-11 18:29:12 +03:00

320 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, "embedded_scss", "color_definitions"],
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;