Editor Extensions¶
Editor Extensions let you write custom JavaScript tools that run inside the Balancy Editor. Use them to automate workflows, manipulate CMS data programmatically, and build team-specific utilities — all from within the browser.
Extensions run in a sandboxed Web Worker and communicate with the editor via RPC. All API methods are asynchronous — always use await.
Quick Start¶
Create a new JavaScript DataObject in the Balancy Editor with the following structure:
class MyExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("MyExtension loaded!");
}
// @menuItem { "path": "Tools/My Tool/Say Hello", "priority": 10 }
async sayHello() {
const items = await Balancy.CMS.getModels("SmartObjects.Item");
Balancy.Editor.Log.info("Found " + items.length + " items");
}
}
The extension will:
- Run
onInit()when the editor loads. - Register a Say Hello action under Tools > My Tool in the Extensions menu (puzzle icon in the header bar).
- When clicked, query all
SmartObjects.Itemdocuments and log the count.
Extension Lifecycle¶
BaseExtension¶
All extensions must extend Balancy.Editor.BaseExtension. The class provides default no-op implementations for all lifecycle methods.
class MyExtension extends Balancy.Editor.BaseExtension {
// Override any lifecycle methods you need
}
The class name (e.g. MyExtension) is used as the extension's unique identifier. It appears in log entries and menu item IDs.
Lifecycle Methods¶
| Method | When Called | Return Value |
|---|---|---|
async onInit() |
Once, when the extension bundle is loaded and bootstrapped | void |
async onDispose() |
When extensions are reloaded or the editor is closed | void |
async preSave(ctx) |
Before a save operation (stub — not yet implemented) | boolean |
async postSave(ctx) |
After a save operation (stub — not yet implemented) | void |
Example:
class SetupExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("Extension initialized");
// Pre-load data, set up state, etc.
}
async onDispose() {
Balancy.Editor.Log.info("Extension disposed");
// Clean up resources
}
}
Menu Items¶
@menuItem Annotation¶
Annotate methods with // @menuItem { ... } to register them as clickable actions in the Extensions dropdown menu (puzzle icon in the header bar).
The annotation must be a single-line comment on the line immediately before the method definition.
Syntax:
// @menuItem { "path": "Tools/Category/Action Name", "priority": 10 }
async myMethod() {
// ...
}
The JSON payload must be valid JSON with the following fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string |
Yes | — | Slash-separated menu path. The last segment is the display label. |
priority |
number |
No | 0 |
Sorting order. Higher values appear first in the menu. |
Path Hierarchy¶
The path field defines where the action appears in the menu hierarchy:
"Tools/Action"— appears directly in the dropdown as "Action""Tools/Category/Action"— appears under a "Category" submenu"Tools/Category/SubCategory/Action"— nested submenu under "Category > SubCategory"
The leading Tools/ segment is stripped automatically (the puzzle icon already represents "Tools"). So "Tools/Test/Log Info" becomes Test > Log Info in the menu.
If you omit the Tools/ prefix (e.g. "MyCategory/Action"), it still works — MyCategory becomes a top-level submenu.
Priority¶
Items with higher priority values appear at the top of their group. Items with equal priority are shown in the order they were registered.
// @menuItem { "path": "Tools/Important Action", "priority": 100 }
async importantAction() { /* shown first */ }
// @menuItem { "path": "Tools/Less Important", "priority": 10 }
async lessImportant() { /* shown second */ }
// @menuItem { "path": "Tools/Default Priority" }
async defaultPriority() { /* shown last (priority 0) */ }
Logging API¶
Balancy.Editor.Log¶
Log messages appear in the Extensions Log panel (accessible from the puzzle icon dropdown).
| Method | Description |
|---|---|
Balancy.Editor.Log.info(message, data?) |
Informational message (blue badge) |
Balancy.Editor.Log.warn(message, data?) |
Warning message (yellow badge) |
Balancy.Editor.Log.error(message, data?) |
Error message (red badge) |
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
message |
string |
Yes | The log message text. Non-strings are coerced via String(). |
data |
any |
No | Additional data displayed as expandable JSON in the log panel. Must be serializable (no Promises, Functions, DOM elements, etc.). |
Example:
Balancy.Editor.Log.info("Processing started");
Balancy.Editor.Log.warn("Item count is low", { count: 2, threshold: 10 });
Balancy.Editor.Log.error("Failed to update", { unnyId: "40071", reason: "not found" });
Always await async results before logging
The data parameter is sent via postMessage and must be structured-clone-compatible. Do not pass Promises, Functions, or unresolved async results.
// WRONG — logs a Promise object, causes postMessage error
const result = Balancy.CMS.getModelByUnnyId("40071");
Balancy.Editor.Log.info("result", result);
// CORRECT
const result = await Balancy.CMS.getModelByUnnyId("40071");
Balancy.Editor.Log.info("result", result);
CMS API¶
The Balancy.CMS namespace provides methods to read and write CMS documents. All methods are asynchronous and return Promises — always use await.
Expanded Model Format¶
The CMS API uses an "expanded model" JSON format designed to be human-friendly:
$unnyId(string) — The document's user-visible ID (e.g."40071"). Read-only in updates.$type(string) — The template name (e.g."SmartObjects.Item"). Required for creates, optional for updates (used for validation).- Field names match template parameter names exactly (e.g.
Name,Level,Condition). - Primitive fields — stored as their native JS types:
number,string,boolean. - Component fields — expanded inline as nested objects with their own
$unnyIdand$type. - Document references — stored as GUID strings (not expanded).
- Lists of components — arrays of expanded component objects.
- Lists of primitives — arrays of native values.
Example expanded model:
{
"$unnyId": "40071",
"$type": "SmartObjects.GameEvent",
"Name": "Daily Login",
"Priority": 5,
"IsActive": true,
"Condition": {
"$unnyId": "40072",
"$type": "Conditions.LevelCondition",
"MinLevel": 10,
"MaxLevel": 50
},
"Rewards": [
{
"$unnyId": "40073",
"$type": "SmartObjects.Reward",
"Amount": 100,
"ItemId": "abc-def-123"
}
]
}
Reading Data¶
getModelByUnnyId¶
Retrieve a single document by its ID, fully expanded.
const model = await Balancy.CMS.getModelByUnnyId(unnyId);
| Parameter | Type | Description |
|---|---|---|
unnyId |
string |
The document's user-visible ID (e.g. "40071") |
Returns: object | null — The expanded model, or null if no entity with that ID exists.
Example:
const item = await Balancy.CMS.getModelByUnnyId("40071");
if (item == null) {
Balancy.Editor.Log.warn("Item not found");
return;
}
Balancy.Editor.Log.info("Item name: " + item.Name);
getModels¶
Retrieve all documents of a given template type, fully expanded.
const models = await Balancy.CMS.getModels(templateName, includeChildren?);
| Parameter | Type | Default | Description |
|---|---|---|---|
templateName |
string |
— | Full template name (e.g. "SmartObjects.Item") |
includeChildren |
boolean |
true |
If true, also returns entities of templates that inherit from the named template |
Returns: object[] — Array of expanded models. Empty array if the template doesn't exist or has no entities.
Example:
// Get all items (including child template entities)
const allItems = await Balancy.CMS.getModels("SmartObjects.Item");
// Get only exact-type items (no child templates)
const exactItems = await Balancy.CMS.getModels("SmartObjects.Item", false);
Balancy.Editor.Log.info("Found " + allItems.length + " items");
getTemplateList¶
Retrieve template descriptors (schema metadata), optionally filtered by name. Useful for discovering available templates, their fields, and inheritance hierarchies.
const templates = await Balancy.CMS.getTemplateList(templateName?);
| Parameter | Type | Default | Description |
|---|---|---|---|
templateName |
string |
— | Optional. Filter to this template and all templates that inherit from it. Omit for all templates. |
Returns: Array<TemplateDescriptor> — Array of template descriptors. Empty array if the template name doesn't exist.
Each template descriptor contains:
{
"name": "SmartObjects.Item",
"displayName": "Item",
"description": "A game item",
"templateType": "Document",
"parentTemplate": "SmartObjects.BaseItem",
"fields": [
{
"name": "Name",
"type": "String",
"required": true,
"description": null
},
{
"name": "Rewards",
"type": "List",
"subType": "Component",
"refTemplate": "SmartObjects.Reward",
"required": false,
"description": "Rewards given on use"
}
]
}
Template descriptor fields
| Field | Type | Description |
|---|---|---|
name |
string |
Full template name (e.g. "SmartObjects.Item") |
displayName |
string |
Human-readable display name |
description |
string \| null |
Template description |
templateType |
string |
One of: "Document", "Component", "Singleton", etc. |
parentTemplate |
string \| null |
Parent template name (if using inheritance), or null |
fields |
FieldDescriptor[] |
Array of field descriptors |
Field descriptor fields:
| Field | Type | Description |
|---|---|---|
name |
string |
Parameter name |
type |
string |
Data type (e.g. "Integer", "String", "LocalizedString", "List", "EntityRef") |
subType |
string \| undefined |
For List types, the element type (e.g. "Component", "Primitive") |
refTemplate |
string \| undefined |
For EntityRef and List of components, the referenced template name |
required |
boolean |
Whether the field is required |
description |
string \| null |
Field description |
Example:
// Get all templates
const all = await Balancy.CMS.getTemplateList();
Balancy.Editor.Log.info("Total templates: " + all.length);
// Get a specific template and its children
const items = await Balancy.CMS.getTemplateList("SmartObjects.Item");
for (const tmpl of items) {
Balancy.Editor.Log.info(tmpl.name + " has " + tmpl.fields.length + " fields");
}
// Discover fields of a template
const [tmpl] = await Balancy.CMS.getTemplateList("SmartObjects.Item");
if (tmpl) {
const fieldNames = tmpl.fields.map(f => f.name + " (" + f.type + ")");
Balancy.Editor.Log.info("Fields: " + fieldNames.join(", "));
}
Writing Data¶
All write operations are reflected in the editor immediately — a subsequent getModelByUnnyId returns the updated values. Changes are undoable via Ctrl+Z.
createModel¶
Create a new document from expanded JSON.
const created = await Balancy.CMS.createModel(expandedJson);
| Parameter | Type | Description |
|---|---|---|
expandedJson |
object |
Must contain $type with the template name. Other fields are optional. |
Returns: object | null — The created expanded model with an auto-assigned $unnyId.
Throws:
- If
$typeis missing or not a string - If the template name in
$typedoesn't exist - If a non-object is passed (with a hint if you accidentally pass a JSON string)
Example:
const newItem = await Balancy.CMS.createModel({
$type: "SmartObjects.Item",
Name: "New Sword",
Price: 100,
IsActive: true,
});
Balancy.Editor.Log.info("Created item with ID: " + newItem.$unnyId);
updateModel¶
Update a single document. Only fields present in the JSON are updated (partial update). Fields not included are left unchanged.
const updated = await Balancy.CMS.updateModel(unnyId, expandedJson);
| Parameter | Type | Description |
|---|---|---|
unnyId |
string |
The document's user-visible ID (e.g. "40071") |
expandedJson |
object |
Partial expanded JSON. Should contain $type for validation. Only included fields are updated. |
Returns: object | null — The updated expanded model, or null if the entity doesn't exist.
Throws:
- If
$typeis provided and doesn't match the entity's actual template - If
expandedJsonis not an object - If
unnyIdis not a string
Example — update primitive fields:
const updated = await Balancy.CMS.updateModel("40071", {
$type: "SmartObjects.Item",
Name: "Updated Name",
Price: 200,
});
Balancy.Editor.Log.info("Updated:", updated);
Example — update a component field:
const updated = await Balancy.CMS.updateModel("40071", {
$type: "SmartObjects.GameEvent",
Condition: {
$type: "Conditions.LevelCondition",
MinLevel: 20, // only MinLevel is updated
},
});
Example — update a list of components:
// Read the current document
const doc = await Balancy.CMS.getModelByUnnyId("40071");
// Modify the list (e.g., change a field, remove an element)
doc.Rewards[0].Amount = 999;
doc.Rewards.splice(2, 1); // remove 3rd element
// Write back the full list
const updated = await Balancy.CMS.updateModel(doc.$unnyId, {
$type: doc.$type,
Rewards: doc.Rewards,
});
How component lists work internally
- Existing components are matched by
$type— if a component with the same$typealready exists, it is updated in place. - New components (with a
$typenot found in the existing list) are created automatically. - Components removed from the array are dropped from the list (orphaned entities are cleaned up on deploy).
- The order of the array is preserved.
updateModels¶
Batch-update multiple documents in a single undo/redo group. One Ctrl+Z undoes all changes.
const results = await Balancy.CMS.updateModels(updates);
| Parameter | Type | Description |
|---|---|---|
updates |
Array<{ unnyId: string, expandedJson: object }> |
Array of update objects |
Returns: Array<object | null> — Array of updated expanded models (one per input). null for entities that don't exist.
Example:
const items = await Balancy.CMS.getModels("SmartObjects.Item");
if (items.length < 2) return;
const results = await Balancy.CMS.updateModels([
{
unnyId: items[0].$unnyId,
expandedJson: { $type: items[0].$type, Name: "Batch A" },
},
{
unnyId: items[1].$unnyId,
expandedJson: { $type: items[1].$type, Name: "Batch B" },
},
]);
Balancy.Editor.Log.info("Updated " + results.length + " items");
Localization API¶
The Balancy.Localization namespace provides methods to manage localization groups, keys, and translations. All methods are asynchronous — always use await.
Key Format: "GROUP/KEY"¶
All methods that identify a localization string use the "GROUP/KEY" format:
"UI/welcome_title"— keywelcome_titlein groupUI"DEFAULT/my_key"— keymy_keyin theDEFAULTgroup
Always specify the group. Bare keys without a group are not accepted.
In CMS write operations (createModel, updateModel), use the "GROUP/KEY" format directly for any LocalizedString field. The system resolves the key to its internal GUID and auto-creates the group and key if they don't exist.
Reading Localization Data¶
getGroups¶
Get all localization groups.
const groups = await Balancy.Localization.getGroups();
// [{ guid: "abc-123", path: "UI" }, { guid: "def-456", path: "DEFAULT" }, ...]
Returns: Array<{ guid: string, path: string }>
getStrings¶
Get all localization strings, optionally filtered by group.
// All strings
const all = await Balancy.Localization.getStrings();
// Strings in a specific group
const uiStrings = await Balancy.Localization.getStrings("UI");
| Parameter | Type | Required | Description |
|---|---|---|---|
group |
string |
No | Group path to filter by (e.g. "UI"). Omit for all strings. |
Returns: Array<{ guid, key, group, description, translations, export }>
Each string object:
{
"guid": "abc-123",
"key": "welcome_title",
"group": "UI",
"description": "Title shown on the welcome screen",
"translations": { "en": "Welcome!", "fr": "Bienvenue!" },
"export": true
}
getStringByKey¶
Get a single localization string by "GROUP/KEY".
const str = await Balancy.Localization.getStringByKey("UI/welcome_title");
if (str != null) {
Balancy.Editor.Log.info("English: " + str.translations.en);
}
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | "GROUP/KEY" format (e.g. "UI/welcome_title") |
Returns: { guid, key, group, description, translations, export } | null
getLanguages¶
Get all languages configured in the project.
const langs = await Balancy.Localization.getLanguages();
// [{ guid: "...", code: "en", name: "English", localizedName: "English", enabled: true }, ...]
Returns: Array<{ guid, code, name, localizedName, enabled }>
Managing Groups¶
createGroup¶
Create a new localization group.
const group = await Balancy.Localization.createGroup("Game/Messages");
Balancy.Editor.Log.info("Created group: " + group.path);
| Parameter | Type | Required | Description |
|---|---|---|---|
path |
string |
Yes | Group path (e.g. "UI", "Game/Messages") |
Returns: { guid, path }
Throws: If a group with the same path already exists.
deleteGroup¶
Delete a localization group. The group must have no strings — delete all strings first.
await Balancy.Localization.deleteGroup("Game/Messages");
| Parameter | Type | Required | Description |
|---|---|---|---|
path |
string |
Yes | Group path to delete |
Returns: { success: true }
Throws: If the group has strings (delete them first) or if the group doesn't exist.
Managing Strings¶
createString¶
Create a new localization string. Auto-creates the group if it doesn't exist.
const str = await Balancy.Localization.createString({
key: "welcome_title",
group: "UI",
translations: { en: "Welcome!", fr: "Bienvenue!" },
description: "Welcome screen title",
});
Balancy.Editor.Log.info("Created: " + str.group + "/" + str.key);
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Key name (without group prefix), e.g. "welcome_title" |
group |
string |
Yes | Group path, e.g. "UI" |
translations |
Record<string, string> |
No | Language translations, e.g. { en: "Hello", fr: "Bonjour" } |
description |
string |
No | Description of the string |
Returns: { guid, key, group, description, translations, export }
Throws: If the key already exists in the group.
updateString¶
Update a localization string's translations and/or metadata.
// Update translations
await Balancy.Localization.updateString({
key: "UI/welcome_title",
translations: { en: "Hello!", de: "Hallo!" },
});
// Rename and move
await Balancy.Localization.updateString({
key: "UI/welcome_title",
newKey: "greeting_title",
newGroup: "Game",
});
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | "GROUP/KEY" to identify the string, e.g. "UI/welcome_title" |
translations |
Record<string, string> |
No | Language translations to update (partial — only listed languages are changed) |
description |
string |
No | New description |
newKey |
string |
No | Rename the key |
newGroup |
string |
No | Move to a different group (path, auto-created if missing) |
Returns: { guid, key, group, description, translations, export }
Throws: If the key is not found. If a language code in translations doesn't exist.
deleteString¶
Delete a localization string by "GROUP/KEY".
await Balancy.Localization.deleteString("UI/welcome_title");
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | "GROUP/KEY" format, e.g. "UI/welcome_title" |
Returns: { success: true }
Throws: If the key is not found.
Using Localization in CMS Operations¶
When creating or updating documents via Balancy.CMS, use "GROUP/KEY" format for any LocalizedString field. The system resolves the key to its internal GUID — and auto-creates the group and key if they don't exist.
const item = await Balancy.CMS.createModel({
$type: "SmartObjects.Item",
Name: "UI/item_sword_name",
Description: "UI/item_sword_desc",
});
Strings must contain a / separator. Raw GUIDs are not accepted and will throw an error.
Reading: LocalizedString fields are returned in "GROUP/KEY" format (e.g. "UI/welcome_title"). Use Balancy.Localization.getStringByKey() to retrieve the full translation details including all languages.
Data Types Reference¶
How each Balancy data type is represented in the expanded model:
| Data Type | JS Type | Notes |
|---|---|---|
| Integer | number |
|
| Float | number |
|
| Long | number |
|
| String | string |
|
| Boolean | boolean |
|
| Enum | number |
Enum integer value |
| Duration | number |
Seconds |
| Date | string |
ISO date string |
| Color | string |
Hex color string |
| Localized String | string \| null |
Read: "GROUP/KEY" format. Write: "GROUP/KEY" (auto-resolves/creates). |
| Entity Ref (Component) | object |
Expanded inline as nested object with $unnyId, $type |
| Entity Ref (Document) | string |
Entity GUID string |
| Template | string \| null |
Template name (e.g. "SmartObjects.Item") |
| TemplateParam | string \| null |
Parameter name |
| List (Primitive) | array |
Array of primitive values |
| List (Component) | array |
Array of expanded component objects |
| List (Document Ref) | array |
Array of entity GUID strings |
| List (TemplateParam) | string |
Dot-notation path (e.g. "GeneralInfo.Level") |
| DataObject | string |
GUID |
| Product | string |
GUID |
| BaseData | string |
GUID |
Error Handling¶
All CMS API errors are propagated as rejected Promises. If an error occurs inside a @menuItem method, it is automatically caught and logged to the Extensions Log as an error entry.
// @menuItem { "path": "Tools/Safe Action", "priority": 10 }
async safeAction() {
try {
const result = await Balancy.CMS.updateModel("99999", {
$type: "SmartObjects.Item",
Name: "Test",
});
if (result == null) {
Balancy.Editor.Log.warn("Entity not found");
return;
}
Balancy.Editor.Log.info("Updated!", result);
} catch (e) {
Balancy.Editor.Log.error("Update failed: " + e.message);
}
}
Common errors:
| Error | Cause |
|---|---|
"$type mismatch: expected X, got Y" |
The $type in your JSON doesn't match the entity's actual template |
"Unknown template: X" |
The template name in $type doesn't exist in the game |
"createEntity requires $type in expandedJson" |
Missing $type field in createModel |
"expected unnyId to be a string, got ..." |
Wrong argument type passed to a method |
"expected an object with $type, got string. Hint: ..." |
Passed a JSON string instead of a JS object |
Limitations¶
- Execution environment: Scripts run in a Web Worker. No DOM access (
document,window), nofetch, nolocalStorage. - No imports: Scripts cannot import external modules. All code must be self-contained.
- Serialization: All data passed between the extension and the editor goes through
postMessage(structured clone). Promises, Functions, DOM elements, and circular references cannot be transferred. - Single class per script: While multiple classes can technically exist, each script should define one extension class for clarity.
- Save hooks:
preSaveandpostSaveare defined but not yet wired. They will be implemented in a future release. - Hot reload: Automatic reload on script change is not yet implemented. Use "Reload Extensions" from the menu.
- Localized strings: Read operations return the
"GROUP/KEY"format. Write operations accept"GROUP/KEY"strings only (auto-resolved/created; raw GUIDs are not accepted). UseBalancy.Localizationfor full CRUD on localization data.
Examples¶
Batch Rename Items¶
class BatchRenameExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("BatchRenameExtension ready");
}
// @menuItem { "path": "Tools/Batch/Add Prefix to All Items", "priority": 50 }
async addPrefix() {
const items = await Balancy.CMS.getModels("SmartObjects.Item");
if (items.length === 0) {
Balancy.Editor.Log.warn("No items found");
return;
}
const updates = items.map(function(item) {
return {
unnyId: item.$unnyId,
expandedJson: {
$type: item.$type,
Name: "[SALE] " + item.Name,
},
};
});
const results = await Balancy.CMS.updateModels(updates);
Balancy.Editor.Log.info("Renamed " + results.length + " items");
}
}
Create Item with Components¶
class ItemFactoryExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("ItemFactoryExtension ready");
}
// @menuItem { "path": "Tools/Factory/Create Test Item", "priority": 30 }
async createTestItem() {
const created = await Balancy.CMS.createModel({
$type: "SmartObjects.Item",
Name: "Auto-Created Item",
Price: 42,
IsActive: true,
});
if (created == null) {
Balancy.Editor.Log.error("Failed to create item");
return;
}
Balancy.Editor.Log.info("Created: " + created.$unnyId, created);
}
}
Inspect and Modify a Document¶
class InspectorExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("InspectorExtension ready");
}
// @menuItem { "path": "Tools/Inspector/Log First Item", "priority": 20 }
async logFirstItem() {
const items = await Balancy.CMS.getModels("SmartObjects.Item");
if (items.length === 0) {
Balancy.Editor.Log.warn("No items");
return;
}
Balancy.Editor.Log.info("First item:", items[0]);
}
// @menuItem { "path": "Tools/Inspector/Double First Item Price", "priority": 19 }
async doublePrice() {
const items = await Balancy.CMS.getModels("SmartObjects.Item");
if (items.length === 0) {
Balancy.Editor.Log.warn("No items");
return;
}
const item = items[0];
const currentPrice = item.Price || 0;
const updated = await Balancy.CMS.updateModel(item.$unnyId, {
$type: item.$type,
Price: currentPrice * 2,
});
Balancy.Editor.Log.info(
"Price changed: " + currentPrice + " -> " + updated.Price,
updated
);
}
}
Working with Component Lists¶
class ComponentListExtension extends Balancy.Editor.BaseExtension {
async onInit() {
Balancy.Editor.Log.info("ComponentListExtension ready");
}
// @menuItem { "path": "Tools/Components/Modify List", "priority": 15 }
async modifyComponentList() {
const docs = await Balancy.CMS.getModels("MyTemplate");
if (docs.length === 0) return;
const doc = docs[0];
Balancy.Editor.Log.info("Before:", doc);
// Modify a component field in the list
if (doc.MyComponents && doc.MyComponents.length > 0) {
doc.MyComponents[0].SomeField = 999;
}
// Remove the last component from the list
if (doc.MyComponents && doc.MyComponents.length > 1) {
doc.MyComponents.pop();
}
// Write back
const updated = await Balancy.CMS.updateModel(doc.$unnyId, {
$type: doc.$type,
MyComponents: doc.MyComponents,
});
Balancy.Editor.Log.info("After:", updated);
}
}