app.vue 8.89 KB
Newer Older
1
<script>
2
import Visibility from 'visibilityjs';
3 4
import { __, s__, sprintf } from '~/locale';
import createFlash from '~/flash';
5 6 7 8 9 10 11 12 13 14
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
15

16 17 18 19 20 21 22 23 24 25 26 27
export default {
  components: {
    descriptionComponent,
    titleComponent,
    editedComponent,
    formComponent,
  },
  mixins: [recaptchaModalImplementor],
  props: {
    endpoint: {
      required: true,
      type: String,
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
    updateEndpoint: {
      required: true,
      type: String,
    },
    canUpdate: {
      required: true,
      type: Boolean,
    },
    canDestroy: {
      required: true,
      type: Boolean,
    },
    showInlineEditButton: {
      type: Boolean,
      required: false,
      default: true,
    },
    showDeleteButton: {
      type: Boolean,
      required: false,
      default: true,
    },
    enableAutocomplete: {
      type: Boolean,
      required: false,
      default: true,
    },
    issuableRef: {
      type: String,
      required: true,
    },
    initialTitleHtml: {
      type: String,
      required: true,
    },
    initialTitleText: {
      type: String,
      required: true,
    },
    initialDescriptionHtml: {
      type: String,
      required: false,
      default: '',
    },
    initialDescriptionText: {
      type: String,
      required: false,
      default: '',
    },
    initialTaskStatus: {
      type: String,
      required: false,
      default: '',
    },
    updatedAt: {
      type: String,
      required: false,
      default: '',
    },
    updatedByName: {
      type: String,
      required: false,
      default: '',
    },
    updatedByPath: {
      type: String,
      required: false,
      default: '',
    },
    issuableTemplates: {
      type: Array,
      required: false,
      default: () => [],
    },
    markdownPreviewPath: {
      type: String,
      required: true,
    },
    markdownDocsPath: {
      type: String,
      required: true,
    },
    projectPath: {
      type: String,
      required: true,
    },
    projectNamespace: {
      type: String,
      required: true,
    },
    issuableType: {
      type: String,
      required: false,
      default: 'issue',
    },
    canAttachFile: {
      type: Boolean,
      required: false,
      default: true,
    },
129 130
    lockVersion: {
      type: Number,
131 132
      required: false,
      default: 0,
133
    },
134 135 136 137 138 139 140 141 142 143 144
  },
  data() {
    const store = new Store({
      titleHtml: this.initialTitleHtml,
      titleText: this.initialTitleText,
      descriptionHtml: this.initialDescriptionHtml,
      descriptionText: this.initialDescriptionText,
      updatedAt: this.updatedAt,
      updatedByName: this.updatedByName,
      updatedByPath: this.updatedByPath,
      taskStatus: this.initialTaskStatus,
145
      lock_version: this.lockVersion,
146
    });
147

148 149 150 151 152 153 154 155 156
    return {
      store,
      state: store.state,
      showForm: false,
    };
  },
  computed: {
    formState() {
      return this.store.formState;
Luke Bennett's avatar
Luke Bennett committed
157
    },
158 159 160 161
    hasUpdated() {
      return !!this.state.updatedAt;
    },
    issueChanged() {
Rajat Jain's avatar
Rajat Jain committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
      const {
        store: {
          formState: { description, title },
        },
        initialDescriptionText,
        initialTitleText,
      } = this;

      if (initialDescriptionText || description) {
        return initialDescriptionText !== description;
      }

      if (initialTitleText || title) {
        return initialTitleText !== title;
      }

      return false;
179
    },
180
    defaultErrorMessage() {
181
      return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
182
    },
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
  },
  created() {
    this.service = new Service(this.endpoint);
    this.poll = new Poll({
      resource: this.service,
      method: 'getData',
      successCallback: res => this.store.updateState(res.data),
      errorCallback(err) {
        throw new Error(err);
      },
    });

    if (!Visibility.hidden()) {
      this.poll.makeRequest();
    }
198

199
    Visibility.change(() => {
200
      if (!Visibility.hidden()) {
201 202 203
        this.poll.restart();
      } else {
        this.poll.stop();
204
      }
205
    });
206

207
    window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
208

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    eventHub.$on('delete.issuable', this.deleteIssuable);
    eventHub.$on('update.issuable', this.updateIssuable);
    eventHub.$on('close.form', this.closeForm);
    eventHub.$on('open.form', this.openForm);
  },
  beforeDestroy() {
    eventHub.$off('delete.issuable', this.deleteIssuable);
    eventHub.$off('update.issuable', this.updateIssuable);
    eventHub.$off('close.form', this.closeForm);
    eventHub.$off('open.form', this.openForm);
    window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
  },
  methods: {
    handleBeforeUnloadEvent(e) {
      const event = e;
224 225
      if (this.showForm && this.issueChanged && !this.showRecaptcha) {
        event.returnValue = __('Are you sure you want to lose your issue information?');
226 227 228
      }
      return undefined;
    },
229
    updateStoreState() {
230
      return this.service
231
        .getData()
232 233 234
        .then(res => res.data)
        .then(data => {
          this.store.updateState(data);
235 236
        })
        .catch(() => {
237
          createFlash(this.defaultErrorMessage);
238 239 240
        });
    },

241 242 243 244 245 246
    openForm() {
      if (!this.showForm) {
        this.showForm = true;
        this.store.setFormState({
          title: this.state.titleText,
          description: this.state.descriptionText,
247
          lock_version: this.state.lock_version,
248 249 250 251 252 253 254 255
          lockedWarningVisible: false,
          updateLoading: false,
        });
      }
    },
    closeForm() {
      this.showForm = false;
    },
256

257 258 259 260 261 262 263 264 265 266
    updateIssuable() {
      return this.service
        .updateIssuable(this.store.formState)
        .then(res => res.data)
        .then(data => this.checkForSpam(data))
        .then(data => {
          if (window.location.pathname !== data.web_url) {
            visitUrl(data.web_url);
          }
        })
267 268
        .then(this.updateStoreState)
        .then(() => {
269 270
          eventHub.$emit('close.form');
        })
271 272 273 274
        .catch((error = {}) => {
          const { name, response = {} } = error;

          if (name === 'SpamError') {
275 276
            this.openRecaptcha();
          } else {
277
            let errMsg = this.defaultErrorMessage;
Fatih Acet's avatar
Fatih Acet committed
278

279 280
            if (response.data && response.data.errors) {
              errMsg += `. ${response.data.errors.join(' ')}`;
Fatih Acet's avatar
Fatih Acet committed
281 282
            }

283
            createFlash(errMsg);
284
          }
285
        });
286
    },
287

288 289 290 291
    closeRecaptchaModal() {
      this.store.setFormState({
        updateLoading: false,
      });
292

293 294
      this.closeRecaptcha();
    },
295

296 297 298 299 300 301 302 303 304 305 306
    deleteIssuable() {
      this.service
        .deleteIssuable()
        .then(res => res.data)
        .then(data => {
          // Stop the poll so we don't get 404's with the issuable not existing
          this.poll.stop();

          visitUrl(data.web_url);
        })
        .catch(() => {
307 308 309
          createFlash(
            sprintf(s__('Error deleting  %{issuableType}'), { issuableType: this.issuableType }),
          );
310
        });
311
    },
312 313
  },
};
314 315 316
</script>

<template>
317 318 319 320 321 322 323 324 325 326 327 328 329
  <div>
    <div v-if="canUpdate && showForm">
      <form-component
        :form-state="formState"
        :can-destroy="canDestroy"
        :issuable-templates="issuableTemplates"
        :markdown-docs-path="markdownDocsPath"
        :markdown-preview-path="markdownPreviewPath"
        :project-path="projectPath"
        :project-namespace="projectNamespace"
        :show-delete-button="showDeleteButton"
        :can-attach-file="canAttachFile"
        :enable-autocomplete="enableAutocomplete"
330
        :issuable-type="issuableType"
331
      />
332

Mike Greiling's avatar
Mike Greiling committed
333
      <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptchaModal" />
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
    </div>
    <div v-else>
      <title-component
        :issuable-ref="issuableRef"
        :can-update="canUpdate"
        :title-html="state.titleHtml"
        :title-text="state.titleText"
        :show-inline-edit-button="showInlineEditButton"
      />
      <description-component
        v-if="state.descriptionHtml"
        :can-update="canUpdate"
        :description-html="state.descriptionHtml"
        :description-text="state.descriptionText"
        :updated-at="state.updatedAt"
        :task-status="state.taskStatus"
        :issuable-type="issuableType"
        :update-url="updateEndpoint"
Brett Walker's avatar
Brett Walker committed
352
        :lock-version="state.lock_version"
353
        @taskListUpdateFailed="updateStoreState"
354 355 356 357 358 359 360 361
      />
      <edited-component
        v-if="hasUpdated"
        :updated-at="state.updatedAt"
        :updated-by-name="state.updatedByName"
        :updated-by-path="state.updatedByPath"
      />
    </div>
362 363
  </div>
</template>