Help us learn about your current experience with the documentation. Take the survey.

工作项小部件

前端架构

工作项小部件的设计灵感主要来源于前端小部件。 你可以预期一些差异,因为工作项在架构上与可追踪项(issuables)不同。

GraphQL(Vue Apollo)构成了工作项小部件堆栈的核心。

获取工作项的小部件信息

要显示工作项页面,前端需要知道要显示的工作项上有哪些小部件可用。 为此,它需要获取小部件列表,使用如下查询:

query workItem($workItemId: WorkItemID!) {
  workItem(id: $workItemId) {
    id
    widgets {
      ... on WorkItemWidgetAssignees {
        type
        assignees {
          nodes {
            name
          }
        }
      }
    }
  }
}

GraphQL 查询和变更

GraphQL 查询和变更与工作项无关。工作项查询和变更 应该在小部件级别进行,这样小部件就是独立的可重用组件。 工作项查询和变更应该支持任何工作项类型并且是动态的。 它们应该允许你通过指定小部件标识符来查询和变更任何工作项属性。

在这个查询示例中,描述小部件使用查询和变更来 显示和更新任何工作项的描述:

query workItem($fullPath: ID!, $iid: String!) {
  workspace: namespace(fullPath: $fullPath) {
    id
    workItem(iid: $iid) {
      id
      iid
      widgets {
        ... on WorkItemWidgetDescription {
          description
          descriptionHtml
        }
      }
    }
  }
}

变更示例:

mutation {
  workItemUpdate(input: {
    id: "gid://gitlab/AnyWorkItem/499"
    descriptionWidget: {
      description: "新描述"
    }
  }) {
    errors
    workItem {
      description
    }
  }
}

小部件职责和结构

小部件负责显示和更新单个属性,如 标题、描述或标签。小部件必须支持任何类型的工作项。 为了最大化组件的可重用性,小部件应该是字段包装器, 拥有它所负责属性的查询和变更。

字段组件是一个通用的简单组件。它不知道 属性或工作项的详细信息,如输入字段、日期选择器或下拉列表。

小部件必须是可配置的,以支持各种用例,具体取决于工作项。 构建小部件时,使用插槽提供额外上下文,同时最小化 props 和注入属性的使用。

示例

目前,我们有很多可编辑的小部件,你可以在文件夹中找到,例如:

我们还有一个可重用的基础下拉小部件包装器,可用于任何具有下拉功能的新小部件。它支持多选和单选。

在详细视图中实现新的工作项小部件的步骤

开始新小部件工作之前

  1. 确保你了解新小部件的范围并准备好设计稿
  2. 检查新小部件是否已在后端实现,并且对于有效的工作项类型,工作项查询是否返回该小部件。由于多版本兼容性,我们应该将 ~backend 和 ~frontend 放在不同的里程碑中。
  3. 确保小部件更新在 workItemUpdate 中得到支持。
  4. 每个小部件都有不同的要求,因此在开始工作前提问,并与产品经理/UX 讨论后创建 MVC,这将有助于迭代开发。

开始新小部件工作时

  1. 根据输入字段(如下拉框、输入文本或任何其他自定义设计),确保我们使用现有包装器或全新的组件
  2. 理想情况下,任何新小部件都应该开启功能开关(FF),以确保我们有测试空间,除非该小部件有优先级。
  3. 文件夹中创建新小部件
  4. 如果它是侧边栏中的可编辑小部件,你应该将其包含在工作项属性包装器

步骤

参考合并请求 #159720了解添加新工作项小部件的流程示例。

  1. app/assets/javascripts/work_items/constants.js 中定义 I18N_WORK_ITEM_ERROR_FETCHING_<widget_name>
  2. 创建组件 app/assets/javascripts/work_items/components/work_item_<widget_name>.vueee/app/assets/javascripts/work_items/components/work_item_<widget_name>.vue
    • 组件不应接收任何来自 workItemByIidQuery 的 props - 参见问题 #461761
  3. 将组件添加到查看/编辑工作项屏幕 app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
  4. 如果小部件在创建新工作项时可用:
    1. 将组件添加到创建工作项屏幕 app/assets/javascripts/work_items/components/create_work_item.vue
    2. app/assets/javascripts/work_items/graphql/typedefs.graphql 中定义本地输入类型。
    3. app/assets/javascripts/work_items/graphql/cache_utils.js 中为新工作项状态 GraphQL 数据存根小部件。
    4. app/assets/javascripts/work_items/graphql/resolvers.js 中定义 GraphQL 如何更新 GraphQL 数据。
      • 单值小部件需要一个特殊的 CLEAR_VALUE 常量,因为我们无法区分值是 null(因为我们清除了它)还是 null(因为我们没有设置它)。 例如 ee/app/assets/javascripts/work_items/components/work_item_health_status.vue。 对于大多数支持多个值的小部件,我们不需要这个,因为我们可以区分 []null
      • 更多关于如何在创建视图中使用Apollo 缓存存储值的信息。
  5. 添加小部件的 GraphQL 查询:
    • 对于 CE 小部件,添加到 app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphqlee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
    • 对于 EE 小部件,添加到 ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
  6. 更新翻译:tooling/bin/gettext_extractor locale/gitlab.pot

此时你应该能够在前端使用该小部件。

现在你可以更新现有文件的测试并为新文件编写测试:

  1. spec/frontend/work_items/components/create_work_item_spec.jsee/spec/frontend/work_items/components/create_work_item_spec.js
  2. spec/frontend/work_items/components/work_item_attributes_wrapper_spec.jsee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
  3. spec/frontend/work_items/components/work_item_<widget_name>_spec.jsee/spec/frontend/work_items/components/work_item_<widget_name>_spec.js
  4. spec/frontend/work_items/graphql/resolvers_spec.jsee/spec/frontend/work_items/graphql/resolvers_spec.js
  5. spec/features/work_items/detail/work_item_detail_spec.rbee/spec/features/work_items/detail/work_item_detail_spec.rb

你可能会发现一些功能规范因过多的 SQL 查询而失败。 要解决这个问题,请在 spec/support/shared_examples/features/work_items/rolledup_dates_shared_examples.rb 中更新模拟的 Gitlab::QueryLimiting::Transaction.threshold

在创建视图中实现新的工作项小部件的步骤

  1. 确保你了解新小部件的范围并准备好设计稿
  2. 检查新小部件是否已在后端实现,并且对于有效的工作项类型,工作项查询是否返回该小部件。由于多版本兼容性,我们应该将 ~backend 和 ~frontend 放在不同的里程碑中。
  3. 确保小部件在 workItemCreate 变更中得到支持。
  4. 根据设计创建新前端小部件后,确保将其包含在创建工作项视图

在创建视图中使用 Apollo 缓存存储值

由于创建视图几乎与详细视图相同,并且我们希望将每个小部件的草稿数据存储起来,每个特定类型的新工作项都有一个新的缓存条目 apollo。

例如,当我们初始化创建视图时,有一个函数 setNewWorkItemCache 在 work items cache utils 中,它在创建视图工作项模态框创建工作项组件中都会调用

你可以根据使用情况在任何 vue 文件中包含创建工作项视图。如果你传递创建视图的 workItemType,它将只包含适用的工作项小部件,这些小部件是从小部件定义中获取的

我们有一个本地变更来更新创建视图中工作项的草稿数据

在 Apollo 缓存中支持创建表单中的新小部件

  1. 由于每个小部件可以单独使用,每个小部件都使用 updateWorkItem 变更。
  2. 现在,为了更新草稿数据,我们需要用数据更新缓存。
  3. 在更新工作项之前,我们有一个检查,它是一个新工作项还是工作项 id/iid 存在。示例。
if (this.workItemId === newWorkItemId(this.workItemType)) {
  this.$apollo.mutate({
    mutation: updateNewWorkItemMutation,
    variables: {
      input: {
        workItemType: this.workItemType,
        fullPath: this.fullPath,
        assignees: this.localAssignees,
      },
    },
});

在本地变更中支持新工作项小部件

  1. 工作项本地变更类型定义中添加输入类型。它可以是任何东西,自定义对象或原始值。

例如,如果你想添加 parent,它包含工作项父级的名称和 ID

input LocalParentWidgetInput {
  id: String
  name: String
}

input LocalUpdateNewWorkItemInput {
  fullPath: String!
  workItemType: String!
  healthStatus: String
  color: String
  title: String
  description: String
  confidential: Boolean
  parent: [LocalParentWidgetInput]
}
  1. 从小部件传递新参数以支持在创建视图中保存草稿。
this.$apollo.mutate({
    mutation: updateNewWorkItemMutation,
    variables: {
      input: {
        workItemType: this.workItemType,
        fullPath: this.fullPath,
        parent: {
          id: 'gid:://gitlab/WorkItem/1',
          name: '工作项的父级'
        }
      },
    },
})
  1. graphql resolver中支持更新,并添加更新新工作项缓存的逻辑
  const { parent } = input;

  if (parent) {
      const parentWidget = findWidget(WIDGET_TYPE_PARENT, draftData?.workspace?.workItem);
      parentWidget.parent = parent;

      const parentWidgetIndex = draftData.workspace.workItem.widgets.findIndex(
        (widget) => widget.type === WIDGET_TYPE_PARENT,
      );
      draftData.workspace.workItem.widgets[parentWidgetIndex] = parentWidget;
  }
  1. 创建工作项视图中获取草稿值

if (this.isWidgetSupported(WIDGET_TYPE_PARENT)) {
    workItemCreateInput.parentWidget = {
      id: this.workItemParentId
    };
}

await this.$apollo.mutate({
  mutation: createWorkItemMutation,
  variables: {
    input: {
      ...workItemCreateInput,
    },
});

将小部件映射到工作项类型

所有工作项类型共享相同的预定义小部件池,并通过特定类型上激活的小部件进行定制。因为我们计划允许用户创建新的工作项类型并为它们定义一组小部件,因此每个工作项类型的小部件映射存储在数据库中。小部件映射存储在 widget_definitions 表中,可用于为默认工作项类型定义小部件,也可用于将来为自定义类型定义。有关预期数据库表结构的更多详细信息,请参阅此问题描述

向工作项类型添加新小部件

由于每个工作项类型分配了哪些小部件的信息存储在数据库中,因此向工作项类型添加新小部件需要通过数据库迁移来完成。此外,还应更新小部件导入器(lib/gitlab/database_importers/work_items/widgets_importer.rb)。

小部件定义表的结构

表中的每条记录定义了小部件到工作项类型的映射。目前只使用"全局"定义(具有 NULL namespace_id 的定义)。在后续迭代中,我们计划允许自定义这些映射。例如,下表定义了:

  • 权重小部件为工作项类型 0 和 1 启用
  • 权重小部件对于工作项类型 1 不可编辑,只包含汇总值,而工作项类型 0 只包含可编辑值
  • 在命名空间 1 中,权重小部件被重命名为 MyWeight。当用户重命名小部件名称时,重命名该命名空间中所有小部件映射是有意义的 - 因为 name 属性是反规范化的,我们必须为该小部件类型创建所有工作项类型的命名空间映射
  • 权重小部件可以针对特定工作项类型禁用(在命名空间 3 中,它对工作项类型 0 被禁用,而对工作项类型 1 仍然启用)
ID namespace_id work_item_type_id widget_type widget_options Name Disabled
1 0 1 {’editable’ => true, ‘rollup’ => false } 权重 false
2 1 1 {’editable’ => false, ‘rollup’ => true } 权重 false
3 1 0 1 {’editable’ => true, ‘rollup’ => false } 我的权重 false
4 1 1 1 {’editable’ => false, ‘rollup’ => true } 我的权重 false
5 2 0 1 {’editable’ => true, ‘rollup’ => false } 其他权重 false
6 3 0 1 {’editable’ => true, ‘rollup’ => false } 权重 true

后端架构

你可以使用自定义细粒度变更(例如,WorkItemCreateFromTask)或作为 workItemCreateworkItemUpdate 变更的一部分来更新小部件。

小部件回调

当与小部件的工作项变更一起更新时,后端代码应该使用 继承自 WorkItems::Callbacks::Base 的回调类来实现。这些类具有 与 ActiveRecord 回调命名相似的回调方法,行为也类似。

与小部件同名的回调类会自动使用。例如,当工作项具有 AwardEmoji 小部件时,会调用 WorkItems::Callbacks::AwardEmoji。要使用不同的类,你可以覆盖 callback_class 类方法。

当回调类也用于其他可追踪项(如合并请求或史诗)时,请在 Issuable::Callbacks 下定义该类,并将其添加到 IssuableBaseService#available_callbacks 列表中。这些回调既执行工作项更新,也执行传统问题、合并请求或史诗的更新。

使用 excluded_in_new_type? 来检查工作项类型是否正在更改,小部件不再可用。 这通常是触发删除不再相关关联记录的触发器。

可用回调

  • after_initialize 在工作项由 BuildService 初始化之后、 在工作项被 CreateServiceUpdateService 保存之前调用。此回调在创建或更新数据库事务之外运行。
  • before_create 在工作项被 CreateService 保存之前调用。此回调在创建数据库事务内运行。
  • before_update 在工作项被 UpdateService 保存之前调用。此回调在更新数据库事务内运行。
  • after_create 在工作项被 CreateService 保存之后调用。此回调在创建数据库事务内运行。
  • after_update 在工作项被 UpdateService 保存之后调用。此回调在更新数据库事务内运行。
  • after_saveCreateServiceUpdateService 提交创建或 DB 更新事务之前调用。
  • after_update_commitUpdateService 提交 DB 更新事务之后调用。
  • after_save_commitCreateServiceUpdateService 提交创建或 DB 更新事务之后调用。

创建新的后端小部件

参考合并请求 #158688了解添加新工作项小部件的流程示例。

  1. 将小部件参数添加到工作项变更中:

    • 对于 CE 功能,小部件可用且创建和更新工作项的参数相同:app/graphql/mutations/concerns/mutations/work_items/shared_arguments.rb
    • 对于 EE 功能,小部件仅对一个可用,或两个变更的参数不同:
      • 创建:app/graphql/mutations/concerns/mutations/work_items/create_arguments.rbee/app/graphql/ee/mutations/work_items/create.rb
      • 更新:app/graphql/mutations/concerns/mutations/work_items/update_arguments.rbee/app/graphql/ee/mutations/work_items/update.rb
  2. 定义小部件参数,通过在 app/graphql/types/work_items/widgets/<widget_name>_input_type.rbee/app/graphql/types/work_items/widgets/<widget_name>_input_type.rb 中添加小部件输入类型。

    • 如果创建和更新变更的输入类型不同,使用 <widget_name>_create_input_type.rb 和/或 <widget_name>_update_input_type.rb
  3. 定义小部件字段,通过在 app/graphql/types/work_items/widgets/<widget_name>_type.rbee/app/graphql/types/work_items/widgets/<widget_name>_type.rb 中添加小部件类型。

  4. 将小部件添加到 app/assets/javascripts/graphql_shared/possible_types.json 中的 WorkItemWidget 数组。

  5. 将小部件类型映射添加到 app/graphql/types/work_items/widget_interface.rb 中的 TYPE_MAPPINGSee/app/graphql/ee/types/work_items/widget_interface.rb 中的 EE_TYPE_MAPPINGS

  6. 将小部件类型添加到 app/models/work_items/widget_definition.rb 中的 widget_type 枚举。

  7. app/models/work_items/widgets/<widget_name>.rb 中定义作为小部件一部分的可用快速操作。

  8. 定义变更如何创建/更新工作项,通过在 app/services/work_items/callbacks/<widget_name>.rb 中添加回调

    • 考虑是否需要处理 if excluded_in_new_type?
    • 使用 raise_error 处理错误。
  9. lib/gitlab/database_importers/work_items/base_type_importer.rbWIDGET_NAMES 哈希中定义小部件。

  10. 通过以下方式将小部件分配给适当的工作项类型:

    • 将其添加到 lib/gitlab/database_importers/work_items/base_type_importer.rb 中的 WIDGETS_FOR_TYPE 哈希。

    • db/migrate/<version>_add_<widget_name>_widget_to_work_item_types.rb 中创建迁移。 参考 db/migrate/20250121163545_add_custom_fields_widget_to_work_item_types.rb 了解最新的最佳实践。 不需要使用后迁移,请参阅合并请求 148119 上的讨论。 如果你想了解更多关于迁移结构的信息,请参阅 lib/gitlab/database/migration_helpers/work_items/widgets.rb

      # frozen_string_literal: true
      
      class AddDesignsAndDevelopmentWidgetsToTicketWorkItemType < Gitlab::Database::Migration[2.2]
        # Include this helper module as it's not included in Gitlab::Database::migration by default
        include Gitlab::Database::MigrationHelpers::WorkItems::Widgets
      
        restrict_gitlab_migration gitlab_schema: :gitlab_main
        milestone '17.9'
      
        WORK_ITEM_TYPE_ENUM_VALUES = 8 # ticket, use [8,9] for multiple types
        # If you want to add one widget, only use one item here.
        WIDGETS = [
          {
            name: 'Designs',
            widget_type: 22
          },
          {
            name: 'Development',
            widget_type: 23
          }
        ]
      
        def up
          add_widget_definitions(type_enum_values: WORK_ITEM_TYPE_ENUM_VALUES, widgets: WIDGETS)
        end
      
        def down
          remove_widget_definitions(type_enum_values: WORK_ITEM_TYPE_ENUM_VALUES, widgets: WIDGETS)
        end
      end
  11. 更新 GraphQL 文档:bundle exec rake gitlab:graphql:compile_docs

  12. 更新翻译:tooling/bin/gettext_extractor locale/gitlab.pot

此时你应该能够使用GraphQL 查询和变更

现在你可以更新现有文件的测试并为新文件编写测试:

  1. spec/graphql/types/work_items/widget_interface_spec.rbee/spec/graphql/types/work_items/widget_interface_spec.rb

  2. spec/models/work_items/widget_definition_spec.rbee/spec/models/ee/work_items/widget_definition_spec.rb

  3. spec/models/work_items/widgets/<widget_name>_spec.rbee/spec/models/work_items/widgets/<widget_name>_spec.rb

  4. 请求:

    • CE:spec/requests/api/graphql/mutations/work_items/update_spec.rb 和/或 spec/requests/api/graphql/mutations/work_items/create_spec.rb
    • EE:ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb 和/或 ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb
  5. 回调:spec/services/work_items/callbacks/<widget_name>_spec.rbee/spec/services/work_items/callbacks/<widget_name>_spec.rb

  6. GraphQL 类型:spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rbee/spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb

  7. GraphQL 输入类型:

    • CE:spec/graphql/types/work_items/widgets/<widget_name>_input_type_spec.rbspec/graphql/types/work_items/widgets/<widget_name>_create_input_type_spec.rbspec/graphql/types/work_items/widgets/<widget_name>_update_input_type_spec.rb
    • EE:ee/spec/graphql/types/work_items/widgets/<widget_name>_input_type_spec.rbee/spec/graphql/types/work_items/widgets/<widget_name>_create_input_type_spec.rbee/spec/graphql/types/work_items/widgets/<widget_name>_update_input_type_spec.rb
  8. 迁移:spec/migrations/<version>_add_<widget_name>_widget_to_work_item_types_spec.rb。添加使用 described_class 常量的共享示例。

    # frozen_string_literal: true
    
    require 'spec_helper'
    require_migration!
    
    RSpec.describe AddDesignsAndDevelopmentWidgetsToTicketWorkItemType, :migration, feature_category: :team_planning do
      # Tests for `n` widgets in your migration when using the work items widgets migration helper
      it_behaves_like 'migration that adds widgets to a work item type'
    end