작업 항목 위젯

프론트엔드 아키텍처

작업 항목을 위한 위젯은 프론트엔드 위젯에서 영감을 많이 받았습니다.

작업 항목은 발행 가능한 항목과 건축적으로 다르기 때문에 몇 가지 차이를 예상할 수 있습니다.

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. 새로운 위젯이 이미 백엔드에 구현되어 있고 유효한 작업 항목 유형에 대해 작업 항목 쿼리에서 반환되고 있는지 확인하세요. 다중 버전 호환성으로 인해 백엔드와 프런트엔드는 별도의 마일스톤에 있어야 합니다.

  3. workItemUpdate에서 위젯 업데이트가 지원되는지 확인하세요.

  4. 모든 위젯은 요구 사항이 다르므로, 사전에 질문하고 PM/UX와 논의한 후 MVC를 만드는 것이 좋습니다.

새로운 위젯 작업 시작 시

  1. 드롭다운, 텍스트 입력 또는 기타 사용자 정의 디자인과 같은 입력 필드에 따라 기존 래퍼를 사용하거나 완전히 새로운 컴포넌트를 만들어야 합니다.

  2. 이상적으로 모든 새로운 위젯은 테스트 공간을 확보하기 위해 FF 뒤에 있어야 합니다. 위젯에 우선 순위가 없는 경우에는 말입니다.

  3. 폴더에 새로운 위젯을 생성하세요.

  4. 사이드바에 있는 편집 가능한 위젯이라면 work_item_attributes_wrapper에 포함시켜야 합니다.

단계

새로운 작업 항목 위젯 추가 프로세스의 예시는 병합 요청 #159720를 참조하세요.

  1. app/assets/javascripts/work_items/constants.jsI18N_WORK_ITEM_ERROR_FETCHING_<widget_name>을 정의하세요.

  2. app/assets/javascripts/work_items/components/work_item_<widget_name>.vue 또는 ee/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 데이터를 업데이트하는 방법을 정의하세요.

      • 단일 값 위젯에 대해서는 값이 null일 때 우리가 지운 것인지, 또는 우리가 설정하지 않았기 때문에 null인지 구분할 수 없기 때문에 특별한 CLEAR_VALUE 상수가 필요합니다.

        예를 들어 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.js 또는 ee/spec/frontend/work_items/components/create_work_item_spec.js.

  2. spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js 또는 ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js.

  3. spec/frontend/work_items/components/work_item_<widget_name>_spec.js 또는 ee/spec/frontend/work_items/components/work_item_<widget_name>_spec.js.

  4. spec/frontend/work_items/graphql/resolvers_spec.js 또는 ee/spec/frontend/work_items/graphql/resolvers_spec.js.

  5. spec/features/projects/work_items/work_item_spec.rb 또는 ee/spec/features/projects/work_items/work_item_spec.rb.

note
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 함수가 호출되며, 이는 작업 항목 생성 뷰 모달작업 항목 생성 컴포넌트 모두에서 호출됩니다.

작업 항목 생성 뷰를 사용 용도에 따라 어떤 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. 작업 항목 로컬 변형 유형 정의에 입력 유형을 추가합니다. 커스텀 객체나 원시 값을 사용할 수 있습니다.

부모의 이름과 ID를 가지는 parent를 추가하고 싶다면 예시를 들어보세요.

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 리졸버에서 업데이트를 지원하고 새로운 작업 항목 캐시를 업데이트하는 논리를 추가합니다.
  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가 있는 정의)만 사용됩니다. 다음 단계에서는 이러한 매핑의 사용자 정의를 허용할 계획입니다. 예를 들어, 아래 테이블은 다음을 정의합니다:

  • Weight 위젯은 작업 항목 유형 0과 1에 대해 활성화됩니다.
  • Weight 위젯은 작업 항목 유형 1에 대해 편집할 수 없으며, 롤업 값만 포함되어 있고 작업 항목 유형 0은 편집 가능한 값만 포함됩니다.
  • 네임스페이스 1에서 Weight 위젯은 MyWeight로 이름이 변경됩니다. 사용자가 위젯 이름을 변경할 때, 네임스페이스 내의 모든 위젯 매핑의 이름을 변경하는 것이 합리적입니다. name 속성이 비정규화되어 있기 때문에, 이 위젯 유형에 대해 모든 작업 항목 유형에 대한 네임스페이스 매핑을 생성해야 합니다.
  • Weight 위젯은 특정 작업 항목 유형에서 비활성화될 수 있습니다(네임스페이스 3에서는 작업 항목 유형 0에 대해 비활성화되고, 여전히 작업 항목 유형 1에 대해 활성화되어 있습니다).
ID namespace_id work_item_type_id widget_type widget_options Name Disabled
1   0 1 {‘editable’ => true, ‘rollup’ => false } Weight false
2   1 1 {‘editable’ => false, ‘rollup’ => true } Weight false
3 1 0 1 {‘editable’ => true, ‘rollup’ => false } MyWeight false
4 1 1 1 {‘editable’ => false, ‘rollup’ => true } MyWeight false
5 2 0 1 {‘editable’ => true, ‘rollup’ => false } Other Weight false
6 3 0 1 {‘editable’ => true, ‘rollup’ => false } Weight true

백엔드 아키텍처

사용자 정의 세부적인 변형(예: WorkItemCreateFromTask)을 사용하여 위젯을 업데이트하거나 workItemCreate 또는 workItemUpdate 변형의 일부로 사용할 수 있습니다.

위젯 콜백

작업 항목의 변형과 함께 위젯을 업데이트할 때, 백엔드 코드는 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_save는 작업 항목 생성 또는 DB 업데이트 트랜잭션이 CreateService 또는 UpdateService에 의해 커밋되기 전에 호출됩니다.
  • after_update_commit는 DB 업데이트 트랜잭션이 UpdateService에 의해 커밋된 후에 호출됩니다.
  • after_save_commit는 작업 항목 생성 또는 DB 업데이트 트랜잭션이 CreateService 또는 UpdateService에 의해 커밋된 후에 호출됩니다.

새로운 백엔드 위젯 생성

새 작업 항목 위젯 추가 프로세스의 예는 머지 요청 #158688을 참조하십시오.

  1. 작업 항목 변형에 위젯 인수를 추가합니다:
    • 위젯이 사용 가능하고 작업 항목 생성 및 업데이트에 대해 동일한 인수가 있는 CE 기능의 경우: app/graphql/mutations/concerns/mutations/work_items/shared_arguments.rb.
    • 위젯이 하나에만 사용 가능하거나 두 변형 간 인수가 다른 EE 기능의 경우:
      • 생성: app/graphql/mutations/concerns/mutations/work_items/create_arguments.rb 또는 ee/app/graphql/ee/mutations/work_items/create.rb.
      • 업데이트: app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb 또는 ee/app/graphql/ee/mutations/work_items/update.rb.
  2. app/graphql/types/work_items/widgets/<widget_name>_input_type.rb 또는 ee/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.rb 또는 ee/app/graphql/types/work_items/widgets/<widget_name>_type.rb에서 위젯 유형을 추가하여 위젯 필드를 정의합니다.
  4. app/assets/javascripts/graphql_shared/possible_types.jsonWorkItemWidget 배열에 위젯을 추가합니다.
  5. app/graphql/types/work_items/widget_interface.rbTYPE_MAPPINGS 또는 ee/app/graphql/ee/types/work_items/widget_interface.rbEE_TYPE_MAPPINGS에 위젯 유형 매핑을 추가합니다.
  6. app/models/work_items/widget_definition.rbwidget_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.rbWIDGETS_FOR_TYPE 해시에 추가합니다.
    • db/migrate/<version>_add_<widget_name>_widget_to_work_item_types.rb에서 마이그레이션을 생성합니다. 최신 모범 사례는 db/migrate/20240812081354_add_email_participants_widget_to_work_item_types.rb를 참조하십시오. 포스트 마이그레이션을 사용할 필요는 없습니다. 머지 요청 148119에 대한 논의를 참조하십시오.
  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.rb 또는 ee/spec/graphql/types/work_items/widget_interface_spec.rb.
  2. spec/models/work_items/widget_definition_spec.rb 또는 ee/spec/models/ee/work_items/widget_definition_spec.rb.
  3. spec/models/work_items/widgets/<widget_name>_spec.rb 또는 ee/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.rb 또는 ee/spec/services/work_items/callbacks/<widget_name>_spec.rb.
  6. GraphQL 유형: spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb 또는 ee/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.rb 또는 spec/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.rb 또는 ee/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.