Skip to content

Commit 1296cd1

Browse files
authored
Expose editor for flex fields (microsoft#22170)
## Description This unifies the editing API of the flex layer by directly exposing the field editor. Sequence fields already do this, however, they expose an editor that uses cursors for new content. This updates the editor exposed by sequence fields to accept MapTrees as new content, and introduces the same pattern for value and optional fields. The flex layer now translates from the MapTrees to cursors, rather than it being done externally. This is part of a continued effort to simplify and reduce the API surface of the flex layer. This also removes the content setter from value/optional fields, since it is now redundant with the editor.
1 parent aba3e11 commit 1296cd1

File tree

12 files changed

+156
-114
lines changed

12 files changed

+156
-114
lines changed

packages/dds/tree/src/feature-libraries/default-schema/defaultEditBuilder.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,15 @@ export interface IDefaultEditBuilder {
121121
* The returned object can be used (i.e., have its methods called) multiple times but its lifetime
122122
* is bounded by the lifetime of this edit builder.
123123
*/
124-
valueField(field: FieldUpPath): ValueFieldEditBuilder;
124+
valueField(field: FieldUpPath): ValueFieldEditBuilder<ITreeCursorSynchronous>;
125125

126126
/**
127127
* @param field - the optional field which is being edited under the parent node
128128
* @returns An object with methods to edit the given field of the given parent.
129129
* The returned object can be used (i.e., have its methods called) multiple times but its lifetime
130130
* is bounded by the lifetime of this edit builder.
131131
*/
132-
optionalField(field: FieldUpPath): OptionalFieldEditBuilder;
132+
optionalField(field: FieldUpPath): OptionalFieldEditBuilder<ITreeCursorSynchronous>;
133133

134134
/**
135135
* @param field - the sequence field which is being edited under the parent node
@@ -138,7 +138,7 @@ export interface IDefaultEditBuilder {
138138
* The returned object can be used (i.e., have its methods called) multiple times but its lifetime
139139
* is bounded by the lifetime of this edit builder.
140140
*/
141-
sequenceField(field: FieldUpPath): SequenceFieldEditBuilder;
141+
sequenceField(field: FieldUpPath): SequenceFieldEditBuilder<ITreeCursorSynchronous>;
142142

143143
/**
144144
* Moves a subsequence from one sequence field to another sequence field.
@@ -183,7 +183,7 @@ export class DefaultEditBuilder implements ChangeFamilyEditor, IDefaultEditBuild
183183
this.modularBuilder.addNodeExistsConstraint(path);
184184
}
185185

186-
public valueField(field: FieldUpPath): ValueFieldEditBuilder {
186+
public valueField(field: FieldUpPath): ValueFieldEditBuilder<ITreeCursorSynchronous> {
187187
return {
188188
set: (newContent: ITreeCursorSynchronous): void => {
189189
const fillId = this.modularBuilder.generateId();
@@ -207,7 +207,7 @@ export class DefaultEditBuilder implements ChangeFamilyEditor, IDefaultEditBuild
207207
};
208208
}
209209

210-
public optionalField(field: FieldUpPath): OptionalFieldEditBuilder {
210+
public optionalField(field: FieldUpPath): OptionalFieldEditBuilder<ITreeCursorSynchronous> {
211211
return {
212212
set: (newContent: ITreeCursorSynchronous | undefined, wasEmpty: boolean): void => {
213213
const detachId = this.modularBuilder.generateId();
@@ -329,7 +329,7 @@ export class DefaultEditBuilder implements ChangeFamilyEditor, IDefaultEditBuild
329329
}
330330
}
331331

332-
public sequenceField(field: FieldUpPath): SequenceFieldEditBuilder {
332+
public sequenceField(field: FieldUpPath): SequenceFieldEditBuilder<ITreeCursorSynchronous> {
333333
return {
334334
insert: (index: number, content: ITreeCursorSynchronous): void => {
335335
const length =
@@ -386,36 +386,36 @@ export class DefaultEditBuilder implements ChangeFamilyEditor, IDefaultEditBuild
386386

387387
/**
388388
*/
389-
export interface ValueFieldEditBuilder {
389+
export interface ValueFieldEditBuilder<TContent> {
390390
/**
391391
* Issues a change which replaces the current newContent of the field with `newContent`.
392392
* @param newContent - the new content for the field.
393393
* The cursor can be in either Field or Node mode and must represent exactly one node.
394394
*/
395-
set(newContent: ITreeCursorSynchronous): void;
395+
set(newContent: TContent): void;
396396
}
397397

398398
/**
399399
*/
400-
export interface OptionalFieldEditBuilder {
400+
export interface OptionalFieldEditBuilder<TContent> {
401401
/**
402402
* Issues a change which replaces the current newContent of the field with `newContent`
403403
* @param newContent - the new content for the field.
404404
* If provided, the cursor can be in either Field or Node mode and must represent exactly one node.
405405
* @param wasEmpty - whether the field is empty when creating this change
406406
*/
407-
set(newContent: ITreeCursorSynchronous | undefined, wasEmpty: boolean): void;
407+
set(newContent: TContent | undefined, wasEmpty: boolean): void;
408408
}
409409

410410
/**
411411
*/
412-
export interface SequenceFieldEditBuilder {
412+
export interface SequenceFieldEditBuilder<TContent> {
413413
/**
414414
* Issues a change which inserts the `newContent` at the given `index`.
415415
* @param index - the index at which to insert the `newContent`.
416416
* @param newContent - the new content to be inserted in the field. Cursor can be in either Field or Node mode.
417417
*/
418-
insert(index: number, newContent: ITreeCursorSynchronous): void;
418+
insert(index: number, newContent: TContent): void;
419419

420420
/**
421421
* Issues a change which removes `count` elements starting at the given `index`.

packages/dds/tree/src/feature-libraries/flex-map-tree/mapTreeNode.ts

+19-12
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ import {
5050
} from "../typed-schema/index.js";
5151
import { type FlexImplicitAllowedTypes, normalizeAllowedTypes } from "../schemaBuilderBase.js";
5252
import type { FlexFieldKind } from "../modular-schema/index.js";
53-
import { FieldKinds, type SequenceFieldEditBuilder } from "../default-schema/index.js";
53+
import {
54+
FieldKinds,
55+
type OptionalFieldEditBuilder,
56+
type SequenceFieldEditBuilder,
57+
type ValueFieldEditBuilder,
58+
} from "../default-schema/index.js";
5459
import { UsageError } from "@fluidframework/telemetry-utils/internal";
5560

5661
// #region Nodes
@@ -431,18 +436,23 @@ class EagerMapTreeRequiredField<T extends FlexAllowedTypes>
431436
extends EagerMapTreeField<T>
432437
implements FlexTreeRequiredField<T>
433438
{
439+
public get editor(): ValueFieldEditBuilder<ExclusiveMapTree> {
440+
throw unsupportedUsageError("Setting a required field");
441+
}
442+
434443
public get content(): FlexTreeUnboxNodeUnion<T> {
435444
return unboxedUnion(this.schema, this.mapTrees[0] ?? oob(), { parent: this, index: 0 });
436445
}
437-
public set content(_: FlexTreeUnboxNodeUnion<T>) {
438-
throw unsupportedUsageError("Setting an optional field");
439-
}
440446
}
441447

442448
class EagerMapTreeOptionalField<T extends FlexAllowedTypes>
443449
extends EagerMapTreeField<T>
444450
implements FlexTreeOptionalField<T>
445451
{
452+
public get editor(): OptionalFieldEditBuilder<ExclusiveMapTree> {
453+
throw unsupportedUsageError("Setting an optional field");
454+
}
455+
446456
public get content(): FlexTreeUnboxNodeUnion<T> | undefined {
447457
return this.mapTrees.length > 0
448458
? unboxedUnion(this.schema, this.mapTrees[0] ?? oob(), {
@@ -451,15 +461,16 @@ class EagerMapTreeOptionalField<T extends FlexAllowedTypes>
451461
})
452462
: undefined;
453463
}
454-
public set content(_: FlexTreeUnboxNodeUnion<T> | undefined) {
455-
throw unsupportedUsageError("Setting an optional field");
456-
}
457464
}
458465

459466
class EagerMapTreeSequenceField<T extends FlexAllowedTypes>
460467
extends EagerMapTreeField<T>
461468
implements FlexTreeSequenceField<T>
462469
{
470+
public get editor(): SequenceFieldEditBuilder<ExclusiveMapTree[]> {
471+
throw unsupportedUsageError("Editing an array");
472+
}
473+
463474
public at(index: number): FlexTreeUnboxNodeUnion<T> | undefined {
464475
const i = indexForAt(index, this.length);
465476
if (i === undefined) {
@@ -480,12 +491,8 @@ class EagerMapTreeSequenceField<T extends FlexAllowedTypes>
480491
}
481492
}
482493

483-
public sequenceEditor(): SequenceFieldEditBuilder {
484-
throw unsupportedUsageError("Editing a sequence");
485-
}
486-
487494
public getFieldPath(): FieldUpPath {
488-
throw unsupportedUsageError("Editing a sequence");
495+
throw unsupportedUsageError("Editing an array");
489496
}
490497
}
491498

packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import {
1212
anchorSlot,
1313
} from "../../core/index.js";
1414
import type { Assume, FlattenKeys } from "../../util/index.js";
15-
import type { FieldKinds, SequenceFieldEditBuilder } from "../default-schema/index.js";
15+
import type {
16+
FieldKinds,
17+
SequenceFieldEditBuilder,
18+
ValueFieldEditBuilder,
19+
OptionalFieldEditBuilder,
20+
} from "../default-schema/index.js";
1621
import type { FlexFieldKind } from "../modular-schema/index.js";
1722
import type {
1823
Any,
@@ -686,7 +691,7 @@ export interface FlexTreeSequenceField<in out TTypes extends FlexAllowedTypes>
686691
/**
687692
* Get an editor for this sequence.
688693
*/
689-
sequenceEditor(): SequenceFieldEditBuilder;
694+
editor: SequenceFieldEditBuilder<FlexibleFieldContent>;
690695

691696
boxedIterator(): IterableIterator<FlexTreeTypedNodeUnion<TTypes>>;
692697

@@ -707,7 +712,8 @@ export interface FlexTreeSequenceField<in out TTypes extends FlexAllowedTypes>
707712
export interface FlexTreeRequiredField<in out TTypes extends FlexAllowedTypes>
708713
extends FlexTreeField {
709714
get content(): FlexTreeUnboxNodeUnion<TTypes>;
710-
set content(content: FlexibleNodeContent);
715+
716+
editor: ValueFieldEditBuilder<FlexibleNodeContent>;
711717
}
712718

713719
/**
@@ -726,7 +732,8 @@ export interface FlexTreeRequiredField<in out TTypes extends FlexAllowedTypes>
726732
export interface FlexTreeOptionalField<in out TTypes extends FlexAllowedTypes>
727733
extends FlexTreeField {
728734
get content(): FlexTreeUnboxNodeUnion<TTypes> | undefined;
729-
set content(newContent: FlexibleNodeContent | undefined);
735+
736+
editor: OptionalFieldEditBuilder<FlexibleNodeContent>;
730737
}
731738

732739
// #endregion

packages/dds/tree/src/feature-libraries/flex-tree/lazyField.ts

+40-23
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { assert } from "@fluidframework/core-utils/internal";
77

88
import {
99
CursorLocationType,
10+
type ExclusiveMapTree,
1011
type FieldAnchor,
1112
type FieldKey,
1213
type FieldUpPath,
14+
type ITreeCursorSynchronous,
1315
type ITreeSubscriptionCursor,
1416
type TreeNavigationResult,
1517
inCursorNode,
@@ -37,7 +39,6 @@ import {
3739
type FlexTreeTypedField,
3840
type FlexTreeTypedNodeUnion,
3941
type FlexTreeUnboxNodeUnion,
40-
type FlexibleNodeContent,
4142
TreeStatus,
4243
flexTreeMarker,
4344
flexTreeSlot,
@@ -54,7 +55,7 @@ import { type LazyTreeNode, makeTree } from "./lazyNode.js";
5455
import { unboxedUnion } from "./unboxed.js";
5556
import { indexForAt, treeStatusFromAnchorCache } from "./utilities.js";
5657
import { UsageError } from "@fluidframework/telemetry-utils/internal";
57-
import { cursorForMapTreeNode } from "../mapTreeCursor.js";
58+
import { cursorForMapTreeField, cursorForMapTreeNode } from "../mapTreeCursor.js";
5859

5960
/**
6061
* Reuse fields.
@@ -297,10 +298,21 @@ export class LazySequence<TTypes extends FlexAllowedTypes>
297298
return this.map((x) => x);
298299
}
299300

300-
public sequenceEditor(): SequenceFieldEditBuilder {
301+
public editor: SequenceFieldEditBuilder<ExclusiveMapTree[]> = {
302+
insert: (index, newContent) => {
303+
this.sequenceEditor().insert(index, cursorForMapTreeField(newContent));
304+
},
305+
remove: (index, count) => {
306+
this.sequenceEditor().remove(index, count);
307+
},
308+
move: (sourceIndex, count, destIndex) => {
309+
this.sequenceEditor().move(sourceIndex, count, destIndex);
310+
},
311+
};
312+
313+
private sequenceEditor(): SequenceFieldEditBuilder<ITreeCursorSynchronous> {
301314
const fieldPath = this.getFieldPathForEditing();
302-
const fieldEditor = this.context.checkout.editor.sequenceField(fieldPath);
303-
return fieldEditor;
315+
return this.context.checkout.editor.sequenceField(fieldPath);
304316
}
305317
}
306318

@@ -317,13 +329,15 @@ export class ReadonlyLazyValueField<TTypes extends FlexAllowedTypes>
317329
super(context, schema, cursor, fieldAnchor);
318330
}
319331

332+
public editor: ValueFieldEditBuilder<ExclusiveMapTree> = {
333+
set: (newContent) => {
334+
assert(false, "Unexpected set of readonly field");
335+
},
336+
};
337+
320338
public get content(): FlexTreeUnboxNodeUnion<TTypes> {
321339
return this.atIndex(0);
322340
}
323-
324-
public set content(newContent: FlexibleNodeContent) {
325-
fail("cannot set content in readonly field");
326-
}
327341
}
328342

329343
export class LazyValueField<TTypes extends FlexAllowedTypes>
@@ -339,7 +353,13 @@ export class LazyValueField<TTypes extends FlexAllowedTypes>
339353
super(context, schema, cursor, fieldAnchor);
340354
}
341355

342-
private valueFieldEditor(): ValueFieldEditBuilder {
356+
public override editor: ValueFieldEditBuilder<ExclusiveMapTree> = {
357+
set: (newContent) => {
358+
this.valueFieldEditor().set(cursorForMapTreeNode(newContent));
359+
},
360+
};
361+
362+
private valueFieldEditor(): ValueFieldEditBuilder<ITreeCursorSynchronous> {
343363
const fieldPath = this.getFieldPathForEditing();
344364
const fieldEditor = this.context.checkout.editor.valueField(fieldPath);
345365
return fieldEditor;
@@ -348,11 +368,6 @@ export class LazyValueField<TTypes extends FlexAllowedTypes>
348368
public override get content(): FlexTreeUnboxNodeUnion<TTypes> {
349369
return this.atIndex(0);
350370
}
351-
352-
public override set content(newContent: FlexibleNodeContent) {
353-
assert(newContent !== undefined, "Cannot set a required field to undefined");
354-
this.valueFieldEditor().set(cursorForMapTreeNode(newContent));
355-
}
356371
}
357372

358373
export class LazyIdentifierField<TTypes extends FlexAllowedTypes>
@@ -382,7 +397,16 @@ export class LazyOptionalField<TTypes extends FlexAllowedTypes>
382397
super(context, schema, cursor, fieldAnchor);
383398
}
384399

385-
private optionalEditor(): OptionalFieldEditBuilder {
400+
public editor: OptionalFieldEditBuilder<ExclusiveMapTree> = {
401+
set: (newContent, wasEmpty) => {
402+
this.optionalEditor().set(
403+
newContent !== undefined ? cursorForMapTreeNode(newContent) : newContent,
404+
wasEmpty,
405+
);
406+
},
407+
};
408+
409+
private optionalEditor(): OptionalFieldEditBuilder<ITreeCursorSynchronous> {
386410
const fieldPath = this.getFieldPathForEditing();
387411
const fieldEditor = this.context.checkout.editor.optionalField(fieldPath);
388412
return fieldEditor;
@@ -391,13 +415,6 @@ export class LazyOptionalField<TTypes extends FlexAllowedTypes>
391415
public get content(): FlexTreeUnboxNodeUnion<TTypes> | undefined {
392416
return this.length === 0 ? undefined : this.atIndex(0);
393417
}
394-
395-
public set content(newContent: FlexibleNodeContent | undefined) {
396-
this.optionalEditor().set(
397-
newContent !== undefined ? cursorForMapTreeNode(newContent) : undefined,
398-
this.length === 0,
399-
);
400-
}
401418
}
402419

403420
export class LazyForbiddenField<TTypes extends FlexAllowedTypes> extends LazyField<

packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ export class LazyMap<TSchema extends FlexMapNodeSchema>
338338

339339
if (fieldSchema.kind === FieldKinds.optional) {
340340
const optionalField = field as FlexTreeOptionalField<FlexAllowedTypes>;
341-
optionalField.content = content?.[0];
341+
optionalField.editor.set(content?.[0], optionalField.length === 0);
342342
} else {
343343
assert(fieldSchema.kind === FieldKinds.sequence, 0x807 /* Unexpected map field kind */);
344344

@@ -477,7 +477,7 @@ function buildStructClass<TSchema extends FlexObjectNodeSchema>(
477477
key,
478478
fieldSchema,
479479
) as FlexTreeOptionalField<FlexAllowedTypes>;
480-
field.content = newContent;
480+
field.editor.set(newContent, field.length === 0);
481481
};
482482
break;
483483
}
@@ -488,7 +488,7 @@ function buildStructClass<TSchema extends FlexObjectNodeSchema>(
488488
key,
489489
fieldSchema,
490490
) as FlexTreeRequiredField<FlexAllowedTypes>;
491-
field.content = newContent;
491+
field.editor.set(newContent);
492492
};
493493
break;
494494
}

packages/dds/tree/src/feature-libraries/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export {
5151
cursorForMapTreeField,
5252
cursorForMapTreeNode,
5353
mapTreeFromCursor,
54+
mapTreeFieldFromCursor,
5455
} from "./mapTreeCursor.js";
5556
export { MemoizedIdRangeAllocator, type IdRange } from "./memoizedIdRangeAllocator.js";
5657
export { buildForest } from "./object-forest/index.js";

packages/dds/tree/src/feature-libraries/mapTreeCursor.ts

+8
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,11 @@ export function mapTreeFromCursor(cursor: ITreeCursor): ExclusiveMapTree {
7676

7777
return node;
7878
}
79+
80+
/**
81+
* Extract an array of MapTrees (a field) from the contents of the given ITreeCursor's current field.
82+
*/
83+
export function mapTreeFieldFromCursor(cursor: ITreeCursor): ExclusiveMapTree[] {
84+
assert(cursor.mode === CursorLocationType.Fields, "must start at field");
85+
return mapCursorField(cursor, mapTreeFromCursor);
86+
}

0 commit comments

Comments
 (0)