Work items 위젯
GitLab v19.4요약
작업 항목 위젯은 프론트엔드 위젯에서 많은 영향을 받았습니다. GraphQL(Vue Apollo)이 작업 항목 위젯 스택의 핵심을 이룹니다. 작업 항목 페이지를 표시하려면 프론트엔드가 표시하려는 작업 항목에서 어떤 위젯을 사용할 수 있는지 알아야 합니다.
프론트엔드 아키텍처#
작업 항목 위젯은 프론트엔드 위젯에서 많은 영향을 받았습니다. 작업 항목은 이슈어블과 아키텍처가 다르므로 일부 차이가 있습니다.
GraphQL(Vue Apollo)이 작업 항목 위젯 스택의 핵심을 이룹니다.
작업 항목의 위젯 정보 조회#
작업 항목 페이지를 표시하려면 프론트엔드가 표시하려는 작업 항목에서 어떤 위젯을 사용할 수 있는지 알아야 합니다. 이를 위해 다음과 같은 쿼리로 위젯 목록을 가져와야 합니다.
query workItem($workItemId: WorkItemID!) {
workItem(id: $workItemId) {
id
widgets {
... on WorkItemWidgetAssignees {
type
assignees {
nodes {
name
}
}
}
}
}
}
GraphQL 쿼리 및 뮤테이션#
GraphQL 쿼리와 뮤테이션은 작업 항목과 무관합니다. 작업 항목 쿼리와 뮤테이션은 위젯 수준에서 이루어져야 하며, 그래야 위젯이 독립적으로 재사용 가능한 컴포넌트가 됩니다. 작업 항목 쿼리와 뮤테이션은 모든 작업 항목 유형을 지원하고 동적이어야 합니다. 또한 위젯 식별자를 지정해 어떤 작업 항목 속성이든 쿼리하고 변경할 수 있어야 합니다.
다음 쿼리 예시에서 description 위젯은 이 쿼리와 뮤테이션을 사용해 모든 작업 항목의 설명을 표시하고 업데이트합니다.
query workItem($fullPath: ID!, $iid: String!) {
namespace(fullPath: $fullPath) {
id
workItem(iid: $iid) {
id
iid
widgets {
... on WorkItemWidgetDescription {
description
descriptionHtml
}
}
}
}
}
뮤테이션 예시.
mutation {
workItemUpdate(input: {
id: "gid://gitlab/AnyWorkItem/499"
descriptionWidget: {
description: "New description"
}
}) {
errors
workItem {
description
}
}
}
위젯의 책임과 구조#
위젯은 제목, 설명, 레이블처럼 단일 속성 하나를 표시하고 업데이트하는 역할을 합니다. 위젯은 모든 유형의 작업 항목을 지원해야 합니다. 컴포넌트 재사용성을 최대화하기 위해 위젯은 자신이 담당하는 속성의 작업 항목 쿼리와 뮤테이션을 소유하는 필드 래퍼여야 합니다.
필드 컴포넌트는 범용적이고 단순한 컴포넌트입니다. 입력 필드, 날짜 선택기, 드롭다운 목록처럼 속성이나 작업 항목의 세부 사항은 알지 못합니다.
위젯은 작업 항목에 따라 다양한 사용 사례를 지원하도록 구성할 수 있어야 합니다. 위젯을 만들 때는 props와 주입 속성 사용을 최소화하면서 슬롯으로 추가 컨텍스트를 제공합니다.
예시#
현재 해당 폴더에는 편집 가능한 위젯이 많이 있으며, 구체적으로 다음과 같습니다
드롭다운이 있는 새 위젯에 사용할 수 있는 재사용 가능한 기본 드롭다운 위젯 래퍼도 있습니다. 이 래퍼는 다중 선택과 단일 선택을 모두 지원합니다.
상세 뷰에서 새 작업 항목 위젯을 프론트엔드에 구현하는 단계#
새 위젯 작업을 시작하기 전에#
- 새 위젯의 범위를 파악하고 디자인이 준비되어 있는지 확인합니다
- 새 위젯이 백엔드에 이미 구현되어 있고 유효한 작업 항목 유형에 대해 작업 항목 쿼리가 이를 반환하는지 확인합니다. 다중 버전 호환성 때문에 ~backend와 ~frontend는 별도의 마일스톤으로 두어야 합니다.
- 위젯 업데이트가
workItemUpdate에서 지원되는지 확인합니다. - 위젯마다 요구 사항이 다르므로, 미리 질문하고 PM/UX와 논의한 뒤 MVC를 만들어 반복 개선하는 방식이 좋습니다.
새 위젯 작업을 시작할 때#
- 드롭다운, 입력 텍스트 등 입력 필드나 그 밖의 맞춤 디자인에 따라 기존 래퍼를 사용할지 완전히 새로운 컴포넌트를 사용할지 결정합니다
- 위젯에 우선순위가 있는 경우가 아니라면, 새 위젯은 테스트 여지를 확보하도록 기능 플래그 뒤에 두는 것이 이상적입니다.
- 해당 폴더에 새 위젯을 만듭니다
- 사이드바의 편집 가능한 위젯이라면 work_item_attributes_wrapper에 포함합니다
단계#
새 작업 항목 위젯을 추가하는 과정의 예시는 머지 리퀘스트 #159720을 참고합니다.
app/assets/javascripts/work_items/constants.js에I18N_WORK_ITEM_ERROR_FETCHING_<widget_name>을 정의합니다.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을 참고합니다.
- 이 컴포넌트는
- 작업 항목 보기/편집 화면인
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에서 해당 위젯의 새 작업 항목 상태 GraphQL 데이터를 스텁으로 만듭니다.app/assets/javascripts/work_items/graphql/resolvers.js에서 GraphQL이 GraphQL 데이터를 업데이트하는 방식을 정의합니다.- 단일 값 위젯에는
CLEAR_VALUE상수가 필요합니다. 값이null인 이유가 값을 지웠기 때문인지, 애초에 설정하지 않았기 때문인지 구분할 수 없기 때문입니다. 예를 들어ee/app/assets/javascripts/work_items/components/work_item_health_status.vue가 있습니다.[]와null을 구분할 수 있는 다중 값 위젯 대부분에는 필요하지 않습니다. - 생성 뷰에서 값을 저장하는 데 사용하는 Apollo 캐시에 대한 내용도 참고합니다.
- 단일 값 위젯에는
- 작업 항목 생성 화면인
- 위젯의 GraphQL 쿼리를 추가합니다.
- CE 위젯은
app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql과ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql에 추가합니다. - EE 위젯은
ee/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql에 추가합니다.
- CE 위젯은
- 번역을 업데이트합니다:
tooling/bin/gettext_extractor locale/gitlab.pot.
이 시점에서 프론트엔드에서 위젯을 사용할 수 있습니다.
이제 기존 파일의 테스트를 업데이트하고 새 파일의 테스트를 작성할 수 있습니다.
spec/frontend/work_items/components/create_work_item_spec.js또는ee/spec/frontend/work_items/components/create_work_item_spec.js.spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js또는ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js.spec/frontend/work_items/components/work_item_<widget_name>_spec.js또는ee/spec/frontend/work_items/components/work_item_<widget_name>_spec.js.spec/frontend/work_items/graphql/resolvers_spec.js또는ee/spec/frontend/work_items/graphql/resolvers_spec.js.spec/features/work_items/detail/work_item_detail_spec.rb또는ee/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를 업데이트합니다.
생성 뷰에서 새 작업 항목 위젯을 프론트엔드에 구현하는 단계#
- 새 위젯의 범위를 파악하고 디자인이 준비되어 있는지 확인합니다
- 새 위젯이 백엔드에 이미 구현되어 있고 유효한 작업 항목 유형에 대해 작업 항목 쿼리가 이를 반환하는지 확인합니다. 다중 버전 호환성 때문에 ~backend와 ~frontend는 별도의 마일스톤으로 두어야 합니다.
- 위젯이
workItemCreate뮤테이션에서 지원되는지 확인합니다. - 디자인을 기반으로 새 프론트엔드 위젯을 만든 뒤에는 작업 항목 생성 뷰에 반드시 포함합니다
생성 뷰에서 값을 저장하는 데 사용하는 Apollo 캐시#
생성 뷰는 상세 뷰와 거의 동일하고 각 위젯의 초안 데이터를 저장하려고 했기 때문에, 특정 유형의 새 작업 항목마다 Apollo에 새 캐시 항목이 생깁니다.
예를 들어 생성 뷰를 초기화할 때 작업 항목 캐시 유틸의 setNewWorkItemCache 함수를 사용하며, 이 함수는 생성 뷰 작업 항목 모달과 작업 항목 생성 컴포넌트 양쪽에서 호출됩니다
사용 목적에 따라 어떤 vue 파일에서도 작업 항목 생성 뷰를 포함할 수 있습니다. 생성 뷰의 workItemType을 전달하면 작업 항목 유형 쿼리에서 가져온, 해당 유형에 적용 가능한 작업 항목 위젯만 포함되며 위젯 정의에 있는 위젯만 표시됩니다
생성 뷰에서 작업 항목 초안 데이터를 업데이트하는 로컬 뮤테이션이 있습니다
Apollo 캐시의 생성 폼에서 새 위젯 지원#
- 모든 위젯은 개별적으로 사용할 수 있으므로 각 위젯은
updateWorkItem뮤테이션을 사용합니다. - 초안 데이터를 업데이트하려면 해당 데이터로 캐시를 업데이트해야 합니다.
- 작업 항목을 업데이트하기 직전에 새 작업 항목인지, 아니면 작업 항목
id/iid가 존재하는지 확인합니다. 예시는 다음과 같습니다.
if (this.workItemId === newWorkItemId(this.workItemType)) {
this.$apollo.mutate({
mutation: updateNewWorkItemMutation,
variables: {
input: {
workItemType: this.workItemType,
fullPath: this.fullPath,
assignees: this.localAssignees,
},
},
});
로컬 뮤테이션에서 새 작업 항목 위젯 지원#
- 작업 항목 로컬 뮤테이션 typedefs에 입력 유형을 추가합니다. 사용자 정의 객체든 기본 값이든 무엇이든 될 수 있습니다.
작업 항목의 상위 항목 이름과 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]
}
- 생성 뷰에서 초안 저장을 지원하도록 위젯에서 새 파라미터를 전달합니다.
this.$apollo.mutate({
mutation: updateNewWorkItemMutation,
variables: {
input: {
workItemType: this.workItemType,
fullPath: this.fullPath,
parent: {
id: 'gid:://gitlab/WorkItem/1',
name: 'Parent of work item'
}
},
},
})
- graphql 리졸버에서 업데이트를 지원하고 새 작업 항목 캐시를 업데이트하는 로직을 추가합니다
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,
},
});
위젯을 작업 항목 유형에 매핑#
모든 작업 항목 유형은 사전 정의된 동일한 위젯 풀을 공유하며, 특정 유형에서 어떤 위젯이 활성화되어 있는지로 맞춤 구성됩니다. 위젯 매핑은 데이터베이스가 아니라 Ruby 정의 클래스를 통해 메모리에서 정의됩니다.
각 작업 항목 유형에는
app/models/work_items/types_framework/system_defined/definitions/ 아래에 정의 클래스가 있습니다.
각 클래스의 widgets 메서드는 해당 유형에서 사용할 수 있는 위젯을 결정하는 위젯 유형
문자열 배열을 반환합니다.
# app/models/work_items/types_framework/system_defined/definitions/issue.rb
def self.widgets
%w[assignees description labels milestone hierarchy weight ...]
end
메모리 내 모델
WorkItems::TypesFramework::SystemDefined::WidgetDefinition은 애플리케이션이 시작될 때
이 정의 클래스들로부터 레코드를 구성합니다. 이 모델은
ActiveRecord::FixedItemsModel을 사용해 데이터베이스 쿼리 없이
ActiveRecord와 유사한 쿼리 인터페이스(find_by, where,
all)를 제공합니다.
작업 항목 유형에 새 위젯 추가#
작업 항목 유형에 위젯을 추가하려면 관련 정의 클래스의 widgets 메서드에
위젯 유형 문자열을 추가합니다. 예를 들어 Ticket 유형에
designs 위젯을 추가하려면 Definitions::Ticket.widgets가 반환하는 배열에
'designs'를 추가합니다. 데이터베이스 마이그레이션은
필요하지 않습니다.
백엔드 아키텍처#
사용자 정의 세분화 뮤테이션(예: 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가 작업 항목을 초기화한 후,CreateService와UpdateService가 작업 항목을 저장하기 전에 호출됩니다. 이 콜백은 생성 또는 업데이트 데이터베이스 트랜잭션 밖에서 실행됩니다.before_create는CreateService가 작업 항목을 저장하기 전에 호출됩니다. 이 콜백은 생성 데이터베이스 트랜잭션 안에서 실행됩니다.before_update는UpdateService가 작업 항목을 저장하기 전에 호출됩니다. 이 콜백은 업데이트 데이터베이스 트랜잭션 안에서 실행됩니다.after_create는CreateService가 작업 항목을 저장한 후에 호출됩니다. 이 콜백은 생성 데이터베이스 트랜잭션 안에서 실행됩니다.after_update는UpdateService가 작업 항목을 저장한 후에 호출됩니다. 이 콜백은 업데이트 데이터베이스 트랜잭션 안에서 실행됩니다.after_save는CreateService또는UpdateService가 생성 또는 DB 업데이트 트랜잭션을 커밋하기 전에 호출됩니다.after_update_commit는UpdateService가 DB 업데이트 트랜잭션을 커밋한 후에 호출됩니다.after_save_commit는CreateService또는UpdateService가 생성 또는 DB 업데이트 트랜잭션을 커밋한 후에 호출됩니다.
새 백엔드 위젯 만들기#
머지 리퀘스트 !158688은 과거 사례로 참조되지만 위젯 정의에 레거시 데이터베이스 기반 방식을 사용합니다. 대신 현재의 메모리 기반 시스템을 반영한 아래 단계를 따릅니다.
- 작업 항목 뮤테이션에 위젯 인수를 추가합니다.
- 위젯을 사용할 수 있고 작업 항목 생성과 업데이트에 동일한 인수를 사용하는 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.
- 생성:
- 위젯을 사용할 수 있고 작업 항목 생성과 업데이트에 동일한 인수를 사용하는 CE 기능:
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를 사용합니다.
- 생성 뮤테이션과 업데이트 뮤테이션의 입력 유형이 다르면
app/graphql/types/work_items/widgets/<widget_name>_type.rb또는ee/app/graphql/types/work_items/widgets/<widget_name>_type.rb에 위젯 유형을 추가해 위젯 필드를 정의합니다.app/assets/javascripts/graphql_shared/possible_types.json의WorkItemWidget배열에 위젯을 추가합니다.app/graphql/types/work_items/widget_interface.rb의TYPE_MAPPINGS또는ee/app/graphql/ee/types/work_items/widget_interface.rb의EE_TYPE_MAPPINGS에 위젯 유형 매핑을 추가합니다.app/models/work_items/types_framework/system_defined/widget_definition.rb의widget_types메서드에 위젯 유형 문자열을 추가합니다.app/models/work_items/widgets/<widget_name>.rb에서 위젯의 일부로 사용할 수 있는 퀵 액션을 정의합니다.app/services/work_items/callbacks/<widget_name>.rb에 콜백을 추가해 뮤테이션이 작업 항목을 생성하거나 업데이트하는 방식을 정의합니다.if excluded_in_new_type?를 처리해야 하는지 검토합니다.- 오류 처리에는
raise_error를 사용합니다.
app/models/work_items/types_framework/system_defined/definitions/아래의 관련 정의 클래스마다widgets메서드에 위젯 유형 문자열을 추가해 적절한 작업 항목 유형에 위젯을 할당합니다. 예를 들어 Issue 유형에 위젯을 추가하려면Definitions::Issue.widgets에 해당 문자열을 추가합니다. 데이터베이스 마이그레이션은 필요하지 않습니다.- GraphQL 문서를 업데이트합니다:
bundle exec rake gitlab:graphql:compile_docs. - 번역을 업데이트합니다:
tooling/bin/gettext_extractor locale/gitlab.pot.
이 시점에서 GraphQL 쿼리와 뮤테이션을 사용할 수 있습니다.
이제 기존 파일의 테스트를 업데이트하고 새 파일의 테스트를 작성할 수 있습니다.
spec/graphql/types/work_items/widget_interface_spec.rb또는ee/spec/graphql/ee/types/work_items/widget_interface_spec.rb.spec/models/work_items/widgets/<widget_name>_spec.rb또는ee/spec/models/work_items/widgets/<widget_name>_spec.rb.- 요청:
- 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.
- CE:
- 콜백:
spec/services/work_items/callbacks/<widget_name>_spec.rb또는ee/spec/services/work_items/callbacks/<widget_name>_spec.rb. - GraphQL 유형:
spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb또는ee/spec/graphql/types/work_items/widgets/<widget_name>_type_spec.rb. - 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.rb와spec/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.rb와ee/spec/graphql/types/work_items/widgets/<widget_name>_update_input_type_spec.rb.
- CE: