diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index b5103ce062df..aa3029274d77 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -21,16 +21,18 @@ import { SharedMatrix } from "@fluidframework/matrix/internal"; import { SharedString } from "@fluidframework/sequence/internal"; import type { ISharedObject, IChannelView } from "@fluidframework/shared-object-base/internal"; import type { ITreeInternal } from "@fluidframework/tree/internal"; -import { SharedTree } from "@fluidframework/tree/internal"; +import { FieldKind, SharedTree } from "@fluidframework/tree/internal"; import { EditType } from "../CommonInterfaces.js"; import type { VisualizeChildData, VisualizeSharedObject } from "./DataVisualization.js"; import { + concatenateTypes, determineNodeKind, toVisualTree, - visualizeSharedTreeNodeBySchema, + visualizeSharedTreeBySchema, } from "./SharedTreeVisualizer.js"; +import type { VisualSharedTreeNode } from "./VisualSharedTreeTypes.js"; import { type FluidObjectNode, type FluidObjectTreeNode, @@ -250,6 +252,8 @@ export const visualizeSharedString: VisualizeSharedObject = async ( /** * {@link VisualizeSharedObject} for {@link ITree}. + * + * TODO: [ADO 31468] Refactor the SharedTreeVisualizer to conform to SharedTree API */ export const visualizeSharedTree: VisualizeSharedObject = async ( sharedObject: ISharedObject, @@ -259,19 +263,47 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Root node of the SharedTree's content. const treeView = sharedTree.exportVerbose(); - // TODO: this visualizer doesn't consider the root as a field, and thus does not display the allowed types or handle when it is empty. - // Tracked by https://dev.azure.com/fluidframework/internal/_workitems/edit/26472. + // All schema definitions for the SharedTree. + const treeSimpleSchema = sharedTree.exportSimpleSchema(); + const treeDefinitions = treeSimpleSchema.definitions; + + /** + * {@link visualizeSharedTreeBySchema} passes `allowedTypes` into co-recursive functions while constructing the visual representation. + * Since the {@link SimpleTreeSchema.allowedTypes} of each children node is only accessible at the parent field level, + * each node's allowed types are computed at the parent field level. + */ + const allowedTypes = treeSimpleSchema.allowedTypes; + const isRequired = treeSimpleSchema.kind === FieldKind.Required; + if (treeView === undefined) { - throw new Error("Support for visualizing empty trees is not implemented"); + return { + fluidObjectId: sharedTree.id, + typeMetadata: "SharedTree", + nodeKind: VisualNodeKind.FluidTreeNode, + tooltipContents: { + schema: { + nodeKind: VisualNodeKind.TreeNode, + children: { + allowedTypes: { + nodeKind: VisualNodeKind.ValueNode, + value: concatenateTypes(allowedTypes), + }, + isRequired: { + nodeKind: VisualNodeKind.ValueNode, + value: isRequired.toString(), + }, + }, + }, + }, + children: {}, + }; } - // Schema of the tree node. - const treeSchema = sharedTree.exportSimpleSchema(); - - // Traverses the SharedTree and generates a visual representation of the tree and its schema. - const visualTreeRepresentation = await visualizeSharedTreeNodeBySchema( + // Create a root field visualization that shows the allowed types at the root + const visualTreeRepresentation: VisualSharedTreeNode = await visualizeSharedTreeBySchema( treeView, - treeSchema, + treeDefinitions, + { allowedTypes, isRequired }, visualizeChildData, ); diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index bcfae4c84904..106b556b7206 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. */ +import { FieldKind } from "@fluidframework/tree"; import type { + SimpleArrayNodeSchema, SimpleMapNodeSchema, + SimpleNodeSchema, SimpleObjectNodeSchema, - SimpleTreeSchema, VerboseTree, VerboseTreeNode, } from "@fluidframework/tree/internal"; @@ -84,6 +86,12 @@ function createToolTipContents(schema: SharedTreeSchemaNode): VisualTreeNode { if (schema.allowedTypes !== undefined) { children.allowedTypes = createAllowedTypesVisualTree(schema.allowedTypes); } + if (schema.isRequired !== undefined) { + children.isRequired = { + nodeKind: VisualNodeKind.ValueNode, + value: schema.isRequired, + }; + } return { nodeKind: VisualNodeKind.TreeNode, children, @@ -91,7 +99,7 @@ function createToolTipContents(schema: SharedTreeSchemaNode): VisualTreeNode { } /** - * Converts the visual representation from {@link visualizeSharedTreeNodeBySchema} to a visual tree compatible with the devtools-view. + * Converts the visual representation from {@link visualizeNodeBySchema} to a visual tree compatible with the devtools-view. * @param tree - the visual representation of the SharedTree. * @returns - the visual representation of type {@link VisualChildNode} */ @@ -148,42 +156,53 @@ export function toVisualTree(tree: VisualSharedTreeNode): VisualChildNode { } /** - * Concatenrate allowed types for `ObjectNodeStoredSchema` and `MapNodeStoredSchema`. + * Concatenate allowed types for `ObjectNodeStoredSchema` and `MapNodeStoredSchema`. */ -function concatenateTypes(fieldTypes: ReadonlySet): string { +export function concatenateTypes(fieldTypes: ReadonlySet): string { return [...fieldTypes].join(" | "); } /** - * Returns the allowed fields & types for the object fields (e.g., `foo : string | number, bar: boolean`) + * Properties that describe schema constraints for a field in the tree */ -function getObjectAllowedTypes(schema: SimpleObjectNodeSchema): string { - const result: string[] = []; - - for (const [fieldKey, treeFieldSimpleSchema] of Object.entries(schema.fields)) { - const fieldTypes = treeFieldSimpleSchema.allowedTypes; - result.push(`${fieldKey} : ${concatenateTypes(fieldTypes)}`); - } +interface FieldSchemaProperties { + allowedTypes: ReadonlySet | undefined; - return `{ ${result.join(", ")} }`; + /** + * Whether the field is required (true) or optional (false). + * + * `undefined` indicates that the field is implicitly required. + * In this case, no requirement information will be displayed by the devtools. + */ + isRequired: boolean | undefined; } /** - * Returns the schema & fields of the node. + * Processes and visualizes the fields of a verbose tree node. + * + * @param treeFields - The fields of the tree node to visualize. Can be either an array of VerboseTree (for array nodes) or a Record of field names to VerboseTree (for object/map nodes). + * @param treeDefinitions - Map containing all schema definitions for the entire tree structure. Each definition describes the shape and constraints of a particular node type. + * @param fieldSchemaProperties - Record mapping field names to their schema properties, including allowed types and whether they are required. + * @param visualizeChildData - Callback function to visualize child node data. + * + * @returns A record mapping field names/indices to their visual tree representations. */ async function visualizeVerboseNodeFields( - tree: VerboseTreeNode, - treeSchema: SimpleTreeSchema, + treeFields: readonly VerboseTree[] | Record, + treeDefinitions: ReadonlyMap, + fieldSchemaProperties: Record, visualizeChildData: VisualizeChildData, ): Promise> { - const treeFields = tree.fields; - const fields: Record = {}; for (const [fieldKey, childField] of Object.entries(treeFields)) { - fields[fieldKey] = await visualizeSharedTreeNodeBySchema( + fields[fieldKey] = await visualizeSharedTreeBySchema( childField, - treeSchema, + treeDefinitions, + { + allowedTypes: fieldSchemaProperties[fieldKey]?.allowedTypes, + isRequired: fieldSchemaProperties[fieldKey]?.isRequired, + }, visualizeChildData, ); } @@ -196,16 +215,31 @@ async function visualizeVerboseNodeFields( */ async function visualizeObjectNode( tree: VerboseTreeNode, - nodeSchema: SimpleObjectNodeSchema, - treeSchema: SimpleTreeSchema, + schema: SimpleObjectNodeSchema, + treeDefinitions: ReadonlyMap, + { allowedTypes, isRequired }: FieldSchemaProperties, visualizeChildData: VisualizeChildData, ): Promise { + const objectNodeSchemaProperties: Record = {}; + for (const [fieldKey, treeFieldSimpleSchema] of Object.entries(schema.fields)) { + objectNodeSchemaProperties[fieldKey] = { + allowedTypes: treeFieldSimpleSchema.allowedTypes, + isRequired: treeFieldSimpleSchema.kind === FieldKind.Required ? true : false, + }; + } + return { schema: { schemaName: tree.type, - allowedTypes: getObjectAllowedTypes(nodeSchema), + allowedTypes: concatenateTypes(allowedTypes ?? new Set()), + isRequired: isRequired?.toString(), }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields( + tree.fields, + treeDefinitions, + objectNodeSchemaProperties, + visualizeChildData, + ), kind: VisualSharedTreeNodeKind.InternalNode, }; } @@ -215,45 +249,95 @@ async function visualizeObjectNode( */ async function visualizeMapNode( tree: VerboseTreeNode, - nodeSchema: SimpleMapNodeSchema, - treeSchema: SimpleTreeSchema, + schema: SimpleMapNodeSchema, + treeDefinitions: ReadonlyMap, + { allowedTypes, isRequired }: FieldSchemaProperties, + visualizeChildData: VisualizeChildData, +): Promise { + const mapNodeSchemaProperties: Record = {}; + for (const key of Object.keys(tree.fields)) { + mapNodeSchemaProperties[key] = { + allowedTypes: schema.allowedTypes, + // Map values are always required. Don't display field requirement information, since that information is redundant. + isRequired: undefined, + }; + } + + return { + schema: { + schemaName: tree.type, + allowedTypes: concatenateTypes(allowedTypes ?? new Set()), + isRequired: isRequired?.toString(), + }, + fields: await visualizeVerboseNodeFields( + tree.fields, + treeDefinitions, + mapNodeSchemaProperties, + visualizeChildData, + ), + kind: VisualSharedTreeNodeKind.InternalNode, + }; +} + +/** + * Returns the schema & fields of the node with type {@link ArrayNodeStoredSchema}. + */ +async function visualizeArrayNode( + tree: VerboseTreeNode, + schema: SimpleArrayNodeSchema, + treeDefinitions: ReadonlyMap, + { allowedTypes, isRequired }: FieldSchemaProperties, visualizeChildData: VisualizeChildData, ): Promise { + const children = tree.fields; + if (!Array.isArray(children)) { + throw new TypeError("Invalid array"); + } + + const arrayNodeSchemaProperties: Record = {}; + for (const [i] of children.entries()) { + arrayNodeSchemaProperties[i] = { + allowedTypes: schema.allowedTypes, + // Array values are always required. Don't display field requirement information, since that information is redundant. + isRequired: undefined, + }; + } + return { schema: { schemaName: tree.type, - allowedTypes: `Record`, + allowedTypes: concatenateTypes(allowedTypes ?? new Set()), + isRequired: isRequired?.toString(), }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields( + tree.fields, + treeDefinitions, + arrayNodeSchemaProperties, + visualizeChildData, + ), kind: VisualSharedTreeNodeKind.InternalNode, }; } /** - * Main recursive helper function to create the visual representation of the SharedTree. - * Processes tree nodes based on their schema type (e.g., ObjectNodeStoredSchema, MapNodeStoredSchema, LeafNodeStoredSchema), producing the visual representation for each type. + * Creates the visual representation of non-leaf SharedTree nodes. + * + * @remarks + * Processes internal tree nodes based on their schema type (e.g., ObjectNodeStoredSchema, MapNodeStoredSchema, ArrayNodeStoredSchema), + * producing the visual representation for each type. * * @see {@link https://fluidframework.com/docs/data-structures/tree/} for more information on the SharedTree schema. * * @remarks */ -export async function visualizeSharedTreeNodeBySchema( - tree: VerboseTree, - treeSchema: SimpleTreeSchema, +async function visualizeNodeBySchema( + tree: VerboseTreeNode, + treeDefinitions: ReadonlyMap, + { allowedTypes, isRequired }: FieldSchemaProperties, visualizeChildData: VisualizeChildData, ): Promise { - if (Tree.is(tree, SchemaFactory.leaves)) { - const nodeSchema = Tree.schema(tree); - return { - schema: { - schemaName: nodeSchema.identifier, - }, - value: await visualizeChildData(tree), - kind: VisualSharedTreeNodeKind.LeafNode, - }; - } + const schema = treeDefinitions.get(tree.type); - const schema = treeSchema.definitions.get(tree.type); if (schema === undefined) { throw new TypeError("Unrecognized schema type."); } @@ -263,41 +347,76 @@ export async function visualizeSharedTreeNodeBySchema( const objectVisualized = visualizeObjectNode( tree, schema, - treeSchema, + treeDefinitions, + { allowedTypes, isRequired }, visualizeChildData, ); return objectVisualized; } case NodeKind.Map: { - const mapVisualized = visualizeMapNode(tree, schema, treeSchema, visualizeChildData); + const mapVisualized = visualizeMapNode( + tree, + schema, + treeDefinitions, + { allowedTypes, isRequired }, + visualizeChildData, + ); return mapVisualized; } case NodeKind.Array: { - const fields: Record = {}; - const children = tree.fields; - if (!Array.isArray(children)) { - throw new TypeError("Invalid array"); - } - - for (const [i, child] of children.entries()) { - fields[i] = await visualizeSharedTreeNodeBySchema( - child, - treeSchema, - visualizeChildData, - ); - } - - return { - schema: { - schemaName: tree.type, - allowedTypes: concatenateTypes(schema.allowedTypes), - }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), - kind: VisualSharedTreeNodeKind.InternalNode, - }; + return visualizeArrayNode( + tree, + schema, + treeDefinitions, + { allowedTypes, isRequired }, + visualizeChildData, + ); } default: { throw new TypeError("Unrecognized schema type."); } } } + +/** + * Creates a visual representation of a SharedTree based on its schema. + * @param tree - The {@link VerboseTree} to visualize + * @param treeDefinitions - Map containing all schema definitions for the entire tree structure. Each definition + * describes the shape and constraints of a particular node type. + * @param fieldSchemaProperties - Properties describing schema constraints for this field: + * - `allowedTypes`: Set of type names that are valid for this specific node position in the tree. + * This is a subset of the types defined in treeDefinitions. + * - `isRequired`: Whether this field is required in its parent object schema. + * Only meaningful for direct children of object nodes. + * Undefined for array/map elements since they are always required within their parent. + * @param visualizeChildData - Callback function to visualize child node data + * @returns A visual representation of the tree that includes schema information and node values + * + * @remarks + * This function handles both leaf nodes (primitive values, handles) and internal nodes (objects, maps, arrays). + * For leaf nodes, it creates a visual representation with the node's schema and value. + * For internal nodes, it recursively processes the node's fields using {@link visualizeNodeBySchema}. + */ +export async function visualizeSharedTreeBySchema( + tree: VerboseTree, + treeDefinitions: ReadonlyMap, + { allowedTypes, isRequired }: FieldSchemaProperties, + visualizeChildData: VisualizeChildData, +): Promise { + return Tree.is(tree, SchemaFactory.leaves) + ? { + schema: { + schemaName: Tree.schema(tree).identifier, + allowedTypes: concatenateTypes(allowedTypes ?? new Set()), + isRequired: isRequired?.toString(), + }, + value: await visualizeChildData(tree), + kind: VisualSharedTreeNodeKind.LeafNode, + } + : visualizeNodeBySchema( + tree, + treeDefinitions, + { allowedTypes, isRequired }, + visualizeChildData, + ); +} diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/VisualSharedTreeTypes.ts b/packages/tools/devtools/devtools-core/src/data-visualization/VisualSharedTreeTypes.ts index 8cfd3cd7106e..bfc13db93548 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/VisualSharedTreeTypes.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/VisualSharedTreeTypes.ts @@ -27,13 +27,21 @@ export interface SharedTreeSchemaNode { /** * Name of the SharedTree schema. */ - schemaName: string; + readonly schemaName: string; /** * Types allowed (e.g., string, number, boolean, handle & etc.) inside the node. * - InternalNode: `Record`. */ - allowedTypes?: string | Record; + readonly allowedTypes: string | Record; + + /** + * If the field is required or optional. + * - When {@link FieldKind.Required}: The field must be present + * - When {@link FieldKind.Optional}: The field may be omitted + * - When undefined: Treated the same as {@link FieldKind.Optional} + */ + readonly isRequired?: string; } /** diff --git a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts index bd922daf590e..9c71fa5beadc 100644 --- a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts +++ b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts @@ -394,7 +394,9 @@ describe("DefaultVisualizers unit tests", () => { "test", ); - const view = sharedTree.viewWith(new TreeViewConfiguration({ schema: builder.number })); + const view = sharedTree.viewWith( + new TreeViewConfiguration({ schema: [builder.number, builder.string] }), + ); view.initialize(0); const result = await visualizeSharedTree( @@ -413,6 +415,14 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.number", }, + allowedTypes: { + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -423,7 +433,7 @@ describe("DefaultVisualizers unit tests", () => { expect(result).to.deep.equal(expected); }); - it("SharedTree: Array of Leaves", async () => { + it("SharedTree: Array", async () => { const factory = SharedTree.getFactory(); const builder = new SchemaFactory("shared-tree-test"); @@ -433,13 +443,13 @@ describe("DefaultVisualizers unit tests", () => { ); class RootNodeSchema extends builder.object("root-item", { - foo: builder.array([builder.number, builder.string]), + foo: builder.optional(builder.array([builder.number, builder.string])), }) {} const view = sharedTree.viewWith(new TreeViewConfiguration({ schema: RootNodeSchema })); view.initialize( new RootNodeSchema({ - foo: [0, 1, 2, 3, "hello", "world"], + foo: [1, "hello world"], }), ); @@ -453,21 +463,6 @@ describe("DefaultVisualizers unit tests", () => { foo: { children: { "0": { - value: 0, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - "1": { value: 1, nodeKind: "ValueNode", tooltipContents: { @@ -478,42 +473,16 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.number", }, - }, - }, - }, - }, - "2": { - value: 2, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - "3": { - value: 3, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { + allowedTypes: { + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", }, }, }, }, }, - "4": { - value: "hello", + "1": { + value: "hello world", nodeKind: "ValueNode", tooltipContents: { schema: { @@ -523,20 +492,9 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.string", }, - }, - }, - }, - }, - "5": { - value: "world", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { + allowedTypes: { + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", }, }, }, @@ -554,8 +512,13 @@ describe("DefaultVisualizers unit tests", () => { 'shared-tree-test.Array<["com.fluidframework.leaf.number","com.fluidframework.leaf.string"]>', }, allowedTypes: { - value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", + value: + 'shared-tree-test.Array<["com.fluidframework.leaf.number","com.fluidframework.leaf.string"]>', + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "false", }, }, }, @@ -572,9 +535,12 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.root-item", }, allowedTypes: { - value: - '{ foo : shared-tree-test.Array<["com.fluidframework.leaf.number","com.fluidframework.leaf.string"]> }', + value: "shared-tree-test.root-item", + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, @@ -582,7 +548,6 @@ describe("DefaultVisualizers unit tests", () => { fluidObjectId: "test", typeMetadata: "SharedTree", }; - expect(result).to.deep.equal(expected); }); @@ -596,7 +561,7 @@ describe("DefaultVisualizers unit tests", () => { ); class RootNodeSchema extends builder.object("root-item", { - foo: builder.map([builder.string, builder.number, builder.handle]), + foo: builder.map([builder.number, builder.handle]), }) {} const view = sharedTree.viewWith(new TreeViewConfiguration({ schema: RootNodeSchema })); @@ -605,7 +570,6 @@ describe("DefaultVisualizers unit tests", () => { foo: new Map([ ["apple", 1], ["banana", 2], - ["cherry", 3], ]), }), ); @@ -630,6 +594,10 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.number", }, + allowedTypes: { + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.handle", + nodeKind: "ValueNode", + }, }, }, }, @@ -645,20 +613,9 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.number", }, - }, - }, - }, - }, - cherry: { - value: 3, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { + allowedTypes: { + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.handle", nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", }, }, }, @@ -673,13 +630,17 @@ describe("DefaultVisualizers unit tests", () => { name: { nodeKind: "ValueNode", value: - 'shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number","com.fluidframework.leaf.string"]>', + 'shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number"]>', }, allowedTypes: { value: - "Record", + 'shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number"]>', nodeKind: "ValueNode", }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -695,9 +656,12 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.root-item", }, allowedTypes: { - value: - '{ foo : shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number","com.fluidframework.leaf.string"]> }', + value: "shared-tree-test.root-item", + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, @@ -705,7 +669,6 @@ describe("DefaultVisualizers unit tests", () => { fluidObjectId: "test", typeMetadata: "SharedTree", }; - expect(result).to.deep.equal(expected); }); @@ -720,7 +683,7 @@ describe("DefaultVisualizers unit tests", () => { class RootNodeSchema extends builder.object("root-item", { foo: builder.object("bar-item", { - apple: builder.boolean, + apple: [builder.boolean, builder.string], banana: builder.string, }), }) {} @@ -755,6 +718,15 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.boolean", }, + allowedTypes: { + value: + "com.fluidframework.leaf.boolean | com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -770,6 +742,14 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.string", }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -785,10 +765,13 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.bar-item", }, allowedTypes: { - value: - "{ apple : com.fluidframework.leaf.boolean, banana : com.fluidframework.leaf.string }", + value: "shared-tree-test.bar-item", nodeKind: "ValueNode", }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -804,8 +787,12 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.root-item", }, allowedTypes: { - value: "{ foo : shared-tree-test.bar-item }", + value: "shared-tree-test.root-item", + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, @@ -813,7 +800,6 @@ describe("DefaultVisualizers unit tests", () => { fluidObjectId: "test", typeMetadata: "SharedTree", }; - expect(result).to.deep.equal(expected); }); @@ -849,12 +835,19 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.handle", }, + allowedTypes: { + value: "com.fluidframework.leaf.handle", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, typeMetadata: "SharedTree", }; - expect(result).to.deep.equal(expected); }); @@ -906,6 +899,14 @@ describe("DefaultVisualizers unit tests", () => { nodeKind: "ValueNode", value: "com.fluidframework.leaf.handle", }, + allowedTypes: { + value: "com.fluidframework.leaf.handle", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -921,9 +922,13 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.bar-item", }, allowedTypes: { - value: "{ apple : com.fluidframework.leaf.handle }", + value: "shared-tree-test.bar-item", nodeKind: "ValueNode", }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, }, }, }, @@ -939,8 +944,12 @@ describe("DefaultVisualizers unit tests", () => { value: "shared-tree-test.root-item", }, allowedTypes: { - value: "{ foo : shared-tree-test.bar-item }", + value: "shared-tree-test.root-item", + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, @@ -948,11 +957,10 @@ describe("DefaultVisualizers unit tests", () => { fluidObjectId: "test", typeMetadata: "SharedTree", }; - expect(result).to.deep.equal(expected); }); - it("SharedTree: Complex Nesting", async () => { + it("SharedTree: Array and Map in Object Node", async () => { const factory = SharedTree.getFactory(); const builder = new SchemaFactory("shared-tree-test"); @@ -961,90 +969,44 @@ describe("DefaultVisualizers unit tests", () => { "test", ); - class BroccoliSchema extends builder.object("broccoli-object-schema", { - alpaca: builder.string, - }) {} - - class AppleSchema extends builder.object("apple-object-schema", { - avocado: [builder.number, builder.string], - broccoli: builder.array(BroccoliSchema), + class WorkItem extends builder.object("work-item", { + title: builder.string, + completed: builder.boolean, + dueDate: builder.string, + assignee: builder.string, + collaborators: builder.optional(builder.array(builder.string)), }) {} - class FooSchema extends builder.object("foo-item", { - apple: builder.array(AppleSchema), - banana: builder.object("banana-object", { - miniBanana: [builder.boolean, builder.string, builder.number], + class TodoWorkspace extends builder.object("todo-workspace", { + categories: builder.object("todo-categories", { + work: [builder.map([WorkItem]), builder.array(WorkItem)], }), - cherry: builder.optional(builder.number), }) {} - class RootNodeSchema extends builder.object("root-item", { - foo: builder.array(FooSchema), - bar: builder.object("bar-item", { - americano: builder.boolean, - bubbleTea: builder.string, - chaiLatte: builder.object("chai-latte-object", { - appleCider: [builder.boolean, builder.string, builder.handle], - }), - dalgona: builder.array( - builder.object("dalgona-object", { - avengers: builder.boolean, - }), - ), - espresso: builder.array([builder.number, builder.string]), + const view = sharedTree.viewWith( + new TreeViewConfiguration({ + schema: builder.optional(TodoWorkspace), }), - baz: [builder.number, builder.string, builder.boolean], - foobar: builder.map([ - builder.string, - builder.number, - builder.handle, - builder.object("map-object", { acorn: builder.boolean }), - ]), - }) {} - - const view = sharedTree.viewWith(new TreeViewConfiguration({ schema: RootNodeSchema })); + ); view.initialize( - new RootNodeSchema({ - foo: [ - { - apple: [{ avocado: 16, broccoli: [{ alpaca: "Llama but cuter." }] }], - banana: { - miniBanana: true, - }, - cherry: 32, - }, - { - apple: [ - { - avocado: "Avacado Advocate.", - broccoli: [{ alpaca: "Llama but not LLM." }], - }, - ], - banana: { - miniBanana: false, + new TodoWorkspace({ + categories: { + work: [ + { + title: "Submit a PR", + completed: false, + dueDate: "2026-01-01", + assignee: "Alice", + collaborators: ["Bob", "Charlie"], }, - cherry: undefined, - }, - ], - bar: { - americano: false, - bubbleTea: "Taro Bubble Tea", - chaiLatte: { - appleCider: true, - }, - dalgona: [ { - avengers: true, + title: "Review a PR", + completed: true, + dueDate: "2025-01-01", + assignee: "David", }, ], - espresso: [256, "FiveHundredTwelve"], }, - baz: 128, - foobar: new Map([ - ["anthropology", 1], - ["biology", 2], - ["choreography", 3], - ]), }), ); @@ -1055,16 +1017,108 @@ describe("DefaultVisualizers unit tests", () => { const expected = { children: { - foo: { + categories: { children: { - "0": { + work: { children: { - apple: { + "0": { children: { - "0": { + title: { + value: "Submit a PR", + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, + }, + }, + }, + }, + completed: { + value: false, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.boolean", + }, + allowedTypes: { + value: "com.fluidframework.leaf.boolean", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, + }, + }, + }, + }, + dueDate: { + value: "2026-01-01", + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, + }, + }, + }, + }, + assignee: { + value: "Alice", + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", + }, + }, + }, + }, + }, + collaborators: { children: { - avocado: { - value: 16, + "0": { + value: "Bob", nodeKind: "ValueNode", tooltipContents: { schema: { @@ -1072,62 +1126,29 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", + value: "com.fluidframework.leaf.string", }, - }, - }, - }, - }, - broccoli: { - children: { - "0": { - children: { - alpaca: { - value: "Llama but cuter.", + allowedTypes: { + value: "com.fluidframework.leaf.string", nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.broccoli-object-schema", - }, - allowedTypes: { - value: "{ alpaca : com.fluidframework.leaf.string }", - nodeKind: "ValueNode", - }, - }, }, }, }, }, - nodeKind: "TreeNode", + }, + "1": { + value: "Charlie", + nodeKind: "ValueNode", tooltipContents: { schema: { nodeKind: "TreeNode", children: { name: { nodeKind: "ValueNode", - value: - 'shared-tree-test.Array<["shared-tree-test.broccoli-object-schema"]>', + value: "com.fluidframework.leaf.string", }, allowedTypes: { - value: "shared-tree-test.broccoli-object-schema", + value: "com.fluidframework.leaf.string", nodeKind: "ValueNode", }, }, @@ -1142,13 +1163,18 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "shared-tree-test.apple-object-schema", + value: + 'shared-tree-test.Array<["com.fluidframework.leaf.string"]>', }, allowedTypes: { value: - '{ avocado : com.fluidframework.leaf.number | com.fluidframework.leaf.string, broccoli : shared-tree-test.Array<["shared-tree-test.broccoli-object-schema"]> }', + 'shared-tree-test.Array<["com.fluidframework.leaf.string"]>', nodeKind: "ValueNode", }, + isRequired: { + nodeKind: "ValueNode", + value: "false", + }, }, }, }, @@ -1161,21 +1187,20 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: - 'shared-tree-test.Array<["shared-tree-test.apple-object-schema"]>', + value: "shared-tree-test.work-item", }, allowedTypes: { - value: "shared-tree-test.apple-object-schema", + value: "shared-tree-test.work-item", nodeKind: "ValueNode", }, }, }, }, }, - banana: { + "1": { children: { - miniBanana: { - value: true, + title: { + value: "Review a PR", nodeKind: "ValueNode", tooltipContents: { schema: { @@ -1183,184 +1208,45 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", }, }, }, }, }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.banana-object", - }, - allowedTypes: { - value: - "{ miniBanana : com.fluidframework.leaf.boolean | com.fluidframework.leaf.string | com.fluidframework.leaf.number }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - cherry: { - value: 32, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.foo-item", - }, - allowedTypes: { - value: - '{ apple : shared-tree-test.Array<["shared-tree-test.apple-object-schema"]>, banana : shared-tree-test.banana-object, cherry : com.fluidframework.leaf.number }', + completed: { + value: true, nodeKind: "ValueNode", - }, - }, - }, - }, - }, - "1": { - children: { - apple: { - children: { - "0": { - children: { - avocado: { - value: "Avacado Advocate.", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - broccoli: { - children: { - "0": { - children: { - alpaca: { - value: "Llama but not LLM.", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.broccoli-object-schema", - }, - allowedTypes: { - value: "{ alpaca : com.fluidframework.leaf.string }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: - 'shared-tree-test.Array<["shared-tree-test.broccoli-object-schema"]>', - }, - allowedTypes: { - value: "shared-tree-test.broccoli-object-schema", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", tooltipContents: { schema: { nodeKind: "TreeNode", children: { name: { nodeKind: "ValueNode", - value: "shared-tree-test.apple-object-schema", + value: "com.fluidframework.leaf.boolean", }, allowedTypes: { - value: - '{ avocado : com.fluidframework.leaf.number | com.fluidframework.leaf.string, broccoli : shared-tree-test.Array<["shared-tree-test.broccoli-object-schema"]> }', + value: "com.fluidframework.leaf.boolean", + nodeKind: "ValueNode", + }, + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, }, }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: - 'shared-tree-test.Array<["shared-tree-test.apple-object-schema"]>', - }, - allowedTypes: { - value: "shared-tree-test.apple-object-schema", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - banana: { - children: { - miniBanana: { - value: false, + dueDate: { + value: "2025-01-01", nodeKind: "ValueNode", tooltipContents: { schema: { @@ -1368,142 +1254,22 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", }, }, }, }, }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.banana-object", - }, - allowedTypes: { - value: - "{ miniBanana : com.fluidframework.leaf.boolean | com.fluidframework.leaf.string | com.fluidframework.leaf.number }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.foo-item", - }, - allowedTypes: { - value: - '{ apple : shared-tree-test.Array<["shared-tree-test.apple-object-schema"]>, banana : shared-tree-test.banana-object, cherry : com.fluidframework.leaf.number }', - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: 'shared-tree-test.Array<["shared-tree-test.foo-item"]>', - }, - allowedTypes: { - value: "shared-tree-test.foo-item", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - bar: { - children: { - americano: { - value: false, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", - }, - }, - }, - }, - }, - bubbleTea: { - value: "Taro Bubble Tea", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - chaiLatte: { - children: { - appleCider: { - value: true, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.chai-latte-object", - }, - allowedTypes: { - value: - "{ appleCider : com.fluidframework.leaf.boolean | com.fluidframework.leaf.string | com.fluidframework.leaf.handle }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - dalgona: { - children: { - "0": { - children: { - avengers: { - value: true, + assignee: { + value: "David", nodeKind: "ValueNode", tooltipContents: { schema: { @@ -1511,7 +1277,15 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", + value: "com.fluidframework.leaf.string", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string", + nodeKind: "ValueNode", + }, + isRequired: { + nodeKind: "ValueNode", + value: "true", }, }, }, @@ -1525,10 +1299,10 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "shared-tree-test.dalgona-object", + value: "shared-tree-test.work-item", }, allowedTypes: { - value: "{ avengers : com.fluidframework.leaf.boolean }", + value: "shared-tree-test.work-item", nodeKind: "ValueNode", }, }, @@ -1543,62 +1317,16 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: 'shared-tree-test.Array<["shared-tree-test.dalgona-object"]>', + value: 'shared-tree-test.Array<["shared-tree-test.work-item"]>', }, allowedTypes: { - value: "shared-tree-test.dalgona-object", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - espresso: { - children: { - "0": { - value: 256, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - "1": { - value: "FiveHundredTwelve", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", value: - 'shared-tree-test.Array<["com.fluidframework.leaf.number","com.fluidframework.leaf.string"]>', + 'shared-tree-test.Map<["shared-tree-test.work-item"]> | shared-tree-test.Array<["shared-tree-test.work-item"]>', + nodeKind: "ValueNode", }, - allowedTypes: { - value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", + isRequired: { nodeKind: "ValueNode", + value: "true", }, }, }, @@ -1612,119 +1340,89 @@ describe("DefaultVisualizers unit tests", () => { children: { name: { nodeKind: "ValueNode", - value: "shared-tree-test.bar-item", + value: "shared-tree-test.todo-categories", }, allowedTypes: { - value: - '{ americano : com.fluidframework.leaf.boolean, bubbleTea : com.fluidframework.leaf.string, chaiLatte : shared-tree-test.chai-latte-object, dalgona : shared-tree-test.Array<["shared-tree-test.dalgona-object"]>, espresso : shared-tree-test.Array<["com.fluidframework.leaf.number","com.fluidframework.leaf.string"]> }', + value: "shared-tree-test.todo-categories", nodeKind: "ValueNode", }, - }, - }, - }, - }, - baz: { - value: 128, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { + isRequired: { nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", + value: "true", }, }, }, }, }, - foobar: { + }, + nodeKind: "FluidTreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", children: { - anthropology: { - value: 1, + name: { nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, + value: "shared-tree-test.todo-workspace", }, - biology: { - value: 2, + allowedTypes: { + value: "shared-tree-test.todo-workspace", nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, }, - choreography: { - value: 3, + isRequired: { nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: - 'shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number","com.fluidframework.leaf.string","shared-tree-test.map-object"]>', - }, - allowedTypes: { - value: - "Record", - nodeKind: "ValueNode", - }, - }, + value: "false", }, }, }, }, - nodeKind: "FluidTreeNode", + fluidObjectId: "test", + typeMetadata: "SharedTree", + }; + + expect(result).to.deep.equal(expected); + }); + + it("SharedTree: Empty Root", async () => { + const factory = SharedTree.getFactory(); + const builder = new SchemaFactory("shared-tree-test"); + + const sharedTree = factory.create( + new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }), + "test", + ); + + const view = sharedTree.viewWith( + new TreeViewConfiguration({ + schema: builder.optional([builder.number, builder.string]), + }), + ); + view.initialize(undefined); + + const result = await visualizeSharedTree( + sharedTree as unknown as ISharedObject, + visualizeChildData, + ); + + const expected = { + fluidObjectId: sharedTree.id, + typeMetadata: "SharedTree", + nodeKind: VisualNodeKind.FluidTreeNode, tooltipContents: { schema: { - nodeKind: "TreeNode", + nodeKind: VisualNodeKind.TreeNode, children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.root-item", - }, allowedTypes: { - value: - '{ foo : shared-tree-test.Array<["shared-tree-test.foo-item"]>, bar : shared-tree-test.bar-item, baz : com.fluidframework.leaf.number | com.fluidframework.leaf.string | com.fluidframework.leaf.boolean, foobar : shared-tree-test.Map<["com.fluidframework.leaf.handle","com.fluidframework.leaf.number","com.fluidframework.leaf.string","shared-tree-test.map-object"]> }', - nodeKind: "ValueNode", + nodeKind: VisualNodeKind.ValueNode, + value: "com.fluidframework.leaf.number | com.fluidframework.leaf.string", + }, + isRequired: { + nodeKind: VisualNodeKind.ValueNode, + value: "false", }, }, }, }, - fluidObjectId: "test", - typeMetadata: "SharedTree", + children: {}, }; expect(result).to.deep.equal(expected); diff --git a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts index d498309e0a12..f72b8e89a448 100644 --- a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts +++ b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts @@ -175,43 +175,75 @@ export class AppData extends DataObject { } private populateSharedTree(sharedTree: ITree): void { - // Set up SharedTree for visualization - const builder = new SchemaFactory("DefaultVisualizer_SharedTree_Test"); - - // TODO: Maybe include example handle - - class LeafSchema extends builder.object("leaf-item", { - leafField: [builder.boolean, builder.handle, builder.string], + const builder = new SchemaFactory("TodoList_Schema"); + + class WorkItem extends builder.object("work-item", { + title: builder.string, + completed: builder.boolean, + dueDate: builder.string, + assignee: builder.string, + collaborators: builder.optional(builder.array(builder.string)), }) {} - class ChildSchema extends builder.object("child-item", { - childField: [builder.string, builder.boolean], - childData: builder.optional(LeafSchema), + class PersonalItem extends builder.object("personal-item", { + title: builder.string, + completed: builder.boolean, + dueDate: builder.string, + location: builder.optional(builder.string), + with: builder.optional(builder.array(builder.string)), }) {} - class RootNodeSchema extends builder.object("root-item", { - childrenOne: builder.array(ChildSchema), - childrenTwo: builder.number, + class TodoWorkspace extends builder.object("todo-workspace", { + categories: builder.object("todo-categories", { + work: [builder.map([WorkItem]), builder.array(WorkItem)], + personal: [builder.map([PersonalItem]), builder.array(PersonalItem)], + }), }) {} - const config = new TreeViewConfiguration({ schema: RootNodeSchema }); + const config = new TreeViewConfiguration({ + schema: [TodoWorkspace], + }); + const view = sharedTree.viewWith(config); - view.initialize({ - childrenOne: [ - { - childField: "Hello world!", - childData: { - leafField: "Hello world again!", - }, - }, - { - childField: true, - childData: { - leafField: false, - }, + view.initialize( + new TodoWorkspace({ + categories: { + work: [ + { + title: "Submit a PR", + completed: false, + dueDate: "2026-01-01", + assignee: "Alice", + collaborators: ["Bob", "Charlie"], + }, + { + title: "Review a PR", + completed: true, + dueDate: "2025-01-01", + assignee: "David", + }, + ], + personal: new Map([ + [ + "Health", + { + title: "Go to the gym", + completed: true, + dueDate: "2025-01-01", + with: ["Wayne", "Tyler"], + }, + ], + [ + "Education", + { + title: "Finish reading the book", + completed: false, + dueDate: "2026-01-01", + }, + ], + ]), }, - ], - childrenTwo: 32, - }); + }), + ); } }