Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/editor/schema/ci.json15
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue7
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue73
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue6
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql9
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue128
-rw-r--r--app/presenters/ci/build_runner_presenter.rb57
9 files changed, 205 insertions, 93 deletions
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 1c56327c03c..2d222903c0c 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -187,6 +187,21 @@
}
]
},
+ "coverage_report": {
+ "type": "object",
+ "description": "Used to collect coverage reports from the job.",
+ "properties": {
+ "coverage_format": {
+ "description": "Code coverage format used by the test framework.",
+ "enum": ["cobertura"]
+ },
+ "path": {
+ "description": "Path to the coverage report file that should be parsed.",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
"codequality": {
"$ref": "#/definitions/string_file_list",
"description": "Path to file or list of files with code quality report(s) (such as Code Climate)."
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 4ebb49b4756..3726743c032 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -19,3 +19,4 @@ export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
+export const TYPE_WORK_ITEM = 'WorkItem';
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 0490728c6bc..78ef909c458 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -185,6 +185,11 @@ export default {
required: false,
default: false,
},
+ issueId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
data() {
const store = new Store({
@@ -534,6 +539,7 @@ export default {
<component
:is="descriptionComponent"
+ :issue-id="issueId"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
@@ -545,6 +551,7 @@ export default {
@taskListUpdateStarted="taskListUpdateStarted"
@taskListUpdateSucceeded="taskListUpdateSucceeded"
@taskListUpdateFailed="taskListUpdateFailed"
+ @updateDescription="state.descriptionHtml = $event"
/>
<edited-component
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 68ed7bb4062..9f2d48ad82b 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -7,6 +7,8 @@ import {
GlButton,
} from '@gitlab/ui';
import $ from 'jquery';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import TaskList from '~/task_list';
@@ -63,6 +65,11 @@ export default {
required: false,
default: 0,
},
+ issueId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -81,6 +88,9 @@ export default {
workItemsEnabled() {
return this.glFeatures.workItems;
},
+ issueGid() {
+ return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null;
+ },
},
watch: {
descriptionHtml(newDescription, oldDescription) {
@@ -92,6 +102,9 @@ export default {
this.$nextTick(() => {
this.renderGFM();
+ if (this.workItemsEnabled) {
+ this.renderTaskActions();
+ }
});
},
taskStatus() {
@@ -168,9 +181,24 @@ export default {
return;
}
+ this.taskButtons = [];
const taskListFields = this.$el.querySelectorAll('.task-list-item');
taskListFields.forEach((item, index) => {
+ const taskLink = item.querySelector('.gfm-issue');
+ if (taskLink) {
+ const { issue, referenceType } = taskLink.dataset;
+ taskLink.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
+ this.track('viewed_work_item_from_modal', {
+ category: 'workItems:show',
+ label: 'work_item_view',
+ property: `type_${referenceType}`,
+ });
+ });
+ return;
+ }
const button = document.createElement('button');
button.classList.add(
'btn',
@@ -195,7 +223,14 @@ export default {
});
},
openCreateTaskModal(id) {
- this.activeTask = { id, title: this.$el.querySelector(`#${id}`).parentElement.innerText };
+ const { parentElement } = this.$el.querySelector(`#${id}`);
+ const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
+ this.activeTask = {
+ id,
+ title: parentElement.innerText,
+ lineNumberStart: lineNumbers[0],
+ lineNumberEnd: lineNumbers[1],
+ };
this.$refs.modal.show();
},
closeCreateTaskModal() {
@@ -207,38 +242,10 @@ export default {
handleWorkItemDetailModalError(message) {
createFlash({ message });
},
- handleCreateTask({ id, title, type }) {
- const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement;
- const taskBadge = document.createElement('span');
- taskBadge.innerHTML = `
- <svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12">
- <use href="${gon.sprite_icons}#issue-open-m"></use>
- </svg>
- <span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
- ${__('Task')}
- </span>
- `;
- const button = this.createWorkItemDetailButton(id, title, type);
- taskBadge.append(button);
-
- listItem.insertBefore(taskBadge, listItem.lastChild);
- listItem.removeChild(listItem.lastChild);
+ handleCreateTask(description) {
+ this.$emit('updateDescription', description);
this.closeCreateTaskModal();
},
- createWorkItemDetailButton(id, title, type) {
- const button = document.createElement('button');
- button.addEventListener('click', () => {
- this.workItemId = id;
- this.track('viewed_work_item_from_modal', {
- category: 'workItems:show',
- label: 'work_item_view',
- property: `type_${type}`,
- });
- });
- button.classList.add('btn-link');
- button.innerText = title;
- return button;
- },
focusButton() {
this.$refs.convertButton[0].$el.focus();
},
@@ -287,6 +294,10 @@ export default {
<create-work-item
:is-modal="true"
:initial-title="activeTask.title"
+ :issue-gid="issueGid"
+ :lock-version="lockVersion"
+ :line-number-start="activeTask.lineNumberStart"
+ :line-number-end="activeTask.lineNumberEnd"
@closeModal="closeCreateTaskModal"
@onCreate="handleCreateTask"
/>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index c9af5d9b4a7..4a5ebf9615b 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -102,7 +102,7 @@ export function initIssueApp(issueData, store) {
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
- id: this.getNoteableData?.id,
+ issueId: this.getNoteableData?.id,
},
});
},
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 942677bb937..d5687d26499 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import workItemQuery from '../graphql/work_item.query.graphql';
import ItemTitle from './item_title.vue';
@@ -7,6 +7,7 @@ import ItemTitle from './item_title.vue';
export default {
components: {
GlModal,
+ GlLoadingIcon,
ItemTitle,
},
props: {
@@ -57,6 +58,7 @@ export default {
<template>
<gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="$emit('close')">
- <item-title class="gl-m-0!" :initial-title="workItemTitle" />
+ <gl-loading-icon v-if="$apollo.queries.workItem.loading" size="md" />
+ <item-title v-else class="gl-m-0!" :initial-title="workItemTitle" />
</gl-modal>
</template>
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
new file mode 100644
index 00000000000..b25210f5c74
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
@@ -0,0 +1,9 @@
+mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
+ workItemCreateFromTask(input: $input) {
+ workItem {
+ id
+ descriptionHtml
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index cc90cedb110..bbbeecbeaeb 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -1,21 +1,25 @@
<script>
-import { GlButton, GlAlert, GlLoadingIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
+import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue';
export default {
+ createErrorText: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
+ fetchTypesErrorText: s__(
+ 'WorkItem|Something went wrong when fetching work item types. Please try again',
+ ),
components: {
GlButton,
GlAlert,
GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
ItemTitle,
+ GlFormSelect,
},
inject: ['fullPath'],
props: {
@@ -29,6 +33,26 @@ export default {
required: false,
default: '',
},
+ issueGid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ lineNumberStart: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ lineNumberEnd: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -36,6 +60,7 @@ export default {
error: null,
workItemTypes: [],
selectedWorkItemType: null,
+ loading: false,
};
},
apollo: {
@@ -47,12 +72,13 @@ export default {
};
},
update(data) {
- return data.workspace?.workItemTypes?.nodes;
+ return data.workspace?.workItemTypes?.nodes.map((node) => ({
+ value: node.id,
+ text: node.name,
+ }));
},
error() {
- this.error = s__(
- 'WorkItem|Something went wrong when fetching work item types. Please try again',
- );
+ this.error = this.$options.fetchTypesErrorText;
},
},
},
@@ -60,9 +86,27 @@ export default {
dropdownButtonText() {
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
},
+ formOptions() {
+ return [
+ { value: null, text: s__('WorkItem|Please select work item type') },
+ ...this.workItemTypes,
+ ];
+ },
+ isButtonDisabled() {
+ return this.title.trim().length === 0 || !this.selectedWorkItemType;
+ },
},
methods: {
async createWorkItem() {
+ this.loading = true;
+ if (this.isModal) {
+ await this.createWorkItemFromTask();
+ } else {
+ await this.createStandaloneWorkItem();
+ }
+ this.loading = false;
+ },
+ async createStandaloneWorkItem() {
try {
const response = await this.$apollo.mutate({
mutation: createWorkItemMutation,
@@ -70,7 +114,7 @@ export default {
input: {
title: this.title,
projectPath: this.fullPath,
- workItemTypeId: this.selectedWorkItemType?.id,
+ workItemTypeId: this.selectedWorkItemType,
},
},
update(store, { data: { workItemCreate } }) {
@@ -96,23 +140,38 @@ export default {
});
},
});
-
const {
data: {
workItemCreate: {
- workItem: { id, type },
+ workItem: { id },
},
},
} = response;
- if (!this.isModal) {
- this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
- } else {
- this.$emit('onCreate', { id, title: this.title, type });
- }
+ this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
+ } catch {
+ this.error = this.$options.createErrorText;
+ }
+ },
+ async createWorkItemFromTask() {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: createWorkItemFromTaskMutation,
+ variables: {
+ input: {
+ id: this.issueGid,
+ workItemData: {
+ lockVersion: this.lockVersion,
+ title: this.title,
+ lineNumberStart: Number(this.lineNumberStart),
+ lineNumberEnd: Number(this.lineNumberEnd),
+ workItemTypeId: this.selectedWorkItemType,
+ },
+ },
+ },
+ });
+ this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
} catch {
- this.error = s__(
- 'WorkItem|Something went wrong when creating a work item. Please try again',
- );
+ this.error = this.$options.createErrorText;
}
},
handleTitleInput(title) {
@@ -125,9 +184,6 @@ export default {
}
this.$emit('closeModal');
},
- selectWorkItemType(type) {
- this.selectedWorkItemType = type;
- },
},
};
</script>
@@ -142,22 +198,17 @@ export default {
@title-input="handleTitleInput"
/>
<div>
- <gl-dropdown :text="dropdownButtonText">
- <gl-loading-icon
- v-if="$apollo.queries.workItemTypes.loading"
- size="md"
- data-testid="loading-types"
- />
- <template v-else>
- <gl-dropdown-item
- v-for="type in workItemTypes"
- :key="type.id"
- @click="selectWorkItemType(type)"
- >
- {{ type.name }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ <gl-loading-icon
+ v-if="$apollo.queries.workItemTypes.loading"
+ size="md"
+ data-testid="loading-types"
+ />
+ <gl-form-select
+ v-else
+ v-model="selectedWorkItemType"
+ :options="formOptions"
+ class="gl-max-w-26"
+ />
</div>
</div>
<div
@@ -166,8 +217,9 @@ export default {
>
<gl-button
variant="confirm"
- :disabled="title.length === 0"
+ :disabled="isButtonDisabled"
:class="{ 'gl-mr-3': !isModal }"
+ :loading="loading"
data-testid="create-button"
type="submit"
>
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 082993130a1..015dfc16df0 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -64,35 +64,50 @@ module Ci
def create_archive(artifacts)
return unless artifacts[:untracked] || artifacts[:paths]
- archive = {
- artifact_type: :archive,
- artifact_format: :zip,
- name: artifacts[:name],
- untracked: artifacts[:untracked],
- paths: artifacts[:paths],
- when: artifacts[:when],
- expire_in: artifacts[:expire_in]
- }
-
- if artifacts.dig(:exclude).present?
- archive.merge(exclude: artifacts[:exclude])
- else
- archive
+ BuildArtifact.for_archive(artifacts).to_h.tap do |artifact|
+ artifact.delete(:exclude) unless artifact[:exclude].present?
end
end
def create_reports(reports, expire_in:)
return unless reports&.any?
- reports.map do |report_type, report_paths|
- {
- artifact_type: report_type.to_sym,
- artifact_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.fetch(report_type.to_sym),
- name: ::Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(report_type.to_sym),
- paths: report_paths,
+ reports.map { |report| BuildArtifact.for_report(report, expire_in).to_h.compact }
+ end
+
+ BuildArtifact = Struct.new(:name, :untracked, :paths, :exclude, :when, :expire_in, :artifact_type, :artifact_format, keyword_init: true) do
+ def self.for_archive(artifacts)
+ self.new(
+ artifact_type: :archive,
+ artifact_format: :zip,
+ name: artifacts[:name],
+ untracked: artifacts[:untracked],
+ paths: artifacts[:paths],
+ when: artifacts[:when],
+ expire_in: artifacts[:expire_in],
+ exclude: artifacts[:exclude]
+ )
+ end
+
+ def self.for_report(report, expire_in)
+ type, params = report
+
+ if type == :coverage_report
+ artifact_type = params[:coverage_format].to_sym
+ paths = [params[:path]]
+ else
+ artifact_type = type
+ paths = params
+ end
+
+ self.new(
+ artifact_type: artifact_type,
+ artifact_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.fetch(artifact_type),
+ name: ::Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(artifact_type),
+ paths: paths,
when: 'always',
expire_in: expire_in
- }
+ )
end
end