The agent built every part again that already stood in a graph, node by node, while the editor has Ctrl+C and Ctrl+V for it. The new command `copy_nodes` in the area graphs copies nodes into the same graph or another one, in one undo step with the rest of the call. - The links between the copied nodes come along, and so do groups and comments that are only about copied nodes. A node belongs to a group by its middle, as when it is dragged. `boxesAbout` in annotationFollow.ts decides this. - `made` names each copy by the id of its original, so later steps link to it with `$copy.<id>`. `$copy` alone is the first one. - Without x and y the copies are arranged at the end of the call like new nodes. With them they keep their layout, top left corner there. - Nodes the target graph does not take are refused by name, not left out as Ctrl+V does. The rule is the graph tab's: `fitsGraph` and `fittingPart` moved from clipboard.ts to paletteEntries.ts. - A copied Variables node takes the variables of its behavior along into the target behavior (`CarryVariablesCommand`), with default, value, Keep and Private. A variable of that name of another kind there is refused. - The relay's instructions name `copy_nodes`. Found on the way: - Groups made by the agent can take in a node of another group once the nodes are arranged. lint_graph does not count that. - Undo after a paste leaves an empty `groups: []`, as AddAnnotationCommand does. It is the same as no list. Tests: - agent-copy (new, 3 tests) with the agent, relay, clipboard and palette tests: 118/118. - Counter-probes: 6 bendings, all compile, all red. - e2e 71 (atx): ALL PASSED, 26 checks. - Gate (atx): CHECK EXIT 0, 327 files, 5024 tests. - Live in the MCP Test Project: 5 nodes of Arrow keys walk into a new behavior, 6 links, group, comment and speed (300) along, lint_graph 0 everywhere, taken back with undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
550 lines
20 KiB
TypeScript
550 lines
20 KiB
TypeScript
import {
|
||
type FieldValue,
|
||
isStruct,
|
||
type PropertyBag,
|
||
type PropertyValue,
|
||
type StructValue,
|
||
withFieldRenamed,
|
||
withoutField,
|
||
} from '../model/Properties';
|
||
import {
|
||
type BehaviorAsset,
|
||
isKeptVariable,
|
||
isPrivateVariable,
|
||
setKeptVariable,
|
||
setPrivateVariable,
|
||
type VariableOwnerAsset,
|
||
} from '../model/Asset';
|
||
import { overrideSites } from '../model/instance';
|
||
import type { Project } from '../model/Project';
|
||
import type { VariableKind } from '../properties/kinds';
|
||
import {
|
||
emptyFieldValue,
|
||
isValidFieldName,
|
||
isValidVariableName,
|
||
type PropertyDef,
|
||
type StructFieldDef,
|
||
variableDef,
|
||
} from '../properties/PropertySchema';
|
||
import { valueAt, withValueAt } from '../properties/valuePath';
|
||
import { moveItem, type Removed, removeFirst, restoreAt } from '../util/list';
|
||
import { type Command, CommandError, MoveCommand, RenameCommand } from './Command';
|
||
import { renameVariablePorts } from './portCommands';
|
||
import { requireBehavior, requireVariableOwner } from './require';
|
||
|
||
/**
|
||
* The variable's default value is written to `props` as well. On a behavior a new variable starts
|
||
* at its default every call; "Keep" is switched on by hand (#229).
|
||
*/
|
||
export class AddVariableCommand implements Command {
|
||
readonly label: string;
|
||
readonly def: PropertyDef;
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
key: string,
|
||
kind: VariableKind,
|
||
) {
|
||
this.def = variableDef(key, kind);
|
||
this.label = `Add variable ${key}`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireVariableOwner(project, this.assetId);
|
||
if (!isValidName(asset, this.def.key)) throw new CommandError(INVALID_NAME);
|
||
if (asset.variables.some((v) => v.key === this.def.key))
|
||
throw new CommandError(`Variable "${this.def.key}" already exists`);
|
||
asset.variables.push(this.def);
|
||
asset.props[this.def.key] = this.def.default;
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const asset = requireVariableOwner(project, this.assetId);
|
||
asset.variables = asset.variables.filter((v) => v.key !== this.def.key);
|
||
delete asset.props[this.def.key];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Declares variables of one behavior in another as they are declared there (#434): default, Keep
|
||
* and Private included, for copied nodes that name them. A key the target has already is left alone.
|
||
*/
|
||
export class CarryVariablesCommand implements Command {
|
||
readonly label = 'Carry variables';
|
||
private added: string[] = [];
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly fromId: string,
|
||
private readonly keys: readonly string[],
|
||
) {}
|
||
|
||
execute(project: Project): void {
|
||
const from = requireBehavior(project, this.fromId);
|
||
const to = requireBehavior(project, this.assetId);
|
||
this.added = [];
|
||
for (const key of this.keys) {
|
||
const def = from.variables.find((v) => v.key === key);
|
||
if (!def) throw new CommandError(`${from.name} has no variable "${key}"`);
|
||
if (to.variables.some((v) => v.key === key)) continue;
|
||
to.variables.push(structuredClone(def));
|
||
to.props[key] = structuredClone(from.props[key] ?? def.default);
|
||
if (isKeptVariable(from, key)) setKeptVariable(to, key, true);
|
||
if (isPrivateVariable(from, key)) setPrivateVariable(to, key, true);
|
||
this.added.push(key);
|
||
}
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const to = requireBehavior(project, this.assetId);
|
||
const added = new Set(this.added);
|
||
to.variables = to.variables.filter((v) => !added.has(v.key));
|
||
for (const key of added) {
|
||
delete to.props[key];
|
||
setKeptVariable(to, key, false);
|
||
setPrivateVariable(to, key, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** One placed object's own value for a variable, remembered so that undo can put it back. */
|
||
interface TakenOverride {
|
||
holderId: string;
|
||
objectId: string;
|
||
path: string;
|
||
value: PropertyValue;
|
||
}
|
||
|
||
function takeOverrides(project: Project, assetId: string, key: string): TakenOverride[] {
|
||
const taken: TakenOverride[] = [];
|
||
for (const site of overrideSites(project, (id) => id === assetId)) {
|
||
const value = site.bag[key];
|
||
if (value === undefined) continue;
|
||
taken.push({ holderId: site.holder.id, objectId: site.object.id, path: site.path, value });
|
||
delete site.bag[key];
|
||
}
|
||
return taken;
|
||
}
|
||
|
||
function restoreOverrides(
|
||
project: Project,
|
||
assetId: string,
|
||
key: string,
|
||
taken: readonly TakenOverride[],
|
||
): void {
|
||
const sites = overrideSites(project, (id) => id === assetId);
|
||
for (const o of taken) {
|
||
const site = sites.find(
|
||
(s) => s.holder.id === o.holderId && s.object.id === o.objectId && s.path === o.path,
|
||
);
|
||
if (site) site.bag[key] = o.value;
|
||
}
|
||
}
|
||
|
||
/** Removes a variable together with its default and every instance override. */
|
||
export class RemoveVariableCommand implements Command {
|
||
readonly label: string;
|
||
private removed?: {
|
||
def: Removed<PropertyDef>;
|
||
value: PropertyValue | undefined;
|
||
kept: boolean;
|
||
hidden: boolean;
|
||
};
|
||
private overrides: TakenOverride[] = [];
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
) {
|
||
this.label = `Remove variable ${key}`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireVariableOwner(project, this.assetId);
|
||
const def = removeFirst(asset.variables, (v) => v.key === this.key);
|
||
if (!def) throw new CommandError(`Variable "${this.key}" not found`);
|
||
const kept = asset.type === 'behavior' && isKeptVariable(asset, this.key);
|
||
const hidden = isPrivateVariable(asset, this.key);
|
||
this.removed = { def, value: asset.props[this.key], kept, hidden };
|
||
delete asset.props[this.key];
|
||
if (asset.type === 'behavior') {
|
||
setKeptVariable(asset, this.key, false);
|
||
setPrivateVariable(asset, this.key, false);
|
||
}
|
||
this.overrides = takeOverrides(project, this.assetId, this.key);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
if (!this.removed) return;
|
||
const asset = requireVariableOwner(project, this.assetId);
|
||
restoreAt(asset.variables, [this.removed.def]);
|
||
if (this.removed.value !== undefined) asset.props[this.key] = this.removed.value;
|
||
if (asset.type === 'behavior') {
|
||
if (this.removed.kept) setKeptVariable(asset, this.key, true);
|
||
if (this.removed.hidden) setPrivateVariable(asset, this.key, true);
|
||
}
|
||
restoreOverrides(project, this.assetId, this.key, this.overrides);
|
||
}
|
||
}
|
||
|
||
/** Renames everywhere: definition, default, instance overrides and the graph nodes referring to it. */
|
||
export class RenameVariableCommand extends RenameCommand {
|
||
constructor(
|
||
private readonly assetId: string,
|
||
from: string,
|
||
to: string,
|
||
) {
|
||
super(`Rename variable ${from}`, from, to);
|
||
}
|
||
|
||
protected rename(project: Project, from: string, to: string): void {
|
||
const asset = requireVariableOwner(project, this.assetId);
|
||
if (from === to) return;
|
||
if (!isValidName(asset, to)) throw new CommandError(INVALID_NAME);
|
||
const def = asset.variables.find((v) => v.key === from);
|
||
if (!def) throw new CommandError(`Variable "${from}" not found`);
|
||
if (asset.variables.some((v) => v.key === to)) throw new CommandError(`Variable "${to}" already exists`);
|
||
def.key = to;
|
||
def.label = to;
|
||
// Renamed in place, so the switches keep their spot in the list.
|
||
if (asset.type === 'behavior') {
|
||
if (asset.keptVariables) asset.keptVariables = asset.keptVariables.map((k) => (k === from ? to : k));
|
||
if (asset.privateVariables)
|
||
asset.privateVariables = asset.privateVariables.map((k) => (k === from ? to : k));
|
||
}
|
||
if (asset.props[from] !== undefined) {
|
||
asset.props[to] = asset.props[from]!;
|
||
delete asset.props[from];
|
||
}
|
||
for (const { bag } of overrideSites(project, (id) => id === this.assetId)) {
|
||
if (bag[from] === undefined) continue;
|
||
bag[to] = bag[from]!;
|
||
delete bag[from];
|
||
}
|
||
renameVariablePorts(project, this.assetId, from, to);
|
||
}
|
||
}
|
||
|
||
/** Only the order changes: values and the ports naming a variable go by its name (#310). */
|
||
export class MoveVariableCommand extends MoveCommand {
|
||
constructor(
|
||
private readonly assetId: string,
|
||
from: number,
|
||
to: number,
|
||
) {
|
||
super('Reorder variables', from, to);
|
||
}
|
||
|
||
protected move(project: Project, from: number, to: number): void {
|
||
if (!moveItem(requireVariableOwner(project, this.assetId).variables, from, to))
|
||
throw new CommandError(`There is no variable ${from + 1} to move to place ${to + 1}`);
|
||
}
|
||
}
|
||
|
||
/** The behavior, once it is known to declare `key`. */
|
||
function requireBehaviorVariable(project: Project, assetId: string, key: string): BehaviorAsset {
|
||
const asset = requireBehavior(project, assetId);
|
||
if (!asset.variables.some((v) => v.key === key)) throw new CommandError(`Variable "${key}" not found`);
|
||
return asset;
|
||
}
|
||
|
||
/** Whether a behavior's variable carries on from one `Run behavior` call to the next (#229). */
|
||
export class SetVariableKeptCommand implements Command {
|
||
readonly label: string;
|
||
private previous = false;
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
private readonly kept: boolean,
|
||
) {
|
||
this.label = kept ? `Keep ${key} between calls` : `Reset ${key} every call`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireBehaviorVariable(project, this.assetId, this.key);
|
||
this.previous = isKeptVariable(asset, this.key);
|
||
setKeptVariable(asset, this.key, this.kept);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
setKeptVariable(requireBehavior(project, this.assetId), this.key, this.previous);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether a behavior's variable is seen outside its own graph (#257). Going private drops the
|
||
* overrides of every placed object: nobody would see them any more, yet they would still act.
|
||
*/
|
||
export class SetVariablePrivateCommand implements Command {
|
||
readonly label: string;
|
||
private previous = false;
|
||
private overrides: TakenOverride[] = [];
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
private readonly hidden: boolean,
|
||
) {
|
||
this.label = hidden ? `Make ${key} private` : `Make ${key} public`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireBehaviorVariable(project, this.assetId, this.key);
|
||
this.previous = isPrivateVariable(asset, this.key);
|
||
setPrivateVariable(asset, this.key, this.hidden);
|
||
this.overrides = this.hidden ? takeOverrides(project, this.assetId, this.key) : [];
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
setPrivateVariable(requireBehavior(project, this.assetId), this.key, this.previous);
|
||
restoreOverrides(project, this.assetId, this.key, this.overrides);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A struct's fields are no properties of anything placed, so the reserved property names are free
|
||
* there – as they are for a struct field.
|
||
*/
|
||
function isValidName(asset: VariableOwnerAsset, name: string): boolean {
|
||
return asset.type === 'struct' ? isValidFieldName(name) : isValidVariableName(name);
|
||
}
|
||
|
||
const INVALID_NAME = 'Variable names must start with a letter and contain only letters, digits or _';
|
||
const INVALID_FIELD = 'Field names must start with a letter and contain only letters, digits or _';
|
||
|
||
/**
|
||
* Where declared fields live: on a struct variable, or on a struct field of one, however deep
|
||
* (#239). A list item carries its own shape instead.
|
||
*/
|
||
interface FieldOwner {
|
||
fields(): readonly StructFieldDef[];
|
||
setFields(next: readonly StructFieldDef[]): void;
|
||
}
|
||
|
||
function variableOf(project: Project, assetId: string, key: string): PropertyDef | undefined {
|
||
return requireVariableOwner(project, assetId).variables.find((v) => v.key === key);
|
||
}
|
||
|
||
/** `path` names the struct fields from the variable down to the struct whose fields are meant. */
|
||
function requireFieldOwner(
|
||
project: Project,
|
||
assetId: string,
|
||
key: string,
|
||
path: readonly string[],
|
||
): FieldOwner {
|
||
const def = variableOf(project, assetId, key);
|
||
if (def?.kind !== 'struct') throw new CommandError(`Variable "${key}" has no fields`);
|
||
if (fieldsAt(def.fields, path) === undefined)
|
||
throw new CommandError(`Field "${path.join('.')}" has no fields`);
|
||
return {
|
||
fields: () => fieldsAt(def.fields, path) ?? [],
|
||
setFields: (next) => (def.fields = withFieldsAt(def.fields, path, next)),
|
||
};
|
||
}
|
||
|
||
function fieldsAt(
|
||
fields: readonly StructFieldDef[],
|
||
path: readonly string[],
|
||
): readonly StructFieldDef[] | undefined {
|
||
if (path.length === 0) return fields;
|
||
const field = fields.find((f) => f.key === path[0]);
|
||
return field?.kind === 'struct' ? fieldsAt(field.fields, path.slice(1)) : undefined;
|
||
}
|
||
|
||
/** The declaration with the fields at `path` replaced, rebuilt on the way down. */
|
||
function withFieldsAt(
|
||
fields: readonly StructFieldDef[],
|
||
path: readonly string[],
|
||
next: readonly StructFieldDef[],
|
||
): readonly StructFieldDef[] {
|
||
if (path.length === 0) return next;
|
||
return fields.map((f) =>
|
||
f.key === path[0] && f.kind === 'struct'
|
||
? { ...f, fields: withFieldsAt(f.fields, path.slice(1), next) }
|
||
: f,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* One struct-carrying value of a variable (its declared default, the behavior's own, an override); a
|
||
* field change hits all of them at once so none keeps a stale shape. `id` pairs a site up on undo.
|
||
* With a `path` the site is the struct that deep inside the value.
|
||
*/
|
||
interface StructSite {
|
||
id: string;
|
||
read(): StructValue;
|
||
write(value: StructValue): void;
|
||
}
|
||
|
||
function structSites(project: Project, assetId: string, key: string, path: readonly string[]): StructSite[] {
|
||
const structIn = (value: PropertyValue | undefined): StructValue => {
|
||
const at = valueAt(value, path);
|
||
return isStruct(at) ? at : {};
|
||
};
|
||
return (
|
||
valueHolders(project, assetId, key)
|
||
// An override the object does not have stays absent: writing one here would invent a deviation.
|
||
.filter((h) => !h.optional || isStruct(h.read()))
|
||
.map((holder) => ({
|
||
id: holder.id,
|
||
read: () => structIn(holder.read()),
|
||
write: (value: StructValue) => holder.write(withValueAt(holder.read(), path, value) ?? value),
|
||
}))
|
||
);
|
||
}
|
||
|
||
/** Everywhere one variable's value is kept: its declared default, the behavior's own, the overrides. */
|
||
interface ValueHolder {
|
||
id: string;
|
||
/** An override may simply not be there; the declared default and the behavior's own always are. */
|
||
optional: boolean;
|
||
read(): PropertyValue | undefined;
|
||
write(value: PropertyValue): void;
|
||
}
|
||
|
||
function valueHolders(project: Project, assetId: string, key: string): ValueHolder[] {
|
||
const asset = requireVariableOwner(project, assetId);
|
||
const def = asset.variables.find((v) => v.key === key);
|
||
const holders: ValueHolder[] = [];
|
||
if (def)
|
||
holders.push({
|
||
id: 'declared',
|
||
optional: false,
|
||
read: () => def.default,
|
||
write: (value) => {
|
||
def.default = value;
|
||
},
|
||
});
|
||
holders.push(bagHolder('asset', false, asset.props, key));
|
||
for (const site of overrideSites(project, (id) => id === assetId))
|
||
holders.push(bagHolder(`${site.object.id}/${site.path}`, true, site.bag, key));
|
||
return holders;
|
||
}
|
||
|
||
function bagHolder(id: string, optional: boolean, bag: PropertyBag, key: string): ValueHolder {
|
||
return { id, optional, read: () => bag[key], write: (value: PropertyValue) => (bag[key] = value) };
|
||
}
|
||
|
||
export class AddStructFieldCommand implements Command {
|
||
readonly label: string;
|
||
|
||
/** `path` names the struct the field is added to: none for the variable itself. */
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
private readonly field: StructFieldDef,
|
||
private readonly path: readonly string[] = [],
|
||
) {
|
||
this.label = `Add field ${field.key}`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
if (!isValidFieldName(this.field.key)) throw new CommandError(INVALID_FIELD);
|
||
if (owner.fields().some((f) => f.key === this.field.key))
|
||
throw new CommandError(`Field "${this.field.key}" already exists`);
|
||
owner.setFields([...owner.fields(), { ...this.field }]);
|
||
for (const site of structSites(project, this.assetId, this.key, this.path))
|
||
site.write({ ...site.read(), [this.field.key]: emptyFieldValue(this.field) });
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
owner.setFields(owner.fields().filter((f) => f.key !== this.field.key));
|
||
for (const site of structSites(project, this.assetId, this.key, this.path))
|
||
site.write(withoutField(site.read(), this.field.key));
|
||
}
|
||
}
|
||
|
||
/** Takes the field's value out of every struct that carries it, and gives each one back on undo. */
|
||
export class RemoveStructFieldCommand implements Command {
|
||
readonly label: string;
|
||
private removed?: { field: Removed<StructFieldDef>; values: Map<string, FieldValue> };
|
||
|
||
/** `path` names the struct the field is taken from: none for the variable itself. */
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
private readonly field: string,
|
||
private readonly path: readonly string[] = [],
|
||
) {
|
||
this.label = `Remove field ${field}`;
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
const fields = [...owner.fields()];
|
||
const field = removeFirst(fields, (f) => f.key === this.field);
|
||
if (!field) throw new CommandError(`Field "${this.field}" not found`);
|
||
const values = new Map<string, FieldValue>();
|
||
for (const site of structSites(project, this.assetId, this.key, this.path)) {
|
||
const value = site.read();
|
||
const item = value[this.field];
|
||
if (item !== undefined) values.set(site.id, item);
|
||
site.write(withoutField(value, this.field));
|
||
}
|
||
owner.setFields(fields);
|
||
this.removed = { field, values };
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
if (!this.removed) return;
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
const fields = [...owner.fields()];
|
||
restoreAt(fields, [this.removed.field]);
|
||
owner.setFields(fields);
|
||
for (const site of structSites(project, this.assetId, this.key, this.path)) {
|
||
const value = this.removed.values.get(site.id);
|
||
if (value !== undefined) site.write({ ...site.read(), [this.field]: value });
|
||
}
|
||
}
|
||
}
|
||
|
||
export class RenameStructFieldCommand extends RenameCommand {
|
||
/** `path` names the struct the field belongs to: none for the variable itself. */
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
from: string,
|
||
to: string,
|
||
private readonly path: readonly string[] = [],
|
||
) {
|
||
super(`Rename field ${from}`, from, to);
|
||
}
|
||
|
||
protected rename(project: Project, from: string, to: string): void {
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
if (from === to) return;
|
||
if (!isValidFieldName(to)) throw new CommandError(INVALID_FIELD);
|
||
const fields = owner.fields();
|
||
if (!fields.some((f) => f.key === from)) throw new CommandError(`Field "${from}" not found`);
|
||
if (fields.some((f) => f.key === to)) throw new CommandError(`Field "${to}" already exists`);
|
||
owner.setFields(fields.map((f) => (f.key === from ? { ...f, key: to } : f)));
|
||
for (const site of structSites(project, this.assetId, this.key, this.path))
|
||
site.write(withFieldRenamed(site.read(), from, to));
|
||
}
|
||
}
|
||
|
||
/** A struct value is read by field name, so only the declaration changes order (#310). */
|
||
export class MoveStructFieldCommand extends MoveCommand {
|
||
/** `path` names the struct whose fields are reordered: none for the variable itself. */
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly key: string,
|
||
from: number,
|
||
to: number,
|
||
private readonly path: readonly string[] = [],
|
||
) {
|
||
super('Reorder fields', from, to);
|
||
}
|
||
|
||
protected move(project: Project, from: number, to: number): void {
|
||
const owner = requireFieldOwner(project, this.assetId, this.key, this.path);
|
||
const fields = [...owner.fields()];
|
||
if (!moveItem(fields, from, to))
|
||
throw new CommandError(`There is no field ${from + 1} to move to place ${to + 1}`);
|
||
owner.setFields(fields);
|
||
}
|
||
}
|