mirror of
https://github.com/discourse/discourse.git
synced 2025-02-08 20:34:52 +00:00
Why this change? Prior to this change, the `SchemaThemeSetting::Editor#tree` was creating a new `Tree` instance which holds instances of `Node`. Both classes consisted of tracked properties. The problem with this approach is that when any tracked properties is updated, Ember will revaluate `SchemaThemeSetting::Editor#tree` and because that method always return a new instance of `Tree`, it causes the whole navigation tree to rerender just because on tracked property changed. This rerendering of the whole navigation tree every time made it hard to implement simple features like hiding a section in 9baa820d53757aec36e4cb86efab8d0bb3f264c0. Instead of being able to just declare a tracked property to hide/show a section, we end up with a more complicated solution. This commit rewrites `SchemaThemeSetting::Editor` to depend on Ember components to form the tree structure instead. As needed, each component in the tree structure can declare its own tracked property as necessary.
282 lines
7.3 KiB
Plaintext
282 lines
7.3 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { fn } from "@ember/helper";
|
|
import { action } from "@ember/object";
|
|
import { service } from "@ember/service";
|
|
import { gt } from "truth-helpers";
|
|
import DButton from "discourse/components/d-button";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { cloneJSON } from "discourse-common/lib/object";
|
|
import I18n from "discourse-i18n";
|
|
import Tree from "admin/components/schema-theme-setting/editor/tree";
|
|
import FieldInput from "admin/components/schema-theme-setting/field";
|
|
|
|
export default class SchemaThemeSettingNewEditor extends Component {
|
|
@service router;
|
|
|
|
@tracked history = [];
|
|
@tracked activeIndex = 0;
|
|
@tracked activeDataPaths = [];
|
|
@tracked activeSchemaPaths = [];
|
|
@tracked saveButtonDisabled = false;
|
|
inputFieldObserver = new Map();
|
|
|
|
data = cloneJSON(this.args.setting.value);
|
|
schema = this.args.setting.objects_schema;
|
|
|
|
@action
|
|
onChildClick(index, propertyName, parentNodeIndex) {
|
|
this.history.pushObject({
|
|
dataPaths: [...this.activeDataPaths],
|
|
schemaPaths: [...this.activeSchemaPaths],
|
|
index: this.activeIndex,
|
|
});
|
|
|
|
this.activeIndex = index;
|
|
this.activeDataPaths.pushObjects([parentNodeIndex, propertyName]);
|
|
this.activeSchemaPaths.pushObject(propertyName);
|
|
this.inputFieldObserver.clear();
|
|
}
|
|
|
|
@action
|
|
updateIndex(index) {
|
|
this.activeIndex = index;
|
|
}
|
|
|
|
generateSchemaTitle(object, schema, index) {
|
|
return object[schema.identifier] || `${schema.name} ${index + 1}`;
|
|
}
|
|
|
|
get backButtonText() {
|
|
if (this.history.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const lastHistory = this.history[this.history.length - 1];
|
|
|
|
return I18n.t("admin.customize.theme.schema.back_button", {
|
|
name: this.generateSchemaTitle(
|
|
this.#resolveDataFromPaths(lastHistory.dataPaths)[lastHistory.index],
|
|
this.#resolveSchemaFromPaths(lastHistory.schemaPaths),
|
|
lastHistory.index
|
|
),
|
|
});
|
|
}
|
|
|
|
get activeData() {
|
|
return this.#resolveDataFromPaths(this.activeDataPaths);
|
|
}
|
|
|
|
#resolveDataFromPaths(paths) {
|
|
if (paths.length === 0) {
|
|
return this.data;
|
|
}
|
|
|
|
let data = this.data;
|
|
|
|
paths.forEach((path) => {
|
|
data = data[path];
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
get activeSchema() {
|
|
return this.#resolveSchemaFromPaths(this.activeSchemaPaths);
|
|
}
|
|
|
|
#resolveSchemaFromPaths(paths) {
|
|
if (paths.length === 0) {
|
|
return this.schema;
|
|
}
|
|
|
|
let schema = this.schema;
|
|
|
|
paths.forEach((path) => {
|
|
schema = schema.properties[path].schema;
|
|
});
|
|
|
|
return schema;
|
|
}
|
|
|
|
@action
|
|
registerInputFieldObserver(index, callback) {
|
|
this.inputFieldObserver[index] = callback;
|
|
}
|
|
|
|
@action
|
|
unregisterInputFieldObserver(index) {
|
|
delete this.inputFieldObserver[index];
|
|
}
|
|
|
|
descriptions(fieldName, key) {
|
|
// The `property_descriptions` metadata is an object with keys in the following format as an example:
|
|
//
|
|
// {
|
|
// some_property.description: <some description>,
|
|
// some_property.label: <some label>,
|
|
// some_objects_property.some_other_property.description: <some description>,
|
|
// some_objects_property.some_other_property.label: <some label>,
|
|
// }
|
|
const descriptions = this.args.setting.metadata?.property_descriptions;
|
|
|
|
if (!descriptions) {
|
|
return;
|
|
}
|
|
|
|
if (this.activeSchemaPaths.length > 0) {
|
|
key = `${this.activeSchemaPaths.join(".")}.${fieldName}.${key}`;
|
|
} else {
|
|
key = `${fieldName}.${key}`;
|
|
}
|
|
|
|
return descriptions[key];
|
|
}
|
|
|
|
fieldLabel(fieldName) {
|
|
return this.descriptions(fieldName, "label") || fieldName;
|
|
}
|
|
|
|
fieldDescription(fieldName) {
|
|
return this.descriptions(fieldName, "description");
|
|
}
|
|
|
|
get fields() {
|
|
const list = [];
|
|
|
|
if (this.activeData.length !== 0) {
|
|
for (const [name, spec] of Object.entries(this.activeSchema.properties)) {
|
|
if (spec.type === "objects") {
|
|
continue;
|
|
}
|
|
|
|
list.push({
|
|
name,
|
|
spec,
|
|
value: this.activeData[this.activeIndex][name],
|
|
description: this.fieldDescription(name),
|
|
label: this.fieldLabel(name),
|
|
});
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
@action
|
|
clickBack() {
|
|
const {
|
|
dataPaths: lastDataPaths,
|
|
schemaPaths: lastSchemaPaths,
|
|
index: lastIndex,
|
|
} = this.history.popObject();
|
|
|
|
this.activeDataPaths = lastDataPaths;
|
|
this.activeSchemaPaths = lastSchemaPaths;
|
|
this.activeIndex = lastIndex;
|
|
this.inputFieldObserver.clear();
|
|
}
|
|
|
|
@action
|
|
addChildItem(propertyName, parentNodeIndex) {
|
|
this.activeData[parentNodeIndex][propertyName].pushObject({});
|
|
}
|
|
|
|
@action
|
|
addItem() {
|
|
this.activeData.pushObject({});
|
|
}
|
|
|
|
@action
|
|
removeItem() {
|
|
this.activeData.removeAt(this.activeIndex);
|
|
|
|
if (this.activeData.length > 0) {
|
|
this.activeIndex = Math.max(this.activeIndex - 1, 0);
|
|
} else if (this.history.length > 0) {
|
|
this.clickBack();
|
|
} else {
|
|
this.activeIndex = 0;
|
|
}
|
|
}
|
|
|
|
@action
|
|
inputFieldChanged(field, newVal) {
|
|
this.activeData[this.activeIndex][field.name] = newVal;
|
|
|
|
if (field.name === this.activeSchema.identifier) {
|
|
this.inputFieldObserver[this.activeIndex]();
|
|
}
|
|
}
|
|
|
|
@action
|
|
saveChanges() {
|
|
this.saveButtonDisabled = true;
|
|
|
|
this.args.setting
|
|
.updateSetting(this.args.themeId, this.data)
|
|
.then((result) => {
|
|
this.args.setting.set("value", result[this.args.setting.setting]);
|
|
|
|
this.router.transitionTo(
|
|
"adminCustomizeThemes.show",
|
|
this.args.themeId
|
|
);
|
|
})
|
|
.catch(popupAjaxError)
|
|
.finally(() => (this.saveButtonDisabled = false));
|
|
}
|
|
|
|
<template>
|
|
<div class="schema-theme-setting-editor">
|
|
<div class="schema-theme-setting-editor__navigation">
|
|
<Tree
|
|
@data={{this.activeData}}
|
|
@schema={{this.activeSchema}}
|
|
@onChildClick={{this.onChildClick}}
|
|
@clickBack={{this.clickBack}}
|
|
@backButtonText={{this.backButtonText}}
|
|
@activeIndex={{this.activeIndex}}
|
|
@updateIndex={{this.updateIndex}}
|
|
@addItem={{this.addItem}}
|
|
@addChildItem={{this.addChildItem}}
|
|
@generateSchemaTitle={{this.generateSchemaTitle}}
|
|
@registerInputFieldObserver={{this.registerInputFieldObserver}}
|
|
@unregisterInputFieldObserver={{this.unregisterInputFieldObserver}}
|
|
/>
|
|
|
|
<div class="schema-theme-setting-editor__footer">
|
|
<DButton
|
|
@disabled={{this.saveButtonDisabled}}
|
|
@action={{this.saveChanges}}
|
|
@label="save"
|
|
class="btn-primary"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="schema-theme-setting-editor__fields">
|
|
{{#each this.fields as |field|}}
|
|
<FieldInput
|
|
@name={{field.name}}
|
|
@value={{field.value}}
|
|
@spec={{field.spec}}
|
|
@onValueChange={{fn this.inputFieldChanged field}}
|
|
@description={{field.description}}
|
|
@label={{field.label}}
|
|
@setting={{@setting}}
|
|
/>
|
|
{{/each}}
|
|
|
|
{{#if (gt this.fields.length 0)}}
|
|
<DButton
|
|
@action={{this.removeItem}}
|
|
@icon="trash-alt"
|
|
class="btn-danger schema-theme-setting-editor__remove-btn"
|
|
/>
|
|
{{/if}}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
}
|