330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
|
import '../utils/MarkdownPreview'
|
||
|
import './AttachmentEditor'
|
||
|
import './UploadProgress'
|
||
|
import { thenCreateComment, thenUpdateComment } from '../../api/comments'
|
||
|
import { thenUploadFile } from '../../api/files'
|
||
|
import { Droptarget } from '../mixins/Droptarget'
|
||
|
import { UnitOfWorkTracker } from '../mixins/UnitOfWorkTracker'
|
||
|
import { EventBus, Events } from './EventBus'
|
||
|
|
||
|
const MAX_ATTACHMENTS = 5;
|
||
|
|
||
|
const TEMPLATE =`
|
||
|
<div class="comment-reply-form"
|
||
|
:class="dropTargetClasses"
|
||
|
>
|
||
|
<div class="attachments">
|
||
|
<comment-attachment-editor
|
||
|
v-for="a in attachments"
|
||
|
@delete="attachmentDelete"
|
||
|
@insert="insertAttachment"
|
||
|
@rename="attachmentRename"
|
||
|
@validation="attachmentValidation"
|
||
|
@unit-of-work="childUnitOfWork"
|
||
|
:slug="a.slug"
|
||
|
:allSlugs="allSlugs"
|
||
|
:oid="a.oid"
|
||
|
:key="a.oid"
|
||
|
/>
|
||
|
<upload-progress
|
||
|
v-if="uploads.nbrOfActive > 0"
|
||
|
:label="uploadProgressLabel"
|
||
|
:progress="uploadProgressPercent"
|
||
|
/>
|
||
|
</div>
|
||
|
<div class="comment-reply-field"
|
||
|
:class="{filled: isMsgLongEnough}"
|
||
|
>
|
||
|
<textarea
|
||
|
ref="inputField"
|
||
|
@keyup="keyUp"
|
||
|
v-model="msg"
|
||
|
id="comment_field"
|
||
|
placeholder="Join the conversation...">
|
||
|
</textarea>
|
||
|
<div class="comment-reply-meta">
|
||
|
<button class="comment-action-submit"
|
||
|
:class="{disabled: !canSubmit}"
|
||
|
@click="submit"
|
||
|
type="button"
|
||
|
title="Post Comment (Ctrl+Enter)">
|
||
|
<span>
|
||
|
<i :class="submitButtonIcon"/>{{ submitButtonText }}
|
||
|
</span>
|
||
|
<span class="hotkey">Ctrl + Enter</span>
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
<markdown-preview
|
||
|
v-show="msg.length > 0"
|
||
|
:markdown="msg"
|
||
|
:attachments="attachmentsAsObject"
|
||
|
/>
|
||
|
</div>
|
||
|
`;
|
||
|
|
||
|
Vue.component('comment-editor', {
|
||
|
template: TEMPLATE,
|
||
|
mixins: [Droptarget, UnitOfWorkTracker],
|
||
|
props: {
|
||
|
user: Object,
|
||
|
parentId: String,
|
||
|
projectId: String,
|
||
|
comment: Object,
|
||
|
mode: {
|
||
|
type: String,
|
||
|
default: 'reply', // reply or update
|
||
|
},
|
||
|
},
|
||
|
data() {
|
||
|
return {
|
||
|
msg: this.initialMsg(),
|
||
|
attachments: this.initialAttachments(),
|
||
|
uploads: {
|
||
|
nbrOfActive: 0,
|
||
|
nbrOfTotal: 0,
|
||
|
total: 0,
|
||
|
loaded: 0
|
||
|
},
|
||
|
}
|
||
|
},
|
||
|
computed: {
|
||
|
submitButtonText() {
|
||
|
switch(this.mode) {
|
||
|
case 'reply': return 'Send';
|
||
|
case 'update': return 'Update';
|
||
|
default: console.error('Unknown mode: ', this.mode);
|
||
|
}
|
||
|
},
|
||
|
submitButtonIcon() {
|
||
|
if (this.isBusyWorking) {
|
||
|
return 'pi-spin spin';
|
||
|
}else{
|
||
|
switch(this.mode) {
|
||
|
case 'reply': return 'pi-paper-plane';
|
||
|
case 'update': return 'pi-check';
|
||
|
default: console.error('Unknown mode: ', this.mode);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
attachmentsAsObject() {
|
||
|
let attachmentsObject = {};
|
||
|
for (let a of this.attachments) {
|
||
|
attachmentsObject[a.slug] = {oid: a.oid};
|
||
|
}
|
||
|
return attachmentsObject;
|
||
|
},
|
||
|
allSlugs() {
|
||
|
return this.attachments.map((a) => {
|
||
|
return a['slug'];
|
||
|
});
|
||
|
},
|
||
|
isMsgLongEnough() {
|
||
|
return this.msg.length >= 5;
|
||
|
},
|
||
|
isAttachmentsValid() {
|
||
|
for (let att of this.attachments) {
|
||
|
if(!att.isSlugValid) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
isValid() {
|
||
|
return this.isAttachmentsValid && this.isMsgLongEnough;
|
||
|
},
|
||
|
canSubmit() {
|
||
|
return this.isValid && !this.isBusyWorking;
|
||
|
},
|
||
|
uploadProgressPercent() {
|
||
|
if (this.uploads.nbrOfActive === 0 || this.uploads.total === 0) {
|
||
|
return 100;
|
||
|
}
|
||
|
return this.uploads.loaded / this.uploads.total * 100;
|
||
|
},
|
||
|
uploadProgressLabel() {
|
||
|
if (this.uploadProgressPercent === 100) {
|
||
|
return 'Processing'
|
||
|
}
|
||
|
if (this.uploads.nbrOfTotal === 1) {
|
||
|
return 'Uploading file';
|
||
|
} else {
|
||
|
let fileOf = this.uploads.nbrOfTotal - this.uploads.nbrOfActive + 1;
|
||
|
return `Uploading ${fileOf}/${this.uploads.nbrOfTotal} files`;
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
watch:{
|
||
|
msg(){
|
||
|
this.autoSizeInputField();
|
||
|
}
|
||
|
},
|
||
|
mounted() {
|
||
|
if(this.comment) {
|
||
|
this.$nextTick(function () {
|
||
|
this.autoSizeInputField();
|
||
|
this.$refs.inputField.focus();
|
||
|
})
|
||
|
}
|
||
|
},
|
||
|
methods: {
|
||
|
initialMsg() {
|
||
|
if (this.comment) {
|
||
|
if (this.mode === 'reply') {
|
||
|
return `***@${this.comment.user.full_name}*** `;
|
||
|
}
|
||
|
if (this.mode === 'update') {
|
||
|
return this.comment.msg_markdown;
|
||
|
}
|
||
|
}
|
||
|
return '';
|
||
|
},
|
||
|
initialAttachments() {
|
||
|
// Transforming the attacmentobject to an array of attachments
|
||
|
let attachmentsList = []
|
||
|
if(this.mode === 'update') {
|
||
|
let attachmentsObj = this.comment.properties.attachments
|
||
|
for (let k in attachmentsObj) {
|
||
|
if (attachmentsObj.hasOwnProperty(k)) {
|
||
|
let a = {
|
||
|
slug: k,
|
||
|
oid: attachmentsObj[k]['oid'],
|
||
|
isSlugValid: true
|
||
|
}
|
||
|
attachmentsList.push(a);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return attachmentsList;
|
||
|
},
|
||
|
submit() {
|
||
|
if(!this.canSubmit) return;
|
||
|
this.unitOfWork(
|
||
|
this.thenSubmit()
|
||
|
.fail((err) => {toastr.error(pillar.utils.messageFromError(err), 'Failed to submit comment')})
|
||
|
);
|
||
|
},
|
||
|
thenSubmit() {
|
||
|
if (this.mode === 'reply') {
|
||
|
return this.thenCreateComment();
|
||
|
} else {
|
||
|
return this.thenUpdateComment();
|
||
|
}
|
||
|
},
|
||
|
keyUp(e) {
|
||
|
if ((e.keyCode == 13 || e.key === 'Enter') && e.ctrlKey) {
|
||
|
this.submit();
|
||
|
}
|
||
|
},
|
||
|
thenCreateComment() {
|
||
|
return thenCreateComment(this.parentId, this.msg, this.attachmentsAsObject)
|
||
|
.then((newComment) => {
|
||
|
EventBus.$emit(Events.NEW_COMMENT, newComment);
|
||
|
EventBus.$emit(Events.EDIT_DONE, newComment.id );
|
||
|
this.cleanUp();
|
||
|
})
|
||
|
},
|
||
|
thenUpdateComment() {
|
||
|
return thenUpdateComment(this.comment.parent, this.comment.id, this.msg, this.attachmentsAsObject)
|
||
|
.then((updatedComment) => {
|
||
|
EventBus.$emit(Events.UPDATED_COMMENT, updatedComment);
|
||
|
EventBus.$emit(Events.EDIT_DONE, updatedComment.id);
|
||
|
this.cleanUp();
|
||
|
})
|
||
|
},
|
||
|
canHandleDrop(event) {
|
||
|
let dataTransfer = event.dataTransfer;
|
||
|
let items = [...dataTransfer.items];
|
||
|
let nbrOfAttachments = items.length + this.uploads.nbrOfActive + this.attachments.length;
|
||
|
if(nbrOfAttachments > MAX_ATTACHMENTS) {
|
||
|
// Exceeds the limit
|
||
|
return false;
|
||
|
}
|
||
|
// Only files in drop
|
||
|
return [...dataTransfer.items].reduce((prev, it) => {
|
||
|
let isFile = it.kind === 'file' && !!it.type;
|
||
|
return prev && isFile;
|
||
|
}, !!items.length);
|
||
|
},
|
||
|
onDrop(event) {
|
||
|
let files = [...event.dataTransfer.files];
|
||
|
for (let f of files) {
|
||
|
this.unitOfWork(
|
||
|
this.thenUploadFile(f)
|
||
|
.fail((err) => {toastr.error(pillar.utils.messageFromError(err), 'File upload failed')})
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
thenUploadFile(file){
|
||
|
let lastReportedTotal = 0;
|
||
|
let lastReportedLoaded = 0;
|
||
|
let progressCB = (total, loaded) => {
|
||
|
this.uploads.loaded += loaded - lastReportedLoaded;
|
||
|
this.uploads.total += total - lastReportedTotal;
|
||
|
lastReportedLoaded = loaded;
|
||
|
lastReportedTotal = total;
|
||
|
}
|
||
|
this.uploads.nbrOfActive++;
|
||
|
this.uploads.nbrOfTotal++;
|
||
|
return thenUploadFile(this.projectId || this.comment.project, file, progressCB)
|
||
|
.then((resp) => {
|
||
|
let attachment = {
|
||
|
slug: file.name,
|
||
|
oid: resp['file_id'],
|
||
|
isSlugValid: false
|
||
|
}
|
||
|
this.attachments.push(attachment);
|
||
|
this.msg += this.getAttachmentMarkdown(attachment);
|
||
|
})
|
||
|
.always(()=>{
|
||
|
this.uploads.nbrOfActive--;
|
||
|
if(this.uploads.nbrOfActive === 0) {
|
||
|
this.uploads.loaded = 0;
|
||
|
this.uploads.total = 0;
|
||
|
this.uploads.nbrOfTotal = 0;
|
||
|
}
|
||
|
})
|
||
|
},
|
||
|
getAttachmentMarkdown(attachment){
|
||
|
return `{attachment ${attachment.slug}}`;
|
||
|
},
|
||
|
insertAttachment(oid){
|
||
|
let attachment = this.getAttachment(oid);
|
||
|
this.msg += this.getAttachmentMarkdown(attachment);
|
||
|
},
|
||
|
attachmentDelete(oid) {
|
||
|
let attachment = this.getAttachment(oid);
|
||
|
let markdownToRemove = this.getAttachmentMarkdown(attachment);
|
||
|
this.msg = this.msg.replace(new RegExp(markdownToRemove,'g'), '');
|
||
|
this.attachments = this.attachments.filter((a) => {return a.oid !== oid});
|
||
|
},
|
||
|
attachmentRename(newName, oid) {
|
||
|
let attachment = this.getAttachment(oid);
|
||
|
let oldMarkdownAttachment = this.getAttachmentMarkdown(attachment);
|
||
|
attachment.slug = newName;
|
||
|
let newMarkdownAttachment = this.getAttachmentMarkdown(attachment);
|
||
|
|
||
|
this.msg = this.msg.replace(new RegExp(oldMarkdownAttachment,'g'), newMarkdownAttachment);
|
||
|
},
|
||
|
getAttachment(oid) {
|
||
|
for (let a of this.attachments) {
|
||
|
if (a.oid === oid) return a;
|
||
|
}
|
||
|
console.error('No attachment found:', oid);
|
||
|
},
|
||
|
attachmentValidation(oid, isValid) {
|
||
|
let attachment = this.getAttachment(oid);
|
||
|
attachment.isSlugValid = isValid;
|
||
|
},
|
||
|
cleanUp() {
|
||
|
this.msg = '';
|
||
|
this.attachments = [];
|
||
|
},
|
||
|
autoSizeInputField() {
|
||
|
let elInputField = this.$refs.inputField;
|
||
|
elInputField.style.cssText = 'height:auto; padding:0';
|
||
|
let newInputHeight = elInputField.scrollHeight + 20;
|
||
|
elInputField.style.cssText = `height:${ newInputHeight }px`;
|
||
|
}
|
||
|
}
|
||
|
});
|