umbraco-tree

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Umbraco Tree

Umbraco Tree

What is it?

什么是Umbraco树形结构?

A tree in Umbraco is a hierarchical structure of nodes registered in the Backoffice extension registry. Trees display organized content hierarchies and can be rendered anywhere in the Backoffice using the
<umb-tree />
element. They require a data source implementation to fetch root items, children, and ancestors.
Umbraco中的树形结构是在后台扩展注册表中注册的节点分层结构。树形结构用于展示组织化的内容层级,可使用
<umb-tree />
元素在后台的任意位置渲染。它们需要实现数据源来获取根项、子项和祖先项。

Documentation

文档说明

CRITICAL: Tree + Workspace Integration

重要提示:树形结构与工作区的集成

Trees and workspaces are tightly coupled. When using
kind: 'default'
tree items:
  1. Tree items REQUIRE workspaces - Clicking a tree item navigates to a workspace for that entity type. Without a workspace registered for the
    entityType
    , clicking causes "forever loading"
  2. Workspaces must be
    kind: 'routable'
    - For proper tree item selection state and navigation between items of the same type, use
    kind: 'routable'
    workspaces (not
    kind: 'default'
    )
  3. Entity types link trees to workspaces - The
    entityType
    in your tree item data must match the
    entityType
    in your workspace manifest
When implementing trees with clickable items, also reference the
umbraco-workspace
skill.
树形结构与工作区紧密耦合。当使用
kind: 'default'
类型的树形结构项时:
  1. 树形结构项必须关联工作区 - 点击树形结构项会导航至对应实体类型的工作区。如果未为
    entityType
    注册工作区,点击后会出现“永久加载”的问题
  2. 工作区必须设置为
    kind: 'routable'
    - 为了确保树形结构项的选中状态正常,以及同类型项之间的导航正常,请使用
    kind: 'routable'
    类型的工作区(而非
    kind: 'default'
  3. 实体类型关联树形结构与工作区 - 树形结构项数据中的
    entityType
    必须与工作区清单中的
    entityType
    匹配
当实现可点击的树形结构项时,请同时参考
umbraco-workspace
技能文档。

File Structure

文件结构

Modern trees use 2-3 files:
my-tree/
├── manifest.ts          # Registers repository and tree
├── tree.repository.ts   # Repository + inline data source
└── types.ts             # Type definitions (optional)
现代树形结构实现通常使用2-3个文件:
my-tree/
├── manifest.ts          # 注册仓库与树形结构
├── tree.repository.ts   # 仓库 + 内联数据源
└── types.ts             # 类型定义(可选)

Reference Example

参考示例

The Umbraco source includes a working example:
Location:
/Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/tree/
This example demonstrates a complete custom tree with data source, repository, and menu integration. Study this for production patterns.
Umbraco源码中包含一个可用示例:
位置
/Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/tree/
该示例展示了一个完整的自定义树形结构,包含数据源、仓库和菜单集成。请学习该示例以遵循生产环境的开发模式。

Related Foundation Skills

相关基础技能

If you need to explain these foundational concepts when implementing trees, reference these skills:
  • Repository Pattern: When implementing tree data sources, repositories, data fetching, or CRUD operations
    • Reference skill:
      umbraco-repository-pattern
  • Context API: When implementing repository contexts or explaining how repositories connect to UI components
    • Reference skill:
      umbraco-context-api
  • State Management: When implementing reactive tree updates, observables, or managing tree state
    • Reference skill:
      umbraco-state-management
如果在实现树形结构时需要解释这些基础概念,请参考以下技能文档:
  • 仓库模式:当实现树形结构数据源、仓库、数据获取或CRUD操作时
    • 参考技能:
      umbraco-repository-pattern
  • Context API:当实现仓库上下文或解释仓库如何与UI组件连接时
    • 参考技能:
      umbraco-context-api
  • 状态管理:当实现响应式树形结构更新、可观察对象或管理树形结构状态时
    • 参考技能:
      umbraco-state-management

Workflow

实现流程

  1. Fetch docs - Use WebFetch on the URLs above
  2. Ask questions - What data will the tree display? What repository will provide the data? Where will it appear? Will tree items be clickable?
  3. Generate files - Create minimal files based on latest docs
    • ✅ Create:
      manifest.ts
      ,
      tree.repository.ts
      (with inline data source)
    • ❌ Don't create:
      tree.store.ts
      (deprecated), separate
      tree.data-source.ts
      (can inline)
    • Use the inline data source pattern shown in examples below
  4. If clickable - Also create routable workspaces for each entity type (reference
    umbraco-workspace
    skill)
  5. Explain - Show what was created and how to test
  1. 获取文档 - 使用WebFetch访问上述URL
  2. 确认需求 - 树形结构将展示什么数据?由哪个仓库提供数据?它将出现在哪里?树形结构项是否可点击?
  3. 生成文件 - 根据最新文档创建最简文件
    • ✅ 创建:
      manifest.ts
      tree.repository.ts
      (包含内联数据源)
    • ❌ 无需创建:
      tree.store.ts
      (已废弃)、单独的
      tree.data-source.ts
      (可内联实现)
    • 使用如下示例所示的内联数据源模式
  4. 若支持点击 - 同时为每个实体类型创建可路由的工作区(参考
    umbraco-workspace
    技能文档)
  5. 说明实现 - 展示已创建的内容以及测试方法

Key Configuration Options

关键配置选项

hideTreeRoot on MenuItem (NOT on Tree)

在MenuItem上设置hideTreeRoot(而非在Tree上)

To show tree items at root level (without a parent folder), use
hideTreeRoot: true
on the menuItem manifest:
typescript
// CORRECT - hideTreeRoot on menuItem
{
  type: 'menuItem',
  kind: 'tree',
  alias: 'My.MenuItem.Tree',
  meta: {
    treeAlias: 'My.Tree',
    menus: ['My.Menu'],
    hideTreeRoot: true,  // Shows items at root level
  },
}

// WRONG - hideTreeRoot on tree (has no effect)
{
  type: 'tree',
  meta: {
    hideTreeRoot: true,  // This does nothing!
  },
}
若要在根级别展示树形结构项(无父文件夹),请在MenuItem清单中设置
hideTreeRoot: true
typescript
// 正确方式 - 在MenuItem上设置hideTreeRoot
{
  type: 'menuItem',
  kind: 'tree',
  alias: 'My.MenuItem.Tree',
  meta: {
    treeAlias: 'My.Tree',
    menus: ['My.Menu'],
    hideTreeRoot: true,  // 在根级别展示项
  },
}

// 错误方式 - 在Tree上设置hideTreeRoot(无效果)
{
  type: 'tree',
  meta: {
    hideTreeRoot: true,  // 此设置无效!
  },
}

Minimal Examples

最简示例

Tree Manifest

树形结构清单

typescript
export const manifests: UmbExtensionManifest[] = [
  // Repository
  {
    type: 'repository',
    alias: 'My.Tree.Repository',
    name: 'My Tree Repository',
    api: () => import('./tree.repository.js'),
  },
  // Tree
  {
    type: 'tree',
    kind: 'default',
    alias: 'My.Tree',
    name: 'My Tree',
    meta: {
      repositoryAlias: 'My.Tree.Repository',
    },
  },
  // Tree Items - use kind: 'default' when workspaces exist
  {
    type: 'treeItem',
    kind: 'default',
    alias: 'My.TreeItem',
    name: 'My Tree Item',
    forEntityTypes: ['my-entity'],
  },
  // MenuItem - hideTreeRoot here
  {
    type: 'menuItem',
    kind: 'tree',
    alias: 'My.MenuItem.Tree',
    meta: {
      treeAlias: 'My.Tree',
      menus: ['My.Menu'],
      hideTreeRoot: true,
    },
  },
];
typescript
export const manifests: UmbExtensionManifest[] = [
  // 仓库
  {
    type: 'repository',
    alias: 'My.Tree.Repository',
    name: 'My Tree Repository',
    api: () => import('./tree.repository.js'),
  },
  // 树形结构
  {
    type: 'tree',
    kind: 'default',
    alias: 'My.Tree',
    name: 'My Tree',
    meta: {
      repositoryAlias: 'My.Tree.Repository',
    },
  },
  // 树形结构项 - 当存在工作区时使用kind: 'default'
  {
    type: 'treeItem',
    kind: 'default',
    alias: 'My.TreeItem',
    name: 'My Tree Item',
    forEntityTypes: ['my-entity'],
  },
  // MenuItem - 在此处设置hideTreeRoot
  {
    type: 'menuItem',
    kind: 'tree',
    alias: 'My.MenuItem.Tree',
    meta: {
      treeAlias: 'My.Tree',
      menus: ['My.Menu'],
      hideTreeRoot: true,
    },
  },
];

Repository with Inline Data Source (tree.repository.ts)

包含内联数据源的仓库(tree.repository.ts)

Modern simplified pattern - everything in one file:
typescript
import { UmbTreeRepositoryBase, UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { MyTreeItemModel, MyTreeRootModel } from './types.js';
import { MY_ROOT_ENTITY_TYPE, MY_ENTITY_TYPE } from './entity.js';

// Data source as simple class using helper base
class MyTreeDataSource extends UmbTreeServerDataSourceBase<any, MyTreeItemModel> {
  constructor(host: UmbControllerHost) {
    super(host, {
      getRootItems: async (args) => {
        // Fetch from API or return mock data
        const items: MyTreeItemModel[] = [
          {
            unique: 'item-1',
            parent: { unique: null, entityType: MY_ROOT_ENTITY_TYPE },
            entityType: MY_ENTITY_TYPE,
            name: 'Item 1',
            hasChildren: false,
            isFolder: false,
            icon: 'icon-document',
          },
        ];
        return { data: { items, total: items.length } };
      },
      getChildrenOf: async (args) => {
        // Return children for parent
        return { data: { items: [], total: 0 } };
      },
      getAncestorsOf: async (args) => {
        // Return ancestor path
        return { data: [] };
      },
      mapper: (item: any) => item, // Identity mapper for this example
    });
  }
}

// Repository
export class MyTreeRepository
  extends UmbTreeRepositoryBase<MyTreeItemModel, MyTreeRootModel>
  implements UmbApi
{
  constructor(host: UmbControllerHost) {
    super(host, MyTreeDataSource);
  }

  async requestTreeRoot() {
    const data: MyTreeRootModel = {
      unique: null,
      entityType: MY_ROOT_ENTITY_TYPE,
      name: 'My Tree',
      hasChildren: true,
      isFolder: true,
    };
    return { data };
  }
}

export { MyTreeRepository as api };
Why this is simpler:
  • ✅ One file instead of two
  • ✅ Uses
    UmbTreeServerDataSourceBase
    helper (pass functions, not methods)
  • ✅ Data fetching logic inline (easier to understand)
  • ✅ No separate data source file to maintain
For complex trees with API calls, you can still separate into different files, but it's not required.
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
现代简化模式 - 所有内容在一个文件中:
typescript
import { UmbTreeRepositoryBase, UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { MyTreeItemModel, MyTreeRootModel } from './types.js';
import { MY_ROOT_ENTITY_TYPE, MY_ENTITY_TYPE } from './entity.js';

// Data source as simple class using helper base
class MyTreeDataSource extends UmbTreeServerDataSourceBase<any, MyTreeItemModel> {
  constructor(host: UmbControllerHost) {
    super(host, {
      getRootItems: async (args) => {
        // Fetch from API or return mock data
        const items: MyTreeItemModel[] = [
          {
            unique: 'item-1',
            parent: { unique: null, entityType: MY_ROOT_ENTITY_TYPE },
            entityType: MY_ENTITY_TYPE,
            name: 'Item 1',
            hasChildren: false,
            isFolder: false,
            icon: 'icon-document',
          },
        ];
        return { data: { items, total: items.length } };
      },
      getChildrenOf: async (args) => {
        // Return children for parent
        return { data: { items: [], total: 0 } };
      },
      getAncestorsOf: async (args) => {
        // Return ancestor path
        return { data: [] };
      },
      mapper: (item: any) => item, // Identity mapper for this example
    });
  }
}

// Repository
export class MyTreeRepository
  extends UmbTreeRepositoryBase<MyTreeItemModel, MyTreeRootModel>
  implements UmbApi
{
  constructor(host: UmbControllerHost) {
    super(host, MyTreeDataSource);
  }

  async requestTreeRoot() {
    const data: MyTreeRootModel = {
      unique: null,
      entityType: MY_ROOT_ENTITY_TYPE,
      name: 'My Tree',
      hasChildren: true,
      isFolder: true,
    };
    return { data };
  }
}

export { MyTreeRepository as api };
为何此模式更简洁:
  • ✅ 一个文件而非两个
  • ✅ 使用
    UmbTreeServerDataSourceBase
    助手类(传入函数而非方法)
  • ✅ 数据获取逻辑内联(更易理解)
  • ✅ 无需维护单独的数据源文件
对于包含API调用的复杂树形结构,你仍可将代码拆分到不同文件中,但这并非必须。
就是这样!请始终获取最新文档,保持示例最简,生成可正常运行的完整代码。