Merge branch 'refactor-realtime-issue' into 'master'
Refactored issue tealtime elements See merge request !11242
This commit is contained in:
commit
20987f4fd2
17 changed files with 543 additions and 262 deletions
|
@ -1,27 +0,0 @@
|
|||
export default (newStateData, tasks) => {
|
||||
const $tasks = $('#task_status');
|
||||
const $tasksShort = $('#task_status_short');
|
||||
const $issueableHeader = $('.issuable-header');
|
||||
const tasksStates = { newState: null, currentState: null };
|
||||
|
||||
if ($tasks.length === 0) {
|
||||
if (!(newStateData.task_status.indexOf('0 of 0') === 0)) {
|
||||
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
|
||||
} else {
|
||||
$issueableHeader.append('<span id="task_status"></span>');
|
||||
}
|
||||
} else {
|
||||
tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0;
|
||||
tasksStates.currentState = tasks.indexOf('0 of 0') === 0;
|
||||
}
|
||||
|
||||
if ($tasks.length !== 0 && !tasksStates.newState) {
|
||||
$tasks.text(newStateData.task_status);
|
||||
$tasksShort.text(newStateData.task_status);
|
||||
} else if (tasksStates.currentState) {
|
||||
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
|
||||
} else if (tasksStates.newState) {
|
||||
$tasks.remove();
|
||||
$tasksShort.remove();
|
||||
}
|
||||
};
|
96
app/assets/javascripts/issue_show/components/app.vue
Normal file
96
app/assets/javascripts/issue_show/components/app.vue
Normal file
|
@ -0,0 +1,96 @@
|
|||
<script>
|
||||
import Visibility from 'visibilityjs';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
import Service from '../services/index';
|
||||
import Store from '../stores';
|
||||
import titleComponent from './title.vue';
|
||||
import descriptionComponent from './description.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
endpoint: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canUpdate: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
issuableRef: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialDescriptionHtml: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
initialDescriptionText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const store = new Store({
|
||||
titleHtml: this.initialTitle,
|
||||
descriptionHtml: this.initialDescriptionHtml,
|
||||
descriptionText: this.initialDescriptionText,
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
state: store.state,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
descriptionComponent,
|
||||
titleComponent,
|
||||
},
|
||||
created() {
|
||||
const resource = new Service(this.endpoint);
|
||||
const poll = new Poll({
|
||||
resource,
|
||||
method: 'getData',
|
||||
successCallback: (res) => {
|
||||
this.store.updateState(res.json());
|
||||
},
|
||||
errorCallback(err) {
|
||||
throw new Error(err);
|
||||
},
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
poll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
poll.restart();
|
||||
} else {
|
||||
poll.stop();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<title-component
|
||||
:issuable-ref="issuableRef"
|
||||
:title-html="state.titleHtml"
|
||||
:title-text="state.titleText" />
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
105
app/assets/javascripts/issue_show/components/description.vue
Normal file
105
app/assets/javascripts/issue_show/components/description.vue
Normal file
|
@ -0,0 +1,105 @@
|
|||
<script>
|
||||
import animateMixin from '../mixins/animate';
|
||||
|
||||
export default {
|
||||
mixins: [animateMixin],
|
||||
props: {
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
descriptionHtml: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
descriptionText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
taskStatus: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
preAnimation: false,
|
||||
pulseAnimation: false,
|
||||
timeAgoEl: $('.js-issue-edited-ago'),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
descriptionHtml() {
|
||||
this.animateChange();
|
||||
|
||||
this.$nextTick(() => {
|
||||
const toolTipTime = gl.utils.formatDate(this.updatedAt);
|
||||
|
||||
this.timeAgoEl.attr('datetime', this.updatedAt)
|
||||
.attr('title', toolTipTime)
|
||||
.tooltip('fixTitle');
|
||||
|
||||
this.renderGFM();
|
||||
});
|
||||
},
|
||||
taskStatus() {
|
||||
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
|
||||
const $issuableHeader = $('.issuable-meta');
|
||||
const $tasks = $('#task_status', $issuableHeader);
|
||||
const $tasksShort = $('#task_status_short', $issuableHeader);
|
||||
|
||||
if (taskRegexMatches) {
|
||||
$tasks.text(this.taskStatus);
|
||||
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
|
||||
} else {
|
||||
$tasks.text('');
|
||||
$tasksShort.text('');
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
$(this.$refs['gfm-entry-content']).renderGFM();
|
||||
|
||||
if (this.canUpdate) {
|
||||
// eslint-disable-next-line no-new
|
||||
new gl.TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderGFM();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="description"
|
||||
:class="{
|
||||
'js-task-list-container': canUpdate
|
||||
}">
|
||||
<div
|
||||
class="wiki"
|
||||
:class="{
|
||||
'issue-realtime-pre-pulse': preAnimation,
|
||||
'issue-realtime-trigger-pulse': pulseAnimation
|
||||
}"
|
||||
v-html="descriptionHtml"
|
||||
ref="gfm-content">
|
||||
</div>
|
||||
<textarea
|
||||
class="hidden js-task-list-field"
|
||||
v-if="descriptionText"
|
||||
v-model="descriptionText">
|
||||
</textarea>
|
||||
</div>
|
||||
</template>
|
53
app/assets/javascripts/issue_show/components/title.vue
Normal file
53
app/assets/javascripts/issue_show/components/title.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import animateMixin from '../mixins/animate';
|
||||
|
||||
export default {
|
||||
mixins: [animateMixin],
|
||||
data() {
|
||||
return {
|
||||
preAnimation: false,
|
||||
pulseAnimation: false,
|
||||
titleEl: document.querySelector('title'),
|
||||
};
|
||||
},
|
||||
props: {
|
||||
issuableRef: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
titleHtml: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
titleText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
titleHtml() {
|
||||
this.setPageTitle();
|
||||
this.animateChange();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setPageTitle() {
|
||||
const currentPageTitleScope = this.titleEl.innerText.split('·');
|
||||
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
|
||||
this.titleEl.textContent = currentPageTitleScope.join('·');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2
|
||||
class="title"
|
||||
:class="{
|
||||
'issue-realtime-pre-pulse': preAnimation,
|
||||
'issue-realtime-trigger-pulse': pulseAnimation
|
||||
}"
|
||||
v-html="titleHtml"
|
||||
>
|
||||
</h2>
|
||||
</template>
|
|
@ -1,20 +1,42 @@
|
|||
import Vue from 'vue';
|
||||
import IssueTitle from './issue_title_description.vue';
|
||||
import issuableApp from './components/app.vue';
|
||||
import '../vue_shared/vue_resource_interceptor';
|
||||
|
||||
(() => {
|
||||
const issueTitleData = document.querySelector('.issue-title-data').dataset;
|
||||
const { canUpdateTasksClass, endpoint } = issueTitleData;
|
||||
document.addEventListener('DOMContentLoaded', () => new Vue({
|
||||
el: document.getElementById('js-issuable-app'),
|
||||
components: {
|
||||
issuableApp,
|
||||
},
|
||||
data() {
|
||||
const issuableElement = this.$options.el;
|
||||
const issuableTitleElement = issuableElement.querySelector('.title');
|
||||
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
|
||||
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
|
||||
const {
|
||||
canUpdate,
|
||||
endpoint,
|
||||
issuableRef,
|
||||
} = issuableElement.dataset;
|
||||
|
||||
const vm = new Vue({
|
||||
el: '.issue-title-entrypoint',
|
||||
render: createElement => createElement(IssueTitle, {
|
||||
return {
|
||||
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
|
||||
endpoint,
|
||||
issuableRef,
|
||||
initialTitle: issuableTitleElement.innerHTML,
|
||||
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
|
||||
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('issuable-app', {
|
||||
props: {
|
||||
canUpdateTasksClass,
|
||||
endpoint,
|
||||
canUpdate: this.canUpdate,
|
||||
endpoint: this.endpoint,
|
||||
issuableRef: this.issuableRef,
|
||||
initialTitle: this.initialTitle,
|
||||
initialDescriptionHtml: this.initialDescriptionHtml,
|
||||
initialDescriptionText: this.initialDescriptionText,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return vm;
|
||||
})();
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
<script>
|
||||
import Visibility from 'visibilityjs';
|
||||
import Poll from './../lib/utils/poll';
|
||||
import Service from './services/index';
|
||||
import tasks from './actions/tasks';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
endpoint: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canUpdateTasksClass: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const resource = new Service(this.$http, this.endpoint);
|
||||
|
||||
const poll = new Poll({
|
||||
resource,
|
||||
method: 'getTitle',
|
||||
successCallback: (res) => {
|
||||
this.renderResponse(res);
|
||||
},
|
||||
errorCallback: (err) => {
|
||||
throw new Error(err);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
poll,
|
||||
apiData: {},
|
||||
tasks: '0 of 0',
|
||||
title: null,
|
||||
titleText: '',
|
||||
titleFlag: {
|
||||
pre: true,
|
||||
pulse: false,
|
||||
},
|
||||
description: null,
|
||||
descriptionText: '',
|
||||
descriptionChange: false,
|
||||
descriptionFlag: {
|
||||
pre: true,
|
||||
pulse: false,
|
||||
},
|
||||
timeAgoEl: $('.issue_edited_ago'),
|
||||
titleEl: document.querySelector('title'),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateFlag(key, toggle) {
|
||||
this[key].pre = toggle;
|
||||
this[key].pulse = !toggle;
|
||||
},
|
||||
renderResponse(res) {
|
||||
this.apiData = res.json();
|
||||
this.triggerAnimation();
|
||||
},
|
||||
updateTaskHTML() {
|
||||
tasks(this.apiData, this.tasks);
|
||||
},
|
||||
elementsToVisualize(noTitleChange, noDescriptionChange) {
|
||||
if (!noTitleChange) {
|
||||
this.titleText = this.apiData.title_text;
|
||||
this.updateFlag('titleFlag', true);
|
||||
}
|
||||
|
||||
if (!noDescriptionChange) {
|
||||
// only change to true when we need to bind TaskLists the html of description
|
||||
this.descriptionChange = true;
|
||||
this.updateTaskHTML();
|
||||
this.tasks = this.apiData.task_status;
|
||||
this.updateFlag('descriptionFlag', true);
|
||||
}
|
||||
},
|
||||
setTabTitle() {
|
||||
const currentTabTitleScope = this.titleEl.innerText.split('·');
|
||||
currentTabTitleScope[0] = `${this.titleText} (#${this.apiData.issue_number}) `;
|
||||
this.titleEl.innerText = currentTabTitleScope.join('·');
|
||||
},
|
||||
animate(title, description) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.setTabTitle();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateFlag('titleFlag', false);
|
||||
this.updateFlag('descriptionFlag', false);
|
||||
});
|
||||
},
|
||||
triggerAnimation() {
|
||||
// always reset to false before checking the change
|
||||
this.descriptionChange = false;
|
||||
|
||||
const { title, description } = this.apiData;
|
||||
this.descriptionText = this.apiData.description_text;
|
||||
|
||||
const noTitleChange = this.title === title;
|
||||
const noDescriptionChange = this.description === description;
|
||||
|
||||
/**
|
||||
* since opacity is changed, even if there is no diff for Vue to update
|
||||
* we must check the title/description even on a 304 to ensure no visual change
|
||||
*/
|
||||
if (noTitleChange && noDescriptionChange) return;
|
||||
|
||||
this.elementsToVisualize(noTitleChange, noDescriptionChange);
|
||||
this.animate(title, description);
|
||||
},
|
||||
updateEditedTimeAgo() {
|
||||
const toolTipTime = gl.utils.formatDate(this.apiData.updated_at);
|
||||
this.timeAgoEl.attr('datetime', this.apiData.updated_at);
|
||||
this.timeAgoEl.attr('title', toolTipTime).tooltip('fixTitle');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (!Visibility.hidden()) {
|
||||
this.poll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
this.poll.restart();
|
||||
} else {
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
// if new html is injected (description changed) - bind TaskList and call renderGFM
|
||||
if (this.descriptionChange) {
|
||||
this.updateEditedTimeAgo();
|
||||
|
||||
$(this.$refs['issue-content-container-gfm-entry']).renderGFM();
|
||||
|
||||
const tl = new gl.TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
});
|
||||
|
||||
return tl && null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2
|
||||
class="title"
|
||||
:class="{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }"
|
||||
ref="issue-title"
|
||||
v-html="title"
|
||||
>
|
||||
</h2>
|
||||
<div
|
||||
class="description is-task-list-enabled"
|
||||
:class="canUpdateTasksClass"
|
||||
v-if="description"
|
||||
>
|
||||
<div
|
||||
class="wiki"
|
||||
:class="{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }"
|
||||
v-html="description"
|
||||
ref="issue-content-container-gfm-entry"
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
class="hidden js-task-list-field"
|
||||
v-if="descriptionText"
|
||||
>{{descriptionText}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
13
app/assets/javascripts/issue_show/mixins/animate.js
Normal file
13
app/assets/javascripts/issue_show/mixins/animate.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export default {
|
||||
methods: {
|
||||
animateChange() {
|
||||
this.preAnimation = true;
|
||||
this.pulseAnimation = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.preAnimation = false;
|
||||
this.pulseAnimation = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,10 +1,16 @@
|
|||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default class Service {
|
||||
constructor(resource, endpoint) {
|
||||
this.resource = resource;
|
||||
constructor(endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
|
||||
this.resource = Vue.resource(this.endpoint);
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.resource.get(this.endpoint);
|
||||
getData() {
|
||||
return this.resource.get();
|
||||
}
|
||||
}
|
||||
|
|
25
app/assets/javascripts/issue_show/stores/index.js
Normal file
25
app/assets/javascripts/issue_show/stores/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
export default class Store {
|
||||
constructor({
|
||||
titleHtml,
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
}) {
|
||||
this.state = {
|
||||
titleHtml,
|
||||
titleText: '',
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
taskStatus: '',
|
||||
updatedAt: '',
|
||||
};
|
||||
}
|
||||
|
||||
updateState(data) {
|
||||
this.state.titleHtml = data.title;
|
||||
this.state.titleText = data.title_text;
|
||||
this.state.descriptionHtml = data.description;
|
||||
this.state.descriptionText = data.description_text;
|
||||
this.state.taskStatus = data.task_status;
|
||||
this.state.updatedAt = data.updated_at;
|
||||
}
|
||||
}
|
|
@ -112,7 +112,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.issue_edited_ago,
|
||||
.issue-edited-ago,
|
||||
.note_edited_ago {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
description: view_context.markdown_field(@issue, :description),
|
||||
description_text: @issue.description,
|
||||
task_status: @issue.task_status,
|
||||
issue_number: @issue.iid,
|
||||
updated_at: @issue.updated_at
|
||||
}
|
||||
end
|
||||
|
|
|
@ -136,11 +136,9 @@ module IssuablesHelper
|
|||
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
|
||||
end
|
||||
|
||||
if issuable.tasks?
|
||||
output << " ".html_safe
|
||||
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
|
||||
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
|
||||
end
|
||||
output << " ".html_safe
|
||||
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
|
||||
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
|
||||
|
||||
output
|
||||
end
|
||||
|
|
|
@ -51,12 +51,17 @@
|
|||
|
||||
.issue-details.issuable-details
|
||||
.detail-page-description.content-block
|
||||
.issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||
"can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
|
||||
#js-issuable-app{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||
"can-update" => can?(current_user, :update_issue, @issue).to_s,
|
||||
"issuable-ref" => @issue.to_reference,
|
||||
} }
|
||||
.issue-title-entrypoint
|
||||
%h2.title= markdown_field(@issue, :title)
|
||||
- if @issue.description.present?
|
||||
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
|
||||
.wiki= markdown_field(@issue, :description)
|
||||
%textarea.hidden.js-task-list-field= @issue.description
|
||||
|
||||
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
|
||||
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
|
||||
|
||||
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
|
||||
// This element is filled in using JavaScript.
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
import $ from 'jquery';
|
||||
import '~/render_math';
|
||||
import '~/render_gfm';
|
||||
import issueTitleDescription from '~/issue_show/issue_title_description.vue';
|
||||
import issueShowData from './mock_data';
|
||||
|
||||
window.$ = $;
|
||||
import issuableApp from '~/issue_show/components/app.vue';
|
||||
import issueShowData from '../mock_data';
|
||||
|
||||
const issueShowInterceptor = data => (request, next) => {
|
||||
next(request.respondWith(JSON.stringify(data), {
|
||||
|
@ -16,13 +13,25 @@ const issueShowInterceptor = data => (request, next) => {
|
|||
}));
|
||||
};
|
||||
|
||||
describe('Issue Title', () => {
|
||||
describe('Issuable output', () => {
|
||||
document.body.innerHTML = '<span id="task_status"></span>';
|
||||
|
||||
let IssueTitleDescriptionComponent;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
IssueTitleDescriptionComponent = Vue.extend(issueTitleDescription);
|
||||
const IssuableDescriptionComponent = Vue.extend(issuableApp);
|
||||
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
|
||||
|
||||
vm = new IssuableDescriptionComponent({
|
||||
propsData: {
|
||||
canUpdate: true,
|
||||
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
|
||||
issuableRef: '#1',
|
||||
initialTitle: '',
|
||||
initialDescriptionHtml: '',
|
||||
initialDescriptionText: '',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -30,28 +39,19 @@ describe('Issue Title', () => {
|
|||
});
|
||||
|
||||
it('should render a title/description and update title/description on update', (done) => {
|
||||
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
|
||||
|
||||
const issueShowComponent = new IssueTitleDescriptionComponent({
|
||||
propsData: {
|
||||
canUpdateIssue: '.css-stuff',
|
||||
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
|
||||
expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
|
||||
expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
|
||||
expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description');
|
||||
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
|
||||
expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
|
||||
expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description');
|
||||
|
||||
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
|
||||
|
||||
setTimeout(() => {
|
||||
expect(document.querySelector('title').innerText).toContain('2 (#1)');
|
||||
expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
|
||||
expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
|
||||
expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('42');
|
||||
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
|
||||
expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
|
||||
expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
|
||||
|
||||
done();
|
||||
});
|
99
spec/javascripts/issue_show/components/description_spec.js
Normal file
99
spec/javascripts/issue_show/components/description_spec.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import Vue from 'vue';
|
||||
import descriptionComponent from '~/issue_show/components/description.vue';
|
||||
|
||||
describe('Description component', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(descriptionComponent);
|
||||
|
||||
if (!document.querySelector('.issuable-meta')) {
|
||||
const metaData = document.createElement('div');
|
||||
metaData.classList.add('issuable-meta');
|
||||
metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>';
|
||||
|
||||
document.body.appendChild(metaData);
|
||||
}
|
||||
|
||||
vm = new Component({
|
||||
propsData: {
|
||||
canUpdate: true,
|
||||
descriptionHtml: 'test',
|
||||
descriptionText: 'test',
|
||||
updatedAt: new Date().toString(),
|
||||
taskStatus: '',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('animates description changes', (done) => {
|
||||
vm.descriptionHtml = 'changed';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'),
|
||||
).toBeTruthy();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'),
|
||||
).toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('re-inits the TaskList when description changed', (done) => {
|
||||
spyOn(gl, 'TaskList');
|
||||
vm.descriptionHtml = 'changed';
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
gl.TaskList,
|
||||
).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not re-init the TaskList when canUpdate is false', (done) => {
|
||||
spyOn(gl, 'TaskList');
|
||||
vm.canUpdate = false;
|
||||
vm.descriptionHtml = 'changed';
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
gl.TaskList,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskStatus', () => {
|
||||
it('adds full taskStatus', (done) => {
|
||||
vm.taskStatus = '1 of 1';
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
document.querySelector('.issuable-meta #task_status').textContent.trim(),
|
||||
).toBe('1 of 1');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('adds short taskStatus', (done) => {
|
||||
vm.taskStatus = '1 of 1';
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
document.querySelector('.issuable-meta #task_status_short').textContent.trim(),
|
||||
).toBe('1/1 task');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
67
spec/javascripts/issue_show/components/title_spec.js
Normal file
67
spec/javascripts/issue_show/components/title_spec.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import Vue from 'vue';
|
||||
import titleComponent from '~/issue_show/components/title.vue';
|
||||
|
||||
describe('Title component', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(titleComponent);
|
||||
vm = new Component({
|
||||
propsData: {
|
||||
issuableRef: '#1',
|
||||
titleHtml: 'Testing <img />',
|
||||
titleText: 'Testing',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('renders title HTML', () => {
|
||||
expect(
|
||||
vm.$el.innerHTML.trim(),
|
||||
).toBe('Testing <img>');
|
||||
});
|
||||
|
||||
it('updates page title when changing titleHtml', (done) => {
|
||||
spyOn(vm, 'setPageTitle');
|
||||
vm.titleHtml = 'test';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.setPageTitle,
|
||||
).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('animates title changes', (done) => {
|
||||
vm.titleHtml = 'test';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.classList.contains('issue-realtime-pre-pulse'),
|
||||
).toBeTruthy();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
vm.$el.classList.contains('issue-realtime-trigger-pulse'),
|
||||
).toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates page title after changing title', (done) => {
|
||||
vm.titleHtml = 'changed';
|
||||
vm.titleText = 'changed';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
document.querySelector('title').textContent.trim(),
|
||||
).toContain('changed');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,23 +4,23 @@ export default {
|
|||
title_text: 'this is a title',
|
||||
description: '<p>this is a description!</p>',
|
||||
description_text: 'this is a description',
|
||||
issue_number: 1,
|
||||
task_status: '2 of 4 completed',
|
||||
updated_at: new Date().toString(),
|
||||
},
|
||||
secondRequest: {
|
||||
title: '<p>2</p>',
|
||||
title_text: '2',
|
||||
description: '<p>42</p>',
|
||||
description_text: '42',
|
||||
issue_number: 1,
|
||||
task_status: '0 of 0 completed',
|
||||
updated_at: new Date().toString(),
|
||||
},
|
||||
issueSpecRequest: {
|
||||
title: '<p>this is a title</p>',
|
||||
title_text: 'this is a title',
|
||||
description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>',
|
||||
description_text: '- [ ] Task List Item',
|
||||
issue_number: 1,
|
||||
task_status: '0 of 1 completed',
|
||||
updated_at: new Date().toString(),
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue