diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 12:45:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 12:45:46 +0300 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/pipeline_wizard | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipeline_wizard')
6 files changed, 519 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipeline_wizard/components/commit.vue b/app/assets/javascripts/pipeline_wizard/components/commit.vue new file mode 100644 index 00000000000..518b41c66b1 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/commit.vue @@ -0,0 +1,224 @@ +<script> +import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import { __, s__, sprintf } from '~/locale'; +import createCommitMutation from '../queries/create_commit.graphql'; +import getFileMetaDataQuery from '../queries/get_file_meta.graphql'; +import StepNav from './step_nav.vue'; + +export const i18n = { + updateFileHeading: s__('PipelineWizard|Commit changes to your file'), + createFileHeading: s__('PipelineWizard|Commit your new file'), + fieldRequiredFeedback: __('This field is required'), + commitMessageLabel: s__('PipelineWizard|Commit Message'), + branchSelectorLabel: s__('PipelineWizard|Commit file to Branch'), + defaultUpdateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Update %{filename}'), + defaultCreateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Add %{filename}'), + commitButtonLabel: s__('PipelineWizard|Commit'), + commitSuccessMessage: s__('PipelineWizard|The file has been committed.'), + errors: { + loadError: s__( + 'PipelineWizard|There was a problem while checking whether your file already exists in the specified branch.', + ), + commitError: s__('PipelineWizard|There was a problem committing the changes.'), + }, +}; + +const COMMIT_ACTION = { + CREATE: 'CREATE', + UPDATE: 'UPDATE', +}; + +export default { + i18n, + name: 'PipelineWizardCommitStep', + components: { + RefSelector, + GlAlert, + GlButton, + GlForm, + GlFormGroup, + GlFormTextarea, + StepNav, + }, + props: { + prev: { + type: Object, + required: false, + default: null, + }, + projectPath: { + type: String, + required: true, + }, + defaultBranch: { + type: String, + required: true, + }, + fileContent: { + type: String, + required: false, + default: '', + }, + filename: { + type: String, + required: true, + }, + }, + data() { + return { + branch: this.defaultBranch, + loading: false, + loadError: null, + commitError: null, + message: null, + }; + }, + computed: { + fileExistsInRepo() { + return this.project?.repository?.blobs.nodes.length > 0; + }, + commitAction() { + return this.fileExistsInRepo ? COMMIT_ACTION.UPDATE : COMMIT_ACTION.CREATE; + }, + defaultMessage() { + return sprintf( + this.fileExistsInRepo + ? this.$options.i18n.defaultUpdateCommitMessage + : this.$options.i18n.defaultCreateCommitMessage, + { filename: this.filename }, + ); + }, + isCommitButtonEnabled() { + return this.fileExistsCheckInProgress; + }, + fileExistsCheckInProgress() { + return this.$apollo.queries.project.loading; + }, + mutationPayload() { + return { + mutation: createCommitMutation, + variables: { + input: { + projectPath: this.projectPath, + branch: this.branch, + message: this.message || this.defaultMessage, + actions: [ + { + action: this.commitAction, + filePath: `/${this.filename}`, + content: this.fileContent, + }, + ], + }, + }, + }; + }, + }, + apollo: { + project: { + query: getFileMetaDataQuery, + variables() { + this.loadError = null; + return { + fullPath: this.projectPath, + filePath: this.filename, + ref: this.branch, + }; + }, + error() { + this.loadError = this.$options.i18n.errors.loadError; + }, + }, + }, + methods: { + async commit() { + this.loading = true; + try { + const { data } = await this.$apollo.mutate(this.mutationPayload); + const hasError = Boolean(data.commitCreate.errors?.length); + if (hasError) { + this.commitError = this.$options.i18n.errors.commitError; + } else { + this.handleCommitSuccess(); + } + } catch (e) { + this.commitError = this.$options.i18n.errors.commitError; + } finally { + this.loading = false; + } + }, + handleCommitSuccess() { + this.$toast.show(this.$options.i18n.commitSuccessMessage); + this.$emit('done'); + }, + }, +}; +</script> + +<template> + <div> + <h4 v-if="fileExistsInRepo" key="create-heading"> + {{ $options.i18n.updateFileHeading }} + </h4> + <h4 v-else key="update-heading"> + {{ $options.i18n.createFileHeading }} + </h4> + <gl-alert + v-if="!!loadError" + :dismissible="false" + class="gl-mb-5" + data-testid="load-error" + variant="danger" + > + {{ loadError }} + </gl-alert> + <gl-form class="gl-max-w-48"> + <gl-form-group + :invalid-feedback="$options.i18n.fieldRequiredFeedback" + :label="$options.i18n.commitMessageLabel" + data-testid="commit_message_group" + label-for="commit_message" + > + <gl-form-textarea + id="commit_message" + v-model="message" + :placeholder="defaultMessage" + data-testid="commit_message" + size="md" + @input="(v) => $emit('update:message', v)" + /> + </gl-form-group> + <gl-form-group + :invalid-feedback="$options.i18n.fieldRequiredFeedback" + :label="$options.i18n.branchSelectorLabel" + data-testid="branch_selector_group" + label-for="branch" + > + <ref-selector id="branch" v-model="branch" data-testid="branch" :project-id="projectPath" /> + </gl-form-group> + <gl-alert + v-if="!!commitError" + :dismissible="false" + class="gl-mb-5" + data-testid="commit-error" + variant="danger" + > + {{ commitError }} + </gl-alert> + <step-nav show-back-button v-bind="$props" @back="$emit('go-back')"> + <template #after> + <gl-button + :disabled="isCommitButtonEnabled" + :loading="fileExistsCheckInProgress || loading" + category="primary" + variant="confirm" + @click="commit" + > + {{ $options.i18n.commitButtonLabel }} + </gl-button> + </template> + </step-nav> + </gl-form> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_wizard/components/editor.vue b/app/assets/javascripts/pipeline_wizard/components/editor.vue new file mode 100644 index 00000000000..41611233f71 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/editor.vue @@ -0,0 +1,94 @@ +<script> +import { debounce } from 'lodash'; +import { isDocument } from 'yaml'; +import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants'; +import SourceEditor from '~/editor/source_editor'; +import { YamlEditorExtension } from '~/editor/extensions/source_editor_yaml_ext'; +import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; + +export default { + name: 'YamlEditor', + props: { + doc: { + type: Object, + required: true, + validator: (d) => isDocument(d), + }, + highlight: { + type: [String, Array], + required: false, + default: null, + }, + filename: { + type: String, + required: true, + }, + }, + data() { + return { + editor: null, + isUpdating: false, + yamlEditorExtension: null, + }; + }, + watch: { + doc: { + handler() { + this.updateEditorContent(); + }, + deep: true, + }, + highlight(v) { + this.requestHighlight(v); + }, + }, + mounted() { + this.editor = new SourceEditor().createInstance({ + el: this.$el, + blobPath: this.filename, + language: 'yaml', + }); + [, this.yamlEditorExtension] = this.editor.use([ + { definition: SourceEditorExtension }, + { + definition: YamlEditorExtension, + setupOptions: { + highlightPath: this.highlight, + }, + }, + ]); + this.editor.onDidChangeModelContent( + debounce(() => this.handleChange(), CONTENT_UPDATE_DEBOUNCE), + ); + this.updateEditorContent(); + this.emitValue(); + }, + methods: { + async updateEditorContent() { + this.isUpdating = true; + this.editor.setDoc(this.doc); + this.isUpdating = false; + this.requestHighlight(this.highlight); + }, + handleChange() { + this.emitValue(); + if (!this.isUpdating) { + this.handleTouch(); + } + }, + emitValue() { + this.$emit('update:yaml', this.editor.getValue()); + }, + handleTouch() { + this.$emit('touch'); + }, + requestHighlight(path) { + this.editor.highlight(path, true); + }, + }, +}; +</script> + +<template> + <div id="source-editor-yaml-editor"></div> +</template> diff --git a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue new file mode 100644 index 00000000000..8f9198855c6 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue @@ -0,0 +1,54 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + name: 'StepNav', + components: { + GlButton, + }, + props: { + showBackButton: { + type: Boolean, + required: false, + default: false, + }, + showNextButton: { + type: Boolean, + required: false, + default: false, + }, + nextButtonEnabled: { + type: Boolean, + required: false, + default: true, + }, + }, +}; +</script> + +<template> + <div> + <slot name="before"></slot> + <gl-button + v-if="showBackButton" + category="secondary" + data-testid="back-button" + @click="$emit('back')" + > + {{ __('Back') }} + </gl-button> + <gl-button + v-if="showNextButton" + :disabled="!nextButtonEnabled" + category="primary" + data-testid="next-button" + variant="confirm" + @click="$emit('next')" + > + {{ __('Next') }} + </gl-button> + <slot name="after"></slot> + </div> +</template> + +<style scoped></style> diff --git a/app/assets/javascripts/pipeline_wizard/components/widgets/text.vue b/app/assets/javascripts/pipeline_wizard/components/widgets/text.vue new file mode 100644 index 00000000000..26235b20ce9 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/widgets/text.vue @@ -0,0 +1,126 @@ +<script> +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; +import { s__ } from '~/locale'; + +const VALIDATION_STATE = { + NO_VALIDATION: null, + INVALID: false, + VALID: true, +}; + +export default { + name: 'TextWidget', + components: { + GlFormGroup, + GlFormInput, + }, + props: { + label: { + type: String, + required: true, + }, + description: { + type: String, + required: false, + default: null, + }, + placeholder: { + type: String, + required: false, + default: null, + }, + invalidFeedback: { + type: String, + required: false, + default: s__('PipelineWizardInputValidation|This value is not valid'), + }, + id: { + type: String, + required: false, + default: () => uniqueId('textWidget-'), + }, + pattern: { + type: String, + required: false, + default: null, + }, + validate: { + type: Boolean, + required: false, + default: false, + }, + required: { + type: Boolean, + required: false, + default: false, + }, + default: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + touched: false, + value: this.default, + }; + }, + computed: { + validationState() { + if (!this.showValidationState) return VALIDATION_STATE.NO_VALIDATION; + if (this.isRequiredButEmpty) return VALIDATION_STATE.INVALID; + return this.needsValidationAndPasses ? VALIDATION_STATE.VALID : VALIDATION_STATE.INVALID; + }, + showValidationState() { + return this.touched || this.validate; + }, + isRequiredButEmpty() { + return this.required && !this.value; + }, + needsValidationAndPasses() { + return !this.pattern || new RegExp(this.pattern).test(this.value); + }, + invalidFeedbackMessage() { + return this.isRequiredButEmpty + ? s__('PipelineWizardInputValidation|This field is required') + : this.invalidFeedback; + }, + }, + watch: { + validationState(v) { + this.$emit('update:valid', v); + }, + value(v) { + this.$emit('input', v.trim()); + }, + }, + created() { + if (this.default) { + this.$emit('input', this.value); + } + }, +}; +</script> + +<template> + <div data-testid="text-widget"> + <gl-form-group + :description="description" + :invalid-feedback="invalidFeedbackMessage" + :label="label" + :label-for="id" + :state="validationState" + > + <gl-form-input + :id="id" + v-model="value" + :placeholder="placeholder" + :state="validationState" + type="text" + @blur="touched = true" + /> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql b/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql new file mode 100644 index 00000000000..9abf8eff587 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql @@ -0,0 +1,9 @@ +mutation CreateCommit($input: CommitCreateInput!) { + commitCreate(input: $input) { + commit { + id + } + content + errors + } +} diff --git a/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql b/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql new file mode 100644 index 00000000000..87f014fade6 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql @@ -0,0 +1,12 @@ +query GetFileMetadata($fullPath: ID!, $filePath: String!, $ref: String) { + project(fullPath: $fullPath) { + id + repository { + blobs(paths: [$filePath], ref: $ref) { + nodes { + id + } + } + } + } +} |