summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipeline_wizard/components/step.vue
blob: c6f793e4cc53e790a7b6c81c8d4eccb121585a44 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<script>
import { GlAlert } from '@gitlab/ui';
import { isNode, isDocument, parseDocument, Document } from 'yaml';
import { merge } from '~/lib/utils/yaml';
import { s__ } from '~/locale';
import { logError } from '~/lib/logger';
import InputWrapper from './input.vue';
import StepNav from './step_nav.vue';

export default {
  name: 'PipelineWizardStep',
  i18n: {
    errors: {
      cloneErrorUserMessage: s__(
        'PipelineWizard|There was an unexpected error trying to set up the template. The error has been logged.',
      ),
    },
  },
  components: {
    StepNav,
    InputWrapper,
    GlAlert,
  },
  props: {
    // As the inputs prop we expect to receive an array of instructions
    // on how to display the input fields that will be used to obtain the
    // user's input. Each input instruction needs a target prop, specifying
    // the placeholder in the template that will be replaced by the user's
    // input. The selected widget may require additional validation for the
    // input object.
    inputs: {
      type: Array,
      required: true,
      validator: (value) =>
        value.every((i) => {
          return i?.target && i?.widget;
        }),
    },
    template: {
      type: null,
      required: true,
      validator: (v) => isNode(v),
    },
    hasPreviousStep: {
      type: Boolean,
      required: false,
      default: false,
    },
    compiled: {
      type: Object,
      required: true,
      validator: (v) => isDocument(v),
    },
  },
  data() {
    return {
      wasCompiled: false,
      validate: false,
      inputValidStates: Array(this.inputs.length).fill(null),
      error: null,
    };
  },
  computed: {
    inputValidStatesThatAreNotNull() {
      return this.inputValidStates?.filter((s) => s !== null);
    },
    areAllInputValidStatesNull() {
      return !this.inputValidStatesThatAreNotNull?.length;
    },
    isValid() {
      return this.areAllInputValidStatesNull || this.inputValidStatesThatAreNotNull.every((s) => s);
    },
  },
  methods: {
    forceClone(yamlNode) {
      try {
        // document.clone() will only clone the root document object,
        // but the references to the child nodes inside will be retained.
        // So in order to ensure a full clone, we need to stringify
        // and parse until there's a better implementation in the
        // yaml package.
        return parseDocument(new Document(yamlNode).toString());
      } catch (e) {
        // eslint-disable-next-line @gitlab/require-i18n-strings
        logError('An unexpected error occurred while trying to clone a template', e);
        this.error = this.$options.i18n.errors.cloneErrorUserMessage;
        return null;
      }
    },
    compile() {
      if (this.wasCompiled) return;
      // NOTE: This modifies this.compiled without triggering reactivity.
      // this is done on purpose, see
      // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81412#note_862972703
      // for more information
      merge(this.compiled, this.forceClone(this.template));
      this.wasCompiled = true;
    },
    onUpdate(c) {
      this.$emit('update:compiled', c);
    },
    onPrevClick() {
      this.$emit('back');
    },
    async onNextClick() {
      this.validate = true;
      await this.$nextTick();
      if (this.isValid) {
        this.$emit('next');
      }
    },
    onInputValidationStateChange(inputId, value) {
      this.$set(this.inputValidStates, inputId, value);
    },
    onHighlight(path) {
      this.$emit('update:highlight', path);
    },
  },
};
</script>
<template>
  <div>
    <gl-alert v-if="error" class="gl-mb-4" variant="danger">
      {{ error }}
    </gl-alert>
    <input-wrapper
      v-for="(input, i) in inputs"
      :key="input.target"
      :compiled="compiled"
      :target="input.target"
      :template="template"
      :validate="validate"
      :widget="input.widget"
      class="gl-mb-2"
      v-bind="input"
      @highlight="onHighlight"
      @update:valid="(validationState) => onInputValidationStateChange(i, validationState)"
      @update:compiled="onUpdate"
      @beforeUpdate:compiled.once="compile"
    />
    <step-nav
      :next-button-enabled="isValid"
      :show-back-button="hasPreviousStep"
      show-next-button
      @back="onPrevClick"
      @next="onNextClick"
    />
  </div>
</template>