Refactored issue tealtime elements
This is to match our docs better and will also help a future issue. Also made it possible for the description & title to be readable when JS is disabled
This commit is contained in:
parent
566ee14516
commit
5a95d6f8da
10 changed files with 327 additions and 225 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();
|
|
||||||
}
|
|
||||||
};
|
|
95
app/assets/javascripts/issue_show/components/app.vue
Normal file
95
app/assets/javascripts/issue_show/components/app.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<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({
|
||||||
|
title: 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
|
||||||
|
:can-update="canUpdate"
|
||||||
|
:description-html="state.descriptionHtml"
|
||||||
|
:description-text="state.descriptionText"
|
||||||
|
:updated-at="state.updatedAt"
|
||||||
|
:task-status="state.taskStatus" />
|
||||||
|
</div>
|
||||||
|
</template>
|
100
app/assets/javascripts/issue_show/components/description.vue
Normal file
100
app/assets/javascripts/issue_show/components/description.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<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: $('.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.$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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
taskStatus() {
|
||||||
|
const $issueableHeader = $('.issuable-header');
|
||||||
|
let $tasks = $('#task_status');
|
||||||
|
let $tasksShort = $('#task_status_short');
|
||||||
|
|
||||||
|
if (this.taskStatus.indexOf('0 of 0') >= 0) {
|
||||||
|
$tasks.remove();
|
||||||
|
$tasksShort.remove();
|
||||||
|
} else if (!$tasks.length && !$tasksShort.length) {
|
||||||
|
$tasks = $issueableHeader.append('<span id="task_status"></span>');
|
||||||
|
$tasksShort = $issueableHeader.append('<span id="task_status_short"></span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks.text(this.taskStatus);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="descriptionHtml"
|
||||||
|
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"
|
||||||
|
>{{ 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,32 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import IssueTitle from './issue_title_description.vue';
|
import issuableApp from './components/app.vue';
|
||||||
import '../vue_shared/vue_resource_interceptor';
|
import '../vue_shared/vue_resource_interceptor';
|
||||||
|
|
||||||
(() => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const issueTitleData = document.querySelector('.issue-title-data').dataset;
|
const issuableElement = document.getElementById('js-issuable-app');
|
||||||
const { canUpdateTasksClass, endpoint } = issueTitleData;
|
const issuableTitleElement = issuableElement.querySelector('.title');
|
||||||
|
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
|
||||||
const vm = new Vue({
|
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
|
||||||
el: '.issue-title-entrypoint',
|
const {
|
||||||
render: createElement => createElement(IssueTitle, {
|
canUpdate,
|
||||||
props: {
|
|
||||||
canUpdateTasksClass,
|
|
||||||
endpoint,
|
endpoint,
|
||||||
|
issuableRef,
|
||||||
|
} = issuableElement.dataset;
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el: issuableElement,
|
||||||
|
components: {
|
||||||
|
issuableApp,
|
||||||
|
},
|
||||||
|
render: createElement => createElement('issuable-app', {
|
||||||
|
props: {
|
||||||
|
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
|
||||||
|
endpoint,
|
||||||
|
issuableRef,
|
||||||
|
initialTitle: issuableTitleElement.innerHTML,
|
||||||
|
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
|
||||||
|
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
});
|
||||||
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 {
|
export default class Service {
|
||||||
constructor(resource, endpoint) {
|
constructor(endpoint) {
|
||||||
this.resource = resource;
|
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
|
|
||||||
|
this.resource = Vue.resource(this.endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle() {
|
getData() {
|
||||||
return this.resource.get(this.endpoint);
|
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({
|
||||||
|
title,
|
||||||
|
descriptionHtml,
|
||||||
|
descriptionText,
|
||||||
|
}) {
|
||||||
|
this.state = {
|
||||||
|
titleHtml: title,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,10 +51,15 @@
|
||||||
|
|
||||||
.issue-details.issuable-details
|
.issue-details.issuable-details
|
||||||
.detail-page-description.content-block
|
.detail-page-description.content-block
|
||||||
.issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
#js-issuable-app{ "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' : '',
|
"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')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue