doc/development/work_items_widgets.md
Widgets for work items are heavily inspired by Frontend widgets. You can expect some differences, because work items are architecturally different from issuables.
GraphQL (Vue Apollo) constitutes the core of work items widgets' stack.
To display a work item page, the frontend must know which widgets are available on the work item it is attempting to display. To do so, it needs to fetch the list of widgets, using a query like this:
query workItem($workItemId: WorkItemID!) {
workItem(id: $workItemId) {
id
widgets {
... on WorkItemWidgetAssignees {
type
assignees {
nodes {
name
}
}
}
}
}
}
GraphQL queries and mutations are work item-independent. Work item queries and mutations should happen at the widget level, so widgets are standalone reusable components. The work item query and mutation should support any work item type and be dynamic. They should allow you to query and mutate any work item attribute by specifying a widget identifier.
In this query example, the description widget uses the query and mutation to display and update the description of any work item:
query workItem($fullPath: ID!, $iid: String!) {
namespace(fullPath: $fullPath) {
id
workItem(iid: $iid) {
id
iid
widgets {
... on WorkItemWidgetDescription {
description
descriptionHtml
}
}
}
}
}
Mutation example:
mutation {
workItemUpdate(input: {
id: "gid://gitlab/AnyWorkItem/499"
descriptionWidget: {
description: "New description"
}
}) {
errors
workItem {
description
}
}
}
A widget is responsible for displaying and updating a single attribute, such as title, description, or labels. Widgets must support any type of work item. To maximize component reusability, widgets should be field wrappers owning the work item query and mutation of the attribute it's responsible for.
A field component is a generic and simple component. It has no knowledge of the attribute or work item details, such as input field, date selector, or dropdown list.
Widgets must be configurable to support various use cases, depending on work items. When building widgets, use slots to provide extra context while minimizing the use of props and injected attributes.
Currently, we have a lot editable widgets which you can find in the folder namely
We also have a reusable base dropdown widget wrapper which can be used for any new widget having a dropdown. It supports both multi select and single select.
workItemUpdate.Refer to merge request #159720 for an example of the process of adding a new work item widget.
I18N_WORK_ITEM_ERROR_FETCHING_<widget_name> in app/assets/javascripts/work_items/constants.js.app/assets/javascripts/work_items/components/work_item_<widget_name>.vue or ee/app/assets/javascripts/work_items/components/work_item_<widget_name>.vue.
workItemByIidQuery- see issue #461761.app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue.app/assets/javascripts/work_items/components/create_work_item.vue.app/assets/javascripts/work_items/graphql/typedefs.graphql.app/assets/javascripts/work_items/graphql/cache_utils.js.app/assets/javascripts/work_items/graphql/resolvers.js.
CLEAR_VALUE constant is required for single value widgets, because we cannot differentiate when a value is null because we cleared it, or null because we did not
set it.
For example ee/app/assets/javascripts/work_items/components/work_item_health_status.vue.
This is not required for most widgets which support multiple values, where we can differentiate between [] and null.app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql and ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql.ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql.tooling/bin/gettext_extractor locale/gitlab.pot.At this point you should be able to use the widget in the frontend.
Now you can update tests for existing files and write tests for the new files:
spec/frontend/work_items/components/create_work_item_spec.js or ee/spec/frontend/work_items/components/create_work_item_spec.js.spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js or ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js.spec/frontend/work_items/components/work_item_<widget_name>_spec.js or ee/spec/frontend/work_items/components/work_item_<widget_name>_spec.js.spec/frontend/work_items/graphql/resolvers_spec.js or ee/spec/frontend/work_items/graphql/resolvers_spec.js.spec/features/work_items/detail/work_item_detail_spec.rb or ee/spec/features/work_items/detail/work_item_detail_spec.rb.[!note] You may find some feature specs failing because of excessive SQL queries. To resolve this, update the mocked
Gitlab::QueryLimiting::Transaction.thresholdinspec/support/shared_examples/features/work_items/rolledup_dates_shared_examples.rb.
workItemCreate mutation.Since create view is almost identical to detail view, and we wanted to store in the draft data of each widget, each new work item for a specific type has a new cache entry apollo.
For example, when we initialize the create view, we have a function setNewWorkItemCache in work items cache utils which is called in both create view work item modal and also create work item component
You can include the create work item view in any vue file depending on usage. If you pass the workItemType of the create view, it will only include the applicable work item widgets which are fetched from work item types query and only showing the ones in widget definitions
We have a local mutation to update the work item draft data in create view
updateWorkItem mutation.id/iid exists. Example.if (this.workItemId === newWorkItemId(this.workItemType)) {
this.$apollo.mutate({
mutation: updateNewWorkItemMutation,
variables: {
input: {
workItemType: this.workItemType,
fullPath: this.fullPath,
assignees: this.localAssignees,
},
},
});
Example if you want add parent which has the name and ID of the parent of the work item
input LocalParentWidgetInput {
id: String
name: String
}
input LocalUpdateNewWorkItemInput {
fullPath: String!
workItemType: String!
healthStatus: String
color: String
title: String
description: String
confidential: Boolean
parent: [LocalParentWidgetInput]
}
this.$apollo.mutate({
mutation: updateNewWorkItemMutation,
variables: {
input: {
workItemType: this.workItemType,
fullPath: this.fullPath,
parent: {
id: 'gid:://gitlab/WorkItem/1',
name: 'Parent of work item'
}
},
},
})
const { parent } = input;
if (parent) {
const parentWidget = findWidget(WIDGET_TYPE_PARENT, draftData?.namespace?.workItem);
parentWidget.parent = parent;
const parentWidgetIndex = draftData.namespace.workItem.widgets.findIndex(
(widget) => widget.type === WIDGET_TYPE_PARENT,
);
draftData.namespace.workItem.widgets[parentWidgetIndex] = parentWidget;
}
if (this.isWidgetSupported(WIDGET_TYPE_PARENT)) {
workItemCreateInput.parentWidget = {
id: this.workItemParentId
};
}
await this.$apollo.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
...workItemCreateInput,
},
});
All work item types share the same pool of predefined widgets and are customized by which widgets are active on a specific type. Widget mappings are defined in-memory through Ruby definition classes, not in the database.
Each work item type has a definition class under
app/models/work_items/types_framework/system_defined/definitions/.
The widgets method in each class returns an array of widget type
strings that determine which widgets are available for that type:
# app/models/work_items/types_framework/system_defined/definitions/issue.rb
def self.widgets
%w[assignees description labels milestone hierarchy weight ...]
end
The in-memory model
WorkItems::TypesFramework::SystemDefined::WidgetDefinition builds
its records from these definition classes at application startup. It
uses ActiveRecord::FixedItemsModel to
provide an ActiveRecord-like query interface (find_by, where,
all) without database queries.
To add a widget to a work item type, add the widget type string to the
widgets method in the relevant definition class. For example, to add
a designs widget to the Ticket type, add 'designs' to the array
returned by Definitions::Ticket.widgets. No database migration is
needed.
You can update widgets using custom fine-grained mutations (for example, WorkItemCreateFromTask) or as part of the
workItemCreate or workItemUpdate mutations.
When updating the widget together with the work item's mutation, backend code should be implemented using
callback classes that inherit from WorkItems::Callbacks::Base. These classes have callback methods
that are named similar to ActiveRecord callbacks and behave similarly.
Callback classes with the same name as the widget are automatically used. For example, WorkItems::Callbacks::AwardEmoji
is called when the work item has the AwardEmoji widget. To use a different class, you can override the callback_class
class method.
When a callback class is also used for other issuables like merge requests or epics, define the class under Issuable::Callbacks
and add the class to the list in IssuableBaseService#available_callbacks. These are executed for both work item updates and
legacy issue, merge request, or epic updates.
Use excluded_in_new_type? to check if the work item type is being changed and a widget is no longer available.
This is typically a trigger to remove associated records which are no longer relevant.
after_initialize is called after the work item is initialized by the BuildService and before
the work item is saved by the CreateService and UpdateService. This callback runs outside the
creation or update database transaction.before_create is called before the work item is saved by the CreateService. This callback runs
within the create database transaction.before_update is called before the work item is saved by the UpdateService. This callback runs
within the update database transaction.after_create is called after the work item is saved by the CreateService. This callback runs
within the create database transaction.after_update is called after the work item is saved by the UpdateService. This callback runs
within the update database transaction.after_save is called before the creation or DB update transaction is committed by the
CreateService or UpdateService.after_update_commit is called after the DB update transaction is committed by the UpdateService.after_save_commit is called after the creation or DB update transaction is committed by the
CreateService or UpdateService.[!warning] Merge request !158688 is referenced as a historical example but uses the legacy database-backed approach for widget definitions. Follow the steps below instead, which reflect the current in-memory system.
Add the widget argument to the work item mutation(s):
app/graphql/mutations/concerns/mutations/work_items/shared_arguments.rb.app/graphql/mutations/concerns/mutations/work_items/create_arguments.rb or ee/app/graphql/ee/mutations/work_items/create.rb.app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb or ee/app/graphql/ee/mutations/work_items/update.rb.Define the widget arguments, by adding a widget input type in app/graphql/types/work_items/widgets/<widget_name>_input_type.rb or ee/app/graphql/types/work_items/widgets/<widget_name>_input_type.rb.
<widget_name>_create_input_type.rb and/or <widget_name>_update_input_type.rb.Define the widget fields, by adding the widget type in app/graphql/types/work_items/widgets/<widget_name>_type.rb or ee/app/graphql/types/work_items/widgets/<widget_name>_type.rb.
Add the widget to the WorkItemWidget array in app/assets/javascripts/graphql_shared/possible_types.json.
Add the widget type mapping to TYPE_MAPPINGS in app/graphql/types/work_items/widget_interface.rb or EE_TYPE_MAPPINGS in ee/app/graphql/ee/types/work_items/widget_interface.rb.
Add the widget type string to the widget_types method in
app/models/work_items/types_framework/system_defined/widget_definition.rb.
Define the quick actions available as part of the widget in app/models/work_items/widgets/<widget_name>.rb.
Define how the mutation(s) create/update work items, by adding callbacks in app/services/work_items/callbacks/<widget_name>.rb.
if excluded_in_new_type?.raise_error to handle errors.Assign the widget to the appropriate work item types by adding the widget
type string to the widgets method in each relevant definition class under
app/models/work_items/types_framework/system_defined/definitions/.
For example, to add a widget to the Issue type, add the string to
Definitions::Issue.widgets. No database migration is needed.
Update the GraphQL docs: bundle exec rake gitlab:graphql:compile_docs.
Update translations: tooling/bin/gettext_extractor locale/gitlab.pot.
At this point you should be able to use the GraphQL query and mutation.
Now you can update tests for existing files and write tests for the new files:
spec/graphql/types/work_items/widget_interface_spec.rb or ee/spec/graphql/ee/types/work_items/widget_interface_spec.rb.spec/models/work_items/widgets/<widget_name>_spec.rb or ee/spec/models/work_items/widgets/<widget_name>_spec.rb.spec/requests/api/graphql/mutations/work_items/update_spec.rb and/or spec/requests/api/graphql/mutations/work_items/create_spec.rb.ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb and/or ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb.spec/services/work_items/callbacks/<widget_name>_spec.rb or ee/spec/services/work_items/callbacks/<widget_name>_spec.rb.spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb or ee/spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb.spec/graphql/types/work_items/widgets/<widget_name>_input_type_spec.rb or spec/graphql/types/work_items/widgets/<widget_name>_create_input_type_spec.rb and spec/graphql/types/work_items/widgets/<widget_name>_update_input_type_spec.rb.ee/spec/graphql/types/work_items/widgets/<widget_name>_input_type_spec.rb or ee/spec/graphql/types/work_items/widgets/<widget_name>_create_input_type_spec.rb and ee/spec/graphql/types/work_items/widgets/<widget_name>_update_input_type_spec.rb.