diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-21 00:09:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-21 00:09:12 +0300 |
commit | 8585e4137fbd499c9fbcb465727d6c54437f4345 (patch) | |
tree | 7a0930d7f23a6e38587d992273d99b01f59e8bfd /app/assets/javascripts | |
parent | 4e5a71c197c67446f64ca11fc903403d3ae52983 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
9 files changed, 369 insertions, 45 deletions
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js index 28a125b2b8f..122f23a5bb5 100644 --- a/app/assets/javascripts/jobs/utils.js +++ b/app/assets/javascripts/jobs/utils.js @@ -1,4 +1,12 @@ -// capture anything starting with http:// or https:// -// up until a disallowed character or whitespace -export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+)/g; +/** + * capture anything starting with http:// or https:// + * https?:\/\/ + * + * up until a disallowed character or whitespace + * [^"<>\\^`{|}\s]+ + * + * and a disallowed character or whitespace, including non-ending chars .,:;!? + * [^"<>\\^`{|}\s.,:;!?] + */ +export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g; export default { linkRegex }; diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js index 6a0e92bff2d..e14696e0d1c 100644 --- a/app/assets/javascripts/packages/list/constants.js +++ b/app/assets/javascripts/packages/list/constants.js @@ -68,6 +68,10 @@ export const PACKAGE_REGISTRY_TABS = [ title: s__('PackageRegistry|Conan'), type: PackageType.CONAN, }, + { + title: s__('PackageRegistry|Generic'), + type: PackageType.GENERIC, + }, { title: s__('PackageRegistry|Maven'), diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js index c481abd8658..c0f7f150337 100644 --- a/app/assets/javascripts/packages/shared/constants.js +++ b/app/assets/javascripts/packages/shared/constants.js @@ -7,6 +7,7 @@ export const PackageType = { NUGET: 'nuget', PYPI: 'pypi', COMPOSER: 'composer', + GENERIC: 'generic', }; export const TrackingActions = { diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js index b0807558266..d7a883e4397 100644 --- a/app/assets/javascripts/packages/shared/utils.js +++ b/app/assets/javascripts/packages/shared/utils.js @@ -21,7 +21,8 @@ export const getPackageTypeLabel = packageType => { return s__('PackageType|PyPI'); case PackageType.COMPOSER: return s__('PackageType|Composer'); - + case PackageType.GENERIC: + return s__('PackageType|Generic'); default: return null; } diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue new file mode 100644 index 00000000000..9279273283e --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue @@ -0,0 +1,139 @@ +<script> +import { + GlButton, + GlForm, + GlFormCheckbox, + GlFormInput, + GlFormGroup, + GlFormTextarea, + GlSprintf, +} from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlButton, + GlForm, + GlFormCheckbox, + GlFormInput, + GlFormGroup, + GlFormTextarea, + GlSprintf, + }, + props: { + defaultBranch: { + type: String, + required: false, + default: '', + }, + defaultMessage: { + type: String, + required: false, + default: '', + }, + isSaving: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + message: this.defaultMessage, + branch: this.defaultBranch, + openMergeRequest: false, + }; + }, + computed: { + isDefaultBranch() { + return this.branch === this.defaultBranch; + }, + submitDisabled() { + return !(this.message && this.branch); + }, + }, + methods: { + onSubmit() { + this.$emit('submit', { + message: this.message, + branch: this.branch, + openMergeRequest: this.openMergeRequest, + }); + }, + onReset() { + this.$emit('cancel'); + }, + }, + i18n: { + commitMessage: __('Commit message'), + targetBranch: __('Target Branch'), + startMergeRequest: __('Start a %{new_merge_request} with these changes'), + newMergeRequest: __('new merge request'), + commitChanges: __('Commit changes'), + cancel: __('Cancel'), + }, +}; +</script> + +<template> + <div> + <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset"> + <gl-form-group + id="commit-group" + :label="$options.i18n.commitMessage" + label-cols-sm="2" + label-for="commit-message" + > + <gl-form-textarea + id="commit-message" + v-model="message" + class="gl-font-monospace!" + required + :placeholder="defaultMessage" + /> + </gl-form-group> + <gl-form-group + id="target-branch-group" + :label="$options.i18n.targetBranch" + label-cols-sm="2" + label-for="target-branch-field" + > + <gl-form-input + id="target-branch-field" + v-model="branch" + class="gl-font-monospace!" + required + /> + <gl-form-checkbox + v-if="!isDefaultBranch" + v-model="openMergeRequest" + data-testid="new-mr-checkbox" + class="gl-mt-3" + > + <gl-sprintf :message="$options.i18n.startMergeRequest"> + <template #new_merge_request> + <strong>{{ $options.i18n.newMergeRequest }}</strong> + </template> + </gl-sprintf> + </gl-form-checkbox> + </gl-form-group> + <div + class="gl-display-flex gl-justify-content-space-between gl-p-5 gl-bg-gray-10 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1" + > + <gl-button + type="submit" + class="js-no-auto-disable" + category="primary" + variant="success" + :disabled="submitDisabled" + :loading="isSaving" + > + {{ $options.i18n.commitChanges }} + </gl-button> + <gl-button type="reset" category="secondary" class="gl-mr-3"> + {{ $options.i18n.cancel }} + </gl-button> + </div> + </gl-form> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue index a925077c906..22f2a32c9ac 100644 --- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue +++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue @@ -5,22 +5,10 @@ export default { components: { EditorLite, }, - props: { - value: { - type: String, - required: false, - default: '', - }, - }, }; </script> <template> <div class="gl-border-solid gl-border-gray-100 gl-border-1"> - <editor-lite - v-model="value" - file-name="*.yml" - :editor-options="{ readOnly: true }" - @editor-ready="$emit('editor-ready')" - /> + <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" /> </div> </template> diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql new file mode 100644 index 00000000000..11bca42fd69 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql @@ -0,0 +1,26 @@ +mutation commitCIFileMutation( + $projectPath: ID! + $branch: String! + $startBranch: String + $message: String! + $filePath: String! + $lastCommitId: String! + $content: String +) { + commitCreate( + input: { + projectPath: $projectPath + branch: $branch + startBranch: $startBranch + message: $message + actions: [ + { action: UPDATE, filePath: $filePath, lastCommitId: $lastCommitId, content: $content } + ] + } + ) { + commit { + id + } + errors + } +} diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js index ccd7b74064f..8268a907a29 100644 --- a/app/assets/javascripts/pipeline_editor/index.js +++ b/app/assets/javascripts/pipeline_editor/index.js @@ -10,7 +10,11 @@ import PipelineEditorApp from './pipeline_editor_app.vue'; export const initPipelineEditor = (selector = '#js-pipeline-editor') => { const el = document.querySelector(selector); - const { projectPath, defaultBranch, ciConfigPath } = el?.dataset; + if (!el) { + return null; + } + + const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset; Vue.use(VueApollo); @@ -24,9 +28,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { render(h) { return h(PipelineEditorApp, { props: { - projectPath, - defaultBranch, ciConfigPath, + commitId, + defaultBranch, + newMergeRequestPath, + projectPath, }, }); }, diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index 50b946af456..59635296de4 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -1,20 +1,33 @@ <script> -import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; +import { redirectTo, mergeUrlParams, refreshCurrentPage } from '~/lib/utils/url_utility'; -import TextEditor from './components/text_editor.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; +import CommitForm from './components/commit/commit_form.vue'; +import TextEditor from './components/text_editor.vue'; +import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql'; import getBlobContent from './graphql/queries/blob_content.graphql'; +const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; +const MR_TARGET_BRANCH = 'merge_request[target_branch]'; + +const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF'; +const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE'; +const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; +const COMMIT_FAILURE = 'COMMIT_FAILURE'; +const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; + export default { components: { - GlLoadingIcon, GlAlert, - GlTabs, + GlLoadingIcon, GlTab, - TextEditor, + GlTabs, PipelineGraph, + CommitForm, + TextEditor, }, props: { projectPath: { @@ -26,16 +39,30 @@ export default { required: false, default: null, }, + commitId: { + type: String, + required: false, + default: null, + }, ciConfigPath: { type: String, required: true, }, + newMergeRequestPath: { + type: String, + required: true, + }, }, data() { return { - error: null, - content: '', + showFailureAlert: false, + failureType: null, + failureReasons: [], + + isSaving: false, editorIsReady: false, + content: '', + contentModel: '', }; }, apollo: { @@ -51,51 +78,168 @@ export default { update(data) { return data?.blobContent?.rawData; }, + result({ data }) { + this.contentModel = data?.blobContent?.rawData ?? ''; + }, error(error) { - this.error = error; + this.handleBlobContentError(error); }, }, }, computed: { - loading() { + isLoading() { return this.$apollo.queries.content.loading; }, - errorMessage() { - const { message: generalReason, networkError } = this.error ?? {}; - - const { data } = networkError?.response ?? {}; - // 404 for missing file uses `message` - // 400 for a missing ref uses `error` - const networkReason = data?.message ?? data?.error; - - const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError; - return sprintf(this.$options.i18n.errorMessageWithReason, { reason }); + defaultCommitMessage() { + return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath }); }, pipelineData() { // Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141 return {}; }, + failure() { + switch (this.failureType) { + case LOAD_FAILURE_NO_REF: + return { + text: this.$options.errorTexts[LOAD_FAILURE_NO_REF], + variant: 'danger', + }; + case LOAD_FAILURE_NO_FILE: + return { + text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE], + variant: 'danger', + }; + case LOAD_FAILURE_UNKNOWN: + return { + text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN], + variant: 'danger', + }; + case COMMIT_FAILURE: + return { + text: this.$options.errorTexts[COMMIT_FAILURE], + variant: 'danger', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT_FAILURE], + variant: 'danger', + }; + } + }, }, i18n: { - unknownError: __('Unknown Error'), - errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'), + defaultCommitMessage: __('Update %{sourcePath} file'), tabEdit: s__('Pipelines|Write pipeline configuration'), tabGraph: s__('Pipelines|Visualize'), }, + errorTexts: { + [LOAD_FAILURE_NO_REF]: s__( + 'Pipelines|Repository does not have a default branch, please set one.', + ), + [LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'), + [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'), + [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), + }, + methods: { + handleBlobContentError(error = {}) { + const { networkError } = error; + + const { response } = networkError; + if (response?.status === 404) { + // 404 for missing CI file + this.reportFailure(LOAD_FAILURE_NO_FILE); + } else if (response?.status === 400) { + // 400 for a missing ref when no default branch is set + this.reportFailure(LOAD_FAILURE_NO_REF); + } else { + this.reportFailure(LOAD_FAILURE_UNKNOWN); + } + }, + dismissFailure() { + this.showFailureAlert = false; + }, + reportFailure(type, reasons = []) { + this.showFailureAlert = true; + this.failureType = type; + this.failureReasons = reasons; + }, + redirectToNewMergeRequest(sourceBranch) { + const url = mergeUrlParams( + { + [MR_SOURCE_BRANCH]: sourceBranch, + [MR_TARGET_BRANCH]: this.defaultBranch, + }, + this.newMergeRequestPath, + ); + redirectTo(url); + }, + async onCommitSubmit(event) { + this.isSaving = true; + const { message, branch, openMergeRequest } = event; + + try { + const { + data: { + commitCreate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: commitCiFileMutation, + variables: { + projectPath: this.projectPath, + branch, + startBranch: this.defaultBranch, + message, + filePath: this.ciConfigPath, + content: this.contentModel, + lastCommitId: this.commitId, + }, + }); + + if (errors?.length) { + this.reportFailure(COMMIT_FAILURE, errors); + return; + } + + if (openMergeRequest) { + this.redirectToNewMergeRequest(branch); + } else { + // Refresh the page to ensure commit is updated + refreshCurrentPage(); + } + } catch (error) { + this.reportFailure(COMMIT_FAILURE, [error?.message]); + } finally { + this.isSaving = false; + } + }, + onCommitCancel() { + this.contentModel = this.content; + }, + }, }; </script> <template> <div class="gl-mt-4"> - <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert> + <gl-alert + v-if="showFailureAlert" + :variant="failure.variant" + :dismissible="true" + @dismiss="dismissFailure" + > + {{ failure.text }} + <ul v-if="failureReasons.length" class="gl-mb-0"> + <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li> + </ul> + </gl-alert> <div class="gl-mt-4"> - <gl-loading-icon v-if="loading" size="lg" /> - <div v-else class="file-editor"> + <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" /> + <div v-else class="file-editor gl-mb-3"> <gl-tabs> <!-- editor should be mounted when its tab is visible, so the container has a size --> <gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady"> <!-- editor should be mounted only once, when the tab is displayed --> - <text-editor v-model="content" @editor-ready="editorIsReady = true" /> + <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" /> </gl-tab> <gl-tab :title="$options.i18n.tabGraph"> @@ -103,6 +247,13 @@ export default { </gl-tab> </gl-tabs> </div> + <commit-form + :default-branch="defaultBranch" + :default-message="defaultCommitMessage" + :is-saving="isSaving" + @cancel="onCommitCancel" + @submit="onCommitSubmit" + /> </div> </div> </template> |