2019-10-31 16:28:10 -04:00
|
|
|
import { get } from "@ember/object";
|
2019-10-31 13:37:24 -04:00
|
|
|
import { isEmpty } from "@ember/utils";
|
2019-10-30 16:28:29 -04:00
|
|
|
import { or, gt } from "@ember/object/computed";
|
2018-06-15 11:03:24 -04:00
|
|
|
import RestModel from "discourse/models/rest";
|
2020-01-16 12:56:53 -05:00
|
|
|
import discourseComputed from "discourse-common/utils/decorators";
|
2018-08-08 00:46:34 -04:00
|
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
2019-05-02 21:43:54 -04:00
|
|
|
import { ajax } from "discourse/lib/ajax";
|
|
|
|
import { escapeExpression } from "discourse/lib/utilities";
|
|
|
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
2020-02-04 11:03:19 -05:00
|
|
|
import { url } from "discourse/lib/computed";
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2017-05-09 17:20:28 -04:00
|
|
|
const THEME_UPLOAD_VAR = 2;
|
2019-05-28 06:15:12 -04:00
|
|
|
const FIELDS_IDS = [0, 1, 5];
|
2017-05-09 17:20:28 -04:00
|
|
|
|
2018-08-30 15:23:15 -04:00
|
|
|
export const THEMES = "themes";
|
|
|
|
export const COMPONENTS = "components";
|
2018-09-06 14:56:00 -04:00
|
|
|
const SETTINGS_TYPE_ID = 5;
|
2018-08-30 15:23:15 -04:00
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
const Theme = RestModel.extend({
|
2019-10-30 16:28:29 -04:00
|
|
|
isActive: or("default", "user_selectable"),
|
|
|
|
isPendingUpdates: gt("remote_theme.commits_behind", 0),
|
|
|
|
hasEditedFields: gt("editedFields.length", 0),
|
2019-12-04 01:13:41 -05:00
|
|
|
hasParents: gt("parent_themes.length", 0),
|
2020-02-04 11:03:19 -05:00
|
|
|
diffLocalChangesUrl: url("id", "/admin/themes/%@/diff_local_changes"),
|
2018-03-28 17:02:34 -04:00
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields.[]")
|
2019-02-19 07:56:01 -05:00
|
|
|
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
|
2019-04-12 06:36:08 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 5,
|
|
|
|
name: "extra_scss",
|
|
|
|
icon: "paint-brush",
|
|
|
|
advanced: true,
|
|
|
|
customNames: true
|
2019-02-19 07:56:01 -05:00
|
|
|
}
|
|
|
|
].map(target => {
|
|
|
|
target["edited"] = this.hasEdited(target.name);
|
|
|
|
target["error"] = this.hasError(target.name);
|
|
|
|
return target;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields.[]")
|
2019-02-19 07:56:01 -05:00
|
|
|
fieldNames() {
|
|
|
|
const common = [
|
|
|
|
"scss",
|
|
|
|
"head_tag",
|
|
|
|
"header",
|
|
|
|
"after_header",
|
|
|
|
"body_tag",
|
|
|
|
"footer"
|
|
|
|
];
|
|
|
|
|
2019-05-27 04:15:39 -04:00
|
|
|
const scss_fields = (this.theme_fields || [])
|
2019-04-12 06:36:08 -04:00
|
|
|
.filter(f => f.target === "extra_scss" && f.name !== "")
|
|
|
|
.map(f => f.name);
|
|
|
|
|
|
|
|
if (scss_fields.length < 1) {
|
|
|
|
scss_fields.push("importable_scss");
|
|
|
|
}
|
|
|
|
|
2019-02-19 07:56:01 -05:00
|
|
|
return {
|
|
|
|
common: [...common, "embedded_scss"],
|
|
|
|
desktop: common,
|
|
|
|
mobile: common,
|
|
|
|
settings: ["yaml"],
|
|
|
|
translations: [
|
|
|
|
"en",
|
2019-05-27 04:15:39 -04:00
|
|
|
...(this.theme_fields || [])
|
2019-02-19 07:56:01 -05:00
|
|
|
.filter(f => f.target === "translations" && f.name !== "en")
|
|
|
|
.map(f => f.name)
|
2019-04-12 06:36:08 -04:00
|
|
|
],
|
|
|
|
extra_scss: scss_fields
|
2019-02-19 07:56:01 -05:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed(
|
|
|
|
"fieldNames",
|
|
|
|
"theme_fields.[]",
|
|
|
|
"theme_fields.@each.error"
|
|
|
|
)
|
2019-02-19 07:56:01 -05:00
|
|
|
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)
|
|
|
|
};
|
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
if (target === "translations" || target === "extra_scss") {
|
2019-02-19 07:56:01 -05:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields")
|
2017-04-12 10:52:52 -04:00
|
|
|
themeFields(fields) {
|
|
|
|
if (!fields) {
|
2018-06-15 11:03:24 -04:00
|
|
|
this.set("theme_fields", []);
|
2017-04-12 10:52:52 -04:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
let hash = {};
|
2018-03-04 19:04:23 -05:00
|
|
|
fields.forEach(field => {
|
2019-05-28 06:15:12 -04:00
|
|
|
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
2018-03-04 19:04:23 -05:00
|
|
|
hash[this.getKey(field)] = field;
|
|
|
|
}
|
|
|
|
});
|
2017-04-12 10:52:52 -04:00
|
|
|
return hash;
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields", "theme_fields.[]")
|
2017-05-09 17:20:28 -04:00
|
|
|
uploads(fields) {
|
|
|
|
if (!fields) {
|
|
|
|
return [];
|
|
|
|
}
|
2018-06-15 11:03:24 -04:00
|
|
|
return fields.filter(
|
|
|
|
f => f.target === "common" && f.type_id === THEME_UPLOAD_VAR
|
|
|
|
);
|
2017-05-09 17:20:28 -04:00
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields", "theme_fields.@each.error")
|
2018-08-30 15:23:15 -04:00
|
|
|
isBroken(fields) {
|
2019-05-16 05:38:47 -04:00
|
|
|
return fields && fields.any(field => field.error && field.error.length > 0);
|
2018-08-30 15:23:15 -04:00
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("theme_fields.[]")
|
2018-09-06 14:56:00 -04:00
|
|
|
editedFields(fields) {
|
|
|
|
return fields.filter(
|
2019-01-11 11:54:23 -05:00
|
|
|
field => !Ember.isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
|
2018-09-06 14:56:00 -04:00
|
|
|
);
|
2018-08-30 15:23:15 -04:00
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("remote_theme.last_error_text")
|
2018-09-08 09:24:11 -04:00
|
|
|
remoteError(errorText) {
|
|
|
|
if (errorText && errorText.length > 0) {
|
|
|
|
return errorText;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-06-15 11:03:24 -04:00
|
|
|
getKey(field) {
|
2018-03-04 19:04:23 -05:00
|
|
|
return `${field.target} ${field.name}`;
|
2017-05-09 17:20:28 -04:00
|
|
|
},
|
|
|
|
|
2018-06-15 11:03:24 -04:00
|
|
|
hasEdited(target, name) {
|
2017-04-19 15:24:00 -04:00
|
|
|
if (name) {
|
2019-10-31 13:37:24 -04:00
|
|
|
return !isEmpty(this.getField(target, name));
|
2017-04-19 15:24:00 -04:00
|
|
|
} else {
|
2019-05-27 04:15:39 -04:00
|
|
|
let fields = this.theme_fields || [];
|
2018-06-15 11:03:24 -04:00
|
|
|
return fields.any(
|
2019-10-31 13:37:24 -04:00
|
|
|
field => field.target === target && !isEmpty(field.value)
|
2018-06-15 11:03:24 -04:00
|
|
|
);
|
2017-04-19 15:24:00 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-02-19 07:56:01 -05:00
|
|
|
hasError(target, name) {
|
2019-05-27 04:15:39 -04:00
|
|
|
return this.theme_fields
|
2019-02-19 15:02:29 -05:00
|
|
|
.filter(f => f.target === target && (!name || name === f.name))
|
2019-02-19 07:56:01 -05:00
|
|
|
.any(f => f.error);
|
|
|
|
},
|
|
|
|
|
2017-04-19 16:46:28 -04:00
|
|
|
getError(target, name) {
|
2019-05-27 04:15:39 -04:00
|
|
|
let themeFields = this.themeFields;
|
2018-06-15 11:03:24 -04:00
|
|
|
let key = this.getKey({ target, name });
|
2017-04-19 16:46:28 -04:00
|
|
|
let field = themeFields[key];
|
|
|
|
return field ? field.error : "";
|
|
|
|
},
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
getField(target, name) {
|
2019-05-27 04:15:39 -04:00
|
|
|
let themeFields = this.themeFields;
|
2018-06-15 11:03:24 -04:00
|
|
|
let key = this.getKey({ target, name });
|
2017-04-12 10:52:52 -04:00
|
|
|
let field = themeFields[key];
|
|
|
|
return field ? field.value : "";
|
|
|
|
},
|
|
|
|
|
2017-05-09 17:20:28 -04:00
|
|
|
removeField(field) {
|
2017-04-12 10:52:52 -04:00
|
|
|
this.set("changed", true);
|
|
|
|
|
2017-05-09 17:20:28 -04:00
|
|
|
field.upload_id = null;
|
|
|
|
field.value = null;
|
|
|
|
|
|
|
|
return this.saveChanges("theme_fields");
|
|
|
|
},
|
|
|
|
|
|
|
|
setField(target, name, value, upload_id, type_id) {
|
|
|
|
this.set("changed", true);
|
2019-05-27 04:15:39 -04:00
|
|
|
let themeFields = this.themeFields;
|
2018-06-15 11:03:24 -04:00
|
|
|
let field = { name, target, value, upload_id, type_id };
|
2017-05-09 17:20:28 -04:00
|
|
|
|
|
|
|
// slow path for uploads and so on
|
|
|
|
if (type_id && type_id > 1) {
|
2019-05-27 04:15:39 -04:00
|
|
|
let fields = this.theme_fields;
|
2018-06-15 11:03:24 -04:00
|
|
|
let existing = fields.find(
|
|
|
|
f => f.target === target && f.name === name && f.type_id === type_id
|
|
|
|
);
|
2017-05-09 17:20:28 -04:00
|
|
|
if (existing) {
|
|
|
|
existing.value = value;
|
|
|
|
existing.upload_id = upload_id;
|
|
|
|
} else {
|
2019-02-19 07:56:01 -05:00
|
|
|
fields.pushObject(field);
|
2017-05-09 17:20:28 -04:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fast path
|
2018-06-15 11:03:24 -04:00
|
|
|
let key = this.getKey({ target, name });
|
2017-05-09 17:20:28 -04:00
|
|
|
let existingField = themeFields[key];
|
|
|
|
if (!existingField) {
|
2019-02-19 07:56:01 -05:00
|
|
|
this.theme_fields.pushObject(field);
|
2017-04-12 10:52:52 -04:00
|
|
|
themeFields[key] = field;
|
|
|
|
} else {
|
2019-02-19 16:49:31 -05:00
|
|
|
const changed =
|
2019-10-31 13:37:24 -04:00
|
|
|
(isEmpty(existingField.value) && !isEmpty(value)) ||
|
|
|
|
(isEmpty(value) && !isEmpty(existingField.value));
|
2019-02-19 16:49:31 -05:00
|
|
|
|
|
|
|
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.[]");
|
2019-02-19 07:56:01 -05:00
|
|
|
}
|
2017-04-12 10:52:52 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("childThemes.[]")
|
2017-04-12 10:52:52 -04:00
|
|
|
child_theme_ids(childThemes) {
|
|
|
|
if (childThemes) {
|
2019-10-31 16:28:10 -04:00
|
|
|
return childThemes.map(theme => get(theme, "id"));
|
2017-04-12 10:52:52 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
removeChildTheme(theme) {
|
2019-05-27 04:15:39 -04:00
|
|
|
const childThemes = this.childThemes;
|
2017-04-12 10:52:52 -04:00
|
|
|
childThemes.removeObject(theme);
|
|
|
|
return this.saveChanges("child_theme_ids");
|
|
|
|
},
|
|
|
|
|
2018-06-15 11:03:24 -04:00
|
|
|
addChildTheme(theme) {
|
2019-05-27 04:15:39 -04:00
|
|
|
let childThemes = this.childThemes;
|
2017-04-20 17:37:13 -04:00
|
|
|
if (!childThemes) {
|
|
|
|
childThemes = [];
|
2018-06-15 11:03:24 -04:00
|
|
|
this.set("childThemes", childThemes);
|
2017-04-20 17:37:13 -04:00
|
|
|
}
|
2017-04-12 10:52:52 -04:00
|
|
|
childThemes.removeObject(theme);
|
|
|
|
childThemes.pushObject(theme);
|
|
|
|
return this.saveChanges("child_theme_ids");
|
|
|
|
},
|
|
|
|
|
2019-11-28 00:19:01 -05:00
|
|
|
addParentTheme(theme) {
|
|
|
|
let parentThemes = this.parentThemes;
|
|
|
|
if (!parentThemes) {
|
|
|
|
parentThemes = [];
|
|
|
|
this.set("parentThemes", parentThemes);
|
|
|
|
}
|
|
|
|
parentThemes.addObject(theme);
|
|
|
|
},
|
|
|
|
|
2019-11-07 16:38:28 -05:00
|
|
|
@discourseComputed("name", "default")
|
2017-04-12 10:52:52 -04:00
|
|
|
description: function(name, isDefault) {
|
|
|
|
if (isDefault) {
|
2018-06-15 11:03:24 -04:00
|
|
|
return I18n.t("admin.customize.theme.default_name", { name: name });
|
2017-04-12 10:52:52 -04:00
|
|
|
} else {
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
checkForUpdates() {
|
2018-06-15 11:03:24 -04:00
|
|
|
return this.save({ remote_check: true }).then(() =>
|
|
|
|
this.set("changed", false)
|
|
|
|
);
|
2017-04-12 10:52:52 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
updateToLatest() {
|
2020-02-04 11:03:19 -05:00
|
|
|
return ajax(this.diffLocalChangesUrl).then(json => {
|
2019-05-02 21:43:54 -04:00
|
|
|
if (json && json.error) {
|
|
|
|
bootbox.alert(
|
|
|
|
I18n.t("generic_error_with_reason", {
|
|
|
|
error: json.error
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else if (json && json.diff) {
|
|
|
|
bootbox.confirm(
|
|
|
|
I18n.t("admin.customize.theme.update_confirm") +
|
|
|
|
`<pre><code class="diff">${escapeExpression(
|
|
|
|
json.diff
|
|
|
|
)}</code></pre>`,
|
|
|
|
I18n.t("cancel"),
|
|
|
|
I18n.t("admin.customize.theme.update_confirm_yes"),
|
|
|
|
result => {
|
|
|
|
if (result) {
|
|
|
|
return this.save({ remote_update: true }).then(() =>
|
|
|
|
this.set("changed", false)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
highlightSyntax();
|
|
|
|
} else {
|
|
|
|
return this.save({ remote_update: true }).then(() =>
|
|
|
|
this.set("changed", false)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2017-04-12 10:52:52 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
changed: false,
|
|
|
|
|
|
|
|
saveChanges() {
|
|
|
|
const hash = this.getProperties.apply(this, arguments);
|
2018-08-08 00:46:34 -04:00
|
|
|
return this.save(hash)
|
|
|
|
.finally(() => this.set("changed", false))
|
|
|
|
.catch(popupAjaxError);
|
2017-04-12 10:52:52 -04:00
|
|
|
},
|
|
|
|
|
2018-03-04 19:04:23 -05:00
|
|
|
saveSettings(name, value) {
|
|
|
|
const settings = {};
|
|
|
|
settings[name] = value;
|
|
|
|
return this.save({ settings });
|
2019-01-17 06:46:11 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
saveTranslation(name, value) {
|
|
|
|
return this.save({ translations: { [name]: value } });
|
2018-03-04 19:04:23 -05:00
|
|
|
}
|
2017-04-12 10:52:52 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
export default Theme;
|