FEATURE: Buttons to add and remove objects for schema theme settings (#26256)

Continue from https://github.com/discourse/discourse/pull/25673.

This feature adds new buttons for schema theme settings that add/remove objects from lists.
This commit is contained in:
Osama Sayegh 2024-03-20 08:41:12 +03:00 committed by GitHub
parent c51ae33a54
commit ec63f3e782
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 277 additions and 7 deletions

View File

@ -33,8 +33,10 @@ class Node {
} }
class Tree { class Tree {
propertyName = null; @tracked nodes = [];
nodes = []; data = [];
propertyName;
schema;
} }
export default class SchemaThemeSettingEditor extends Component { export default class SchemaThemeSettingEditor extends Component {
@ -52,11 +54,16 @@ export default class SchemaThemeSettingEditor extends Component {
let schema = this.schema; let schema = this.schema;
let data = this.data; let data = this.data;
let tree = new Tree(); let tree = new Tree();
tree.data = data;
tree.schema = schema;
for (const point of this.history) { for (const point of this.history) {
tree.propertyName = point.propertyName; data = data[point.parentNode.index][point.propertyName];
data = data[point.node.index][point.propertyName];
schema = schema.properties[point.propertyName].schema; schema = schema.properties[point.propertyName].schema;
tree.propertyName = point.propertyName;
tree.schema = point.node.schema;
tree.data = data;
} }
data.forEach((object, index) => { data.forEach((object, index) => {
@ -78,6 +85,8 @@ export default class SchemaThemeSettingEditor extends Component {
for (const childObjectsProperty of childObjectsProperties) { for (const childObjectsProperty of childObjectsProperties) {
const subtree = new Tree(); const subtree = new Tree();
subtree.propertyName = childObjectsProperty.name; subtree.propertyName = childObjectsProperty.name;
subtree.schema = childObjectsProperty.schema;
subtree.data = data[index][childObjectsProperty.name];
data[index][childObjectsProperty.name]?.forEach( data[index][childObjectsProperty.name]?.forEach(
(childObj, childIndex) => { (childObj, childIndex) => {
@ -116,6 +125,10 @@ export default class SchemaThemeSettingEditor extends Component {
const node = this.activeNode; const node = this.activeNode;
const list = []; const list = [];
if (!node) {
return list;
}
for (const [name, spec] of Object.entries(node.schema.properties)) { for (const [name, spec] of Object.entries(node.schema.properties)) {
if (spec.type === "objects") { if (spec.type === "objects") {
continue; continue;
@ -174,7 +187,8 @@ export default class SchemaThemeSettingEditor extends Component {
onChildClick(node, tree, parentNode) { onChildClick(node, tree, parentNode) {
this.history.push({ this.history.push({
propertyName: tree.propertyName, propertyName: tree.propertyName,
node: parentNode, parentNode,
node,
}); });
this.backButtonText = I18n.t("admin.customize.theme.schema.back_button", { this.backButtonText = I18n.t("admin.customize.theme.schema.back_button", {
@ -187,11 +201,11 @@ export default class SchemaThemeSettingEditor extends Component {
@action @action
backButtonClick() { backButtonClick() {
const historyPoint = this.history.pop(); const historyPoint = this.history.pop();
this.activeIndex = historyPoint.node.index; this.activeIndex = historyPoint.parentNode.index;
if (this.history.length > 0) { if (this.history.length > 0) {
this.backButtonText = I18n.t("admin.customize.theme.schema.back_button", { this.backButtonText = I18n.t("admin.customize.theme.schema.back_button", {
name: this.history[this.history.length - 1].node.text, name: this.history[this.history.length - 1].parentNode.text,
}); });
} else { } else {
this.backButtonText = null; this.backButtonText = null;
@ -207,6 +221,26 @@ export default class SchemaThemeSettingEditor extends Component {
this.activeNode.object[field.name] = newVal; this.activeNode.object[field.name] = newVal;
} }
@action
addItem(tree) {
const schema = tree.schema;
const node = this.createNodeFromSchema(schema, tree);
tree.data.push(node.object);
tree.nodes = [...tree.nodes, node];
}
@action
removeItem() {
const data = this.activeNode.parentTree.data;
data.splice(this.activeIndex, 1);
this.tree.nodes = this.tree.nodes.filter((n, i) => i !== this.activeIndex);
if (data.length > 0) {
this.activeIndex = Math.max(this.activeIndex - 1, 0);
} else if (this.history.length > 0) {
this.backButtonClick();
}
}
fieldDescription(fieldName) { fieldDescription(fieldName) {
const descriptions = this.args.setting.objects_schema_property_descriptions; const descriptions = this.args.setting.objects_schema_property_descriptions;
@ -225,6 +259,30 @@ export default class SchemaThemeSettingEditor extends Component {
return descriptions[key]; return descriptions[key];
} }
createNodeFromSchema(schema, tree) {
const object = {};
const index = tree.nodes.length;
const defaultName = `${schema.name} ${index + 1}`;
if (schema.identifier) {
object[schema.identifier] = defaultName;
}
for (const [name, spec] of Object.entries(schema.properties)) {
if (spec.type === "objects") {
object[name] = [];
}
}
return new Node({
schema,
object,
index,
text: defaultName,
parentTree: tree,
});
}
<template> <template>
<div class="schema-theme-setting-editor"> <div class="schema-theme-setting-editor">
<div class="schema-theme-setting-editor__navigation"> <div class="schema-theme-setting-editor__navigation">
@ -286,9 +344,23 @@ export default class SchemaThemeSettingEditor extends Component {
</div> </div>
</li> </li>
{{/each}} {{/each}}
<DButton
@action={{fn this.addItem nestedTree}}
@translatedLabel={{nestedTree.schema.name}}
@icon="plus"
class="schema-theme-setting-editor__tree-add-button --child"
data-test-parent-index={{node.index}}
/>
{{/each}} {{/each}}
{{/each}} {{/each}}
</ul> </ul>
<DButton
@action={{fn this.addItem this.tree}}
@translatedLabel={{this.tree.schema.name}}
@icon="plus"
class="schema-theme-setting-editor__tree-add-button --root"
/>
</div> </div>
<div class="schema-theme-setting-editor__fields"> <div class="schema-theme-setting-editor__fields">
@ -301,6 +373,13 @@ export default class SchemaThemeSettingEditor extends Component {
@description={{field.description}} @description={{field.description}}
/> />
{{/each}} {{/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>
<div class="schema-theme-setting-editor__footer"> <div class="schema-theme-setting-editor__footer">

View File

@ -39,11 +39,17 @@ class TreeFromDOM {
const childrenHeaderElement = query( const childrenHeaderElement = query(
`.schema-theme-setting-editor__tree-node.--heading[data-test-parent-index="${index}"]` `.schema-theme-setting-editor__tree-node.--heading[data-test-parent-index="${index}"]`
); );
const addButtons = [
...queryAll(
`.schema-theme-setting-editor__tree-add-button.--child[data-test-parent-index="${index}"]`
),
];
return { return {
active, active,
children, children,
childrenHeaderElement, childrenHeaderElement,
addButtons,
element: li, element: li,
textElement: li.querySelector( textElement: li.querySelector(
".schema-theme-setting-editor__tree-node-text" ".schema-theme-setting-editor__tree-node-text"
@ -72,6 +78,10 @@ class InputFieldsFromDOM {
} }
} }
const TOP_LEVEL_ADD_BTN =
".schema-theme-setting-editor__tree-add-button.--root";
const REMOVE_ITEM_BTN = ".schema-theme-setting-editor__remove-btn";
module( module(
"Integration | Admin | Component | schema-theme-setting/editor", "Integration | Admin | Component | schema-theme-setting/editor",
function (hooks) { function (hooks) {
@ -800,5 +810,180 @@ module(
assert.dom(inputFields.fields.text.inputElement).hasValue("Talk to us"); assert.dom(inputFields.fields.text.inputElement).hasValue("Talk to us");
}); });
test("adding an object to the root list of objects", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
assert.dom(TOP_LEVEL_ADD_BTN).hasText("level1");
const tree = new TreeFromDOM();
assert.strictEqual(tree.nodes.length, 2);
await click(TOP_LEVEL_ADD_BTN);
await click(TOP_LEVEL_ADD_BTN);
tree.refresh();
assert.strictEqual(tree.nodes.length, 4);
assert.dom(tree.nodes[2].textElement).hasText("level1 3");
assert.dom(tree.nodes[3].textElement).hasText("level1 4");
});
test("adding an object to a child list of objects", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const tree = new TreeFromDOM();
assert.dom(tree.nodes[0].addButtons[0]).hasText("level2");
assert.strictEqual(tree.nodes[0].children.length, 2);
await click(tree.nodes[0].addButtons[0]);
tree.refresh();
await click(tree.nodes[0].addButtons[0]);
tree.refresh();
assert.strictEqual(tree.nodes[0].children.length, 4);
assert.dom(tree.nodes[0].children[2].textElement).hasText("level2 3");
assert.dom(tree.nodes[0].children[3].textElement).hasText("level2 4");
});
test("navigating 1 level deep and adding an object to the child list of objects that's displayed as the root list", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const tree = new TreeFromDOM();
await click(tree.nodes[0].children[0].element);
tree.refresh();
assert.dom(TOP_LEVEL_ADD_BTN).hasText("level2");
assert.strictEqual(tree.nodes.length, 2);
await click(TOP_LEVEL_ADD_BTN);
await click(TOP_LEVEL_ADD_BTN);
tree.refresh();
assert.strictEqual(tree.nodes.length, 4);
assert.dom(tree.nodes[2].textElement).hasText("level2 3");
assert.dom(tree.nodes[3].textElement).hasText("level2 4");
});
test("navigating 1 level deep and adding an object to a grandchild list of objects", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const tree = new TreeFromDOM();
await click(tree.nodes[0].children[0].element);
tree.refresh();
assert.dom(tree.nodes[0].addButtons[0]).hasText("level3");
assert.strictEqual(tree.nodes[0].children.length, 2);
await click(tree.nodes[0].addButtons[0]);
tree.refresh();
await click(tree.nodes[0].addButtons[0]);
tree.refresh();
assert.strictEqual(tree.nodes[0].children.length, 4);
assert.dom(tree.nodes[0].children[2].textElement).hasText("level3 3");
assert.dom(tree.nodes[0].children[3].textElement).hasText("level3 4");
});
test("removing an object from the root list of objects", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const tree = new TreeFromDOM();
const inputFields = new InputFieldsFromDOM();
assert.strictEqual(tree.nodes.length, 2);
assert.dom(tree.nodes[0].textElement).hasText("item 1");
assert.dom(tree.nodes[1].textElement).hasText("item 2");
assert.dom(inputFields.fields.name.inputElement).hasValue("item 1");
await click(REMOVE_ITEM_BTN);
tree.refresh();
inputFields.refresh();
assert.strictEqual(tree.nodes.length, 1);
assert.dom(tree.nodes[0].textElement).hasText("item 2");
assert.dom(inputFields.fields.name.inputElement).hasValue("item 2");
await click(REMOVE_ITEM_BTN);
tree.refresh();
inputFields.refresh();
assert.strictEqual(tree.nodes.length, 0);
assert.strictEqual(inputFields.count, 0);
assert.dom(REMOVE_ITEM_BTN).doesNotExist();
assert.dom(TOP_LEVEL_ADD_BTN).hasText("level1");
});
test("navigating 1 level deep and removing an object from the child list of objects", async function (assert) {
const setting = schemaAndData(1);
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const tree = new TreeFromDOM();
await click(tree.nodes[0].children[1].element);
tree.refresh();
const inputFields = new InputFieldsFromDOM();
assert.strictEqual(tree.nodes.length, 2);
assert.dom(tree.nodes[0].textElement).hasText("child 1-1");
assert.dom(tree.nodes[1].textElement).hasText("child 1-2");
assert.dom(inputFields.fields.name.inputElement).hasValue("child 1-2");
await click(REMOVE_ITEM_BTN);
tree.refresh();
inputFields.refresh();
assert.strictEqual(tree.nodes.length, 1);
assert.dom(tree.nodes[0].textElement).hasText("child 1-1");
assert.dom(inputFields.fields.name.inputElement).hasValue("child 1-1");
// removing the last object navigates back to the previous level
await click(REMOVE_ITEM_BTN);
tree.refresh();
inputFields.refresh();
assert.strictEqual(tree.nodes.length, 2);
assert.strictEqual(tree.nodes[0].children.length, 0);
assert.dom(tree.nodes[0].textElement).hasText("item 1");
assert.dom(tree.nodes[1].textElement).hasText("item 2");
assert.dom(inputFields.fields.name.inputElement).hasValue("item 1");
assert
.dom(".schema-theme-setting-editor__tree-node--back-btn")
.doesNotExist();
});
} }
); );

View File

@ -64,6 +64,12 @@
font-weight: bold; font-weight: bold;
} }
} }
.schema-theme-setting-editor__tree-add-button {
&.--child {
margin-left: 0.5em;
}
}
} }
} }