Skip to content

Support quick action commands via description editing for work items

What does this MR do and why?

We want to support quick actions via editing work item's description field. Related to #382160 (closed).

What's Work Item?

Internally, Work item model is essentially an alias for Issue model at this point backed by the same issue table.

Each work item has a type. For example, here's a work item that's a Task https://gitlab.com/gitlab-org/gitlab/-/work_items/121960434

Each work item type (e.g, Task, Objective, etc) supports a set of widgets. Task supports assignee, labels and weight widgets (the list is not exhaustive.) Objective might not support the same set of widgets.

Task type supports the assignee widget and when I type /assign @euko in the description field and update the work item, I should be assigned to the task.

How quick actions get interpreted and applied (aka how does it work?)

We define a set of quick actions for domain objects using a custom DSL:

# https://gitlab.com/gitlab-org/gitlab/-/blob/6b4b214bb2e79339dd908fbe6d1aa4f8d03ba3a7/lib/gitlab/quick_actions/issue_actions.rb#L9
        desc { _('Set due date') }
        explanation do |due_date|
          _("Sets the due date to %{due_date}.") % { due_date: due_date.strftime('%b %-d, %Y') } if due_date
        end
        execution_message do |due_date|
          _("Set the due date to %{due_date}.") % { due_date: due_date.strftime('%b %-d, %Y') } if due_date
        end
        params '<in 2 days | this Friday | December 31st>'
        types Issue
        condition do
          quick_action_target.respond_to?(:due_date) &&
            current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
        end
        parse_params do |due_date_param|
          Chronic.parse(due_date_param).try(:to_date)
        end
        command :due do |due_date|
          if due_date
            @updates[:due_date] = due_date
          else
            @execution_message[:due] = _('Failed to set due date because the date format is invalid.')
          end
        end

QuickActions::InterpretService is the service that accepts an input containing quick action strings (raw string commands) and extracts the commands defined with the DSL mentioned earlier.

        description, command_params = QuickActions::InterpretService
            .new(work_item.project, current_user, {})
            .execute(original_description, work_item)
  • original_description may contains a raw input like Foobar this is some text\n \assign @euko.
  • description contains the input without the interpreted commands: Foobar this is some text.
  • command_params contains the extracted parameters for updates: assignee_ids: [12312].

In the mutation resolver for updating a work item, we will run QuickActions::InterpretService then assign each extracted update param to the appropriate widget available on the work item.

To continue with the example, say command_params contained assignee_ids: [12312] and the work item being updated had the assignee widget. Because the assignee widget knows it must handle the update parameter assignee_ids, we will assign it to the assignee widget and so on:

# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110153/diffs#64bc31af50779ac0ebabc322cc4cbca88bdadd60_46_49
        # Widgets have a set of quick action params that they must process.
        # Map them to widget_params so they can be picked up by widget services.
        work_item.work_item_type.widgets
          .filter { |widget| widget.respond_to?(:quick_action_params) }
          .each do |widget|
            widget.quick_action_params
              .filter { |param_name| command_params.key?(param_name) }
              .each do |param_name|
                widget_params[widget.api_symbol] ||= {}
                widget_params[widget.api_symbol][param_name] = command_params.delete(param_name)
              end
          end

Concretely, we will execute a statement like widget_params[assignee_widget.api_symbol][:assignee_ids] = command_params.delete(param_name)

How to set up and validate locally

First, create a new work item. The most common work item in use is Task. You may also create Objective/KeyResult if you are familiar with them.

How to create a Task (work item) Screen_Recording_2023-01-26_at_17.35.48

Note the id of the created work item.

Here's a sample GraphQL query that edits the description of a work item with two quick actions.

  • Change the title to abc
  • Add @root as an assignee
mutation UpdateWorkItem {
  workItemUpdate(
    input: {id: "gid://gitlab/WorkItem/640", descriptionWidget: {description: "/title abc \n/assign @root"}}
  ) {
    workItem {
      id
      title
      widgets {
        ... on WorkItemWidgetDescription {
          description
        }
        ... on WorkItemWidgetAssignees {
          assignees {
            nodes {
              id
              username
            }
          }
        }
      }
    }
  }
}

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #382160 (closed)

Edited by euko

Merge request reports