Files
Disco/Disco.Web/Views/Job/JobParts/Resources.cshtml
T

624 lines
28 KiB
Plaintext

@model Disco.Web.Models.Job.ShowModel
@{
Authorization.RequireAny(Claims.Job.ShowLogs, Claims.Job.ShowAttachments);
var canShowLogs = Authorization.Has(Claims.Job.ShowLogs);
var canShowAttachments = Authorization.Has(Claims.Job.ShowAttachments);
var canAddLogs = Authorization.Has(Claims.Job.Actions.AddLogs);
var canRemoveAnyLogs = Authorization.Has(Claims.Job.Actions.RemoveAnyLogs);
var canRemoveOwnLogs = Authorization.Has(Claims.Job.Actions.RemoveOwnLogs);
var canAddAttachments = Authorization.Has(Claims.Job.Actions.AddAttachments);
var canRemoveAnyAttachments = Authorization.Has(Claims.Job.Actions.RemoveAnyAttachments);
var canRemoveOwnAttachments = Authorization.Has(Claims.Job.Actions.RemoveOwnAttachments);
if (canShowAttachments)
{
Html.BundleDeferred("~/Style/Shadowbox");
Html.BundleDeferred("~/ClientScripts/Modules/Shadowbox");
}
if (canAddAttachments)
{
Html.BundleDeferred("~/ClientScripts/Modules/Disco-AttachmentUploader");
}
Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR");
}
<table id="jobShowResources">
<tr class="@(canShowLogs ? "canShowLogs" : "cannotShowLogs") @(canShowAttachments ? "canShowAttachments" : "cannotShowAttachments")">
@if (canShowLogs)
{
<td id="CommentsContainer">
<div id="Comments" class="@(canAddLogs ? "canAddLogs" : "cannotAddLogs") @(canRemoveAnyLogs ? "canRemoveAnyLogs" : "cannotRemoveAnyLogs") @(canRemoveOwnLogs ? "canRemoveOwnLogs" : "cannotRemoveOwnLogs")" data-jobid="@Model.Job.Id" data-addurl="@Url.Action(MVC.API.Job.CommentAdd(Model.Job.Id))" data-removeurl="@Url.Action(MVC.API.Job.CommentRemove())" data-geturl="@Url.Action(MVC.API.Job.Comment())" data-userid="@CurrentUser.UserId">
<div class="commentOutput">
@foreach (var jl in Model.Job.JobLogs.OrderBy(m => m.Timestamp))
{
<div data-logid="@jl.Id">
<span class="author">@jl.TechUser.ToStringFriendly()</span>@if (canRemoveAnyLogs || (canRemoveOwnLogs && jl.TechUserId.Equals(CurrentUser.UserId, StringComparison.OrdinalIgnoreCase)))
{<text><span class="remove fa fa-times-circle"></span></text>}<span class="timestamp" data-livestamp="@(jl.Timestamp.ToUnixEpoc())" title="@jl.Timestamp.ToFullDateTime()">@jl.Timestamp.ToFullDateTime()</span>
<div class="comment">@jl.Comments.ToHtmlComment()</div>
</div>
}
</div>
@if (canAddLogs)
{
<div class="commentInput clearfix">
<textarea class="commentInput" placeholder="add comment..." accesskey="l"></textarea>
<span class="action post commentInputPost fa fa-comment disabled" title="Add Comment"></span>
</div>
}
</div>
</td>
}
@if (canShowAttachments)
{
<td id="AttachmentsContainer">
<div id="Attachments" class="@(canAddAttachments ? "canAddAttachments" : "cannotAddAttachments") @(canRemoveAnyAttachments ? "canRemoveAnyAttachments" : "cannotRemoveAnyAttachments") @(canRemoveOwnAttachments ? "canRemoveOwnAttachments" : "cannotRemoveOwnAttachments")" data-userid="@CurrentUser.UserId" data-uploadurl="@(Url.Action(MVC.API.Job.AttachmentUpload(Model.Job.Id, null)))" data-onlineuploadurl="@(Url.Action(MVC.API.Job.AttachmentOnlineUploadSession(Model.Job.Id)))" data-qrcodeurl="@Url.Content("~/ClientSource/Scripts/Modules/qrcode.min.js")" data-removeurl="@Url.Action(MVC.API.Job.AttachmentRemove())">
<div class="Disco-AttachmentUpload-DropTarget">
<h2>Drop Attachments Here</h2>
</div>
<div class="attachmentOutput">
@foreach (var ja in Model.Job.JobAttachments.OrderByDescending(a => a.Id))
{
<a href="@Url.Action(MVC.API.Job.AttachmentDownload(ja.Id))" data-attachmentid="@ja.Id" data-mimetype="@ja.MimeType">
<span class="icon" title="@ja.Filename">
<img alt="Attachment Thumbnail" src="@(Url.Action(MVC.API.Job.AttachmentThumbnail(ja.Id)))" />
</span>
<span class="comments" title="@ja.Comments">
@{if (!string.IsNullOrEmpty(ja.DocumentTemplateId))
{ @ja.DocumentTemplate.Description}
else
{ @(ja.Comments ?? ja.Filename) }}
</span><span class="author">@ja.TechUser.ToStringFriendly()</span>@if (canRemoveAnyAttachments || (canRemoveOwnAttachments && ja.TechUserId.Equals(CurrentUser.UserId, StringComparison.OrdinalIgnoreCase)))
{<text><span class="remove fa fa-times-circle"></span></text>}<span class="timestamp" data-livestamp="@(ja.Timestamp.ToUnixEpoc())" title="@ja.Timestamp.ToFullDateTime()">@ja.Timestamp.ToFullDateTime()</span>
</a>
}
</div>
@if (canAddAttachments)
{
<div class="Disco-AttachmentUpload-Progress"></div>
<div class="attachmentInput clearfix">
<span class="action enabled upload fa fa-upload disabled" title="Attach File"></span><span class="action enabled photo fa fa-camera disabled" title="Capture Image"></span><span class="action enabled online-upload fa fa-qrcode disabled" title="Upload with Online Services"></span>
</div>
}
</div>
</td>
}
</tr>
</table>
@if (canShowLogs)
{
<script>
if (!document.DiscoFunctions) {
document.DiscoFunctions = {};
}
$(function () {
//#region Comments
const $Comments = $('#Comments');
const jobId = parseInt($Comments.attr('data-jobid'));
const $CommentOutput = $Comments.find('.commentOutput');
const $CommentOutputContainer = $Comments.find('.commentOutputContainer');
window.setTimeout(function () {
$CommentOutput[0].scrollTop = $CommentOutput[0].scrollHeight; // Scroll to Bottom
}, 0);
$('#jobDetailTabs').on('tabsactivate', function (event, ui) {
if (ui.newPanel && ui.newPanel.is('#jobDetailTab-Resources')) {
$CommentOutput[0].scrollTop = $CommentOutput[0].scrollHeight; // Scroll to Bottom
}
});
@if (canAddLogs)
{<text>
//#region Add Logs
const $CommentInput = $Comments.find('textarea.commentInput');
const $CommentInputPost = $Comments.find('.commentInputPost');
$CommentInputPost.on('click', postComment);
$CommentInput.on('keypress', function (e) {
if (e.which == 13 && !e.shiftKey) {
postComment();
return false;
}
});
async function postComment() {
if ($CommentInputPost.hasClass('disabled')) {
alert('Disconnected from the Disco ICT Server, please refresh this page and try again');
return;
}
const comment = $CommentInput.val();
if (comment == '') {
alert('Enter a comment to post');
$CommentInput.focus();
return;
}
$CommentInput.prop('disabled', true);
const formData = new FormData();
formData.append('__RequestVerificationToken', document.body.dataset.antiforgery);
formData.append('comment', comment);
const response = await fetch($Comments.attr('data-addurl'), {
method: 'POST',
body: formData
});
if (response.ok) {
$CommentInput.val('').prop('disabled', false).focus();
} else {
alert('Unable to add comment: ' + response.statusText);
$CommentInput.prop('disabled', false).focus();
}
}
//#endregion
</text>}
@if (canRemoveAnyLogs || canRemoveOwnLogs)
{<text>
//#region Remove Logs
let $dialogRemoveLog = null;
$CommentOutput.on('click', 'span.remove', removePost);
async function removeComment(commentId) {
const formData = new FormData();
formData.append('__RequestVerificationToken', document.body.dataset.antiforgery);
formData.append('id', commentId);
const response = await fetch($Comments.attr('data-removeurl'), {
method: 'POST',
body: formData
});
if (!response.ok) {
alert('Unable to remove comment: ' + response.statusText);
}
$dialogRemoveLog.dialog("close");
}
function removePost(e) {
e.preventDefault();
$this = $(this);
const commentId = $this.closest('div').attr('data-logid');
if (!$dialogRemoveLog) {
$dialogRemoveLog = $('<div class="dialog" title="Remove this Comment?"><p><i class="fa fa-exclamation-triangle fa-lg"></i>&nbsp;Are you sure?</p></div>')
.appendTo(document.body)
.dialog({
resizable: false,
height: 140,
modal: true,
autoOpen: false
});
}
$dialogRemoveLog.dialog("enable").dialog('option', 'buttons', {
"Remove": function () {
$dialogRemoveLog.dialog("disable");
$dialogRemoveLog.dialog("option", "buttons", null);
removeComment(commentId);
},
"Cancel": function () {
$dialogRemoveLog.dialog("close");
}
}).dialog('open');
}
//#endregion
</text>}
async function loadLiveComment(id) {
const formData = new FormData();
formData.append('__RequestVerificationToken', document.body.dataset.antiforgery);
formData.append('id', id);
const response = await fetch($Comments.attr('data-geturl'), {
method: 'POST',
body: formData
});
if (!response.ok) {
alert('Unable to load live comment ' + id + ': ' + response.statusText);
} else {
const comment = await response.json();
if ($Comments.hasClass('canRemoveAnyLogs'))
addComment(comment, false, true);
else if ($Comments.hasClass('canRemoveOwnLogs'))
addComment(comment, false, (comment.AuthorId === $Comments.attr('data-userid')));
else
addComment(comment, false, false);
}
}
function liveRemoveComment(id) {
$CommentOutput.children('div[data-logid="' + id + '"]').slideUp(300).delay(300).queue(function () {
const $this = $(this);
$this.find('.timestamp').livestamp('destroy');
$this.remove();
});
}
function addComment(c, quick, canRemove) {
let t = '<div><span class="author" />';
if (canRemove)
t += '<span class="remove fa fa-times-circle" />';
t += '<span class="timestamp" /><div class="comment" /></div>';
const e = $(t);
e.attr('data-logid', c.Id);
e.find('.author').text(c.Author);
e.find('.timestamp').text(c.TimestampFull).attr('title', c.TimestampFull).livestamp(c.TimestampUnixEpoc);
e.find('.comment').html(c.HtmlComments);
$CommentOutput.append(e);
if (!quick) {
e.animate({ backgroundColor: '#ffff99' }, 500, function () {
e.animate({ backgroundColor: '#fafafa' }, 500, function () {
e.css('background-color', '');
});
});
$CommentOutput.animate({ scrollTop: $CommentOutput[0].scrollHeight }, 250)
}
}
// Add Globally Available Functions
document.DiscoFunctions.liveLoadComment = function (id) {
loadLiveComment(id);
};
document.DiscoFunctions.liveRemoveComment = liveRemoveComment;
//#endregion
});
</script>
}
@if (canShowAttachments)
{
<script>
Shadowbox.init({
skipSetup: true,
modal: true
});
if (!document.DiscoFunctions) {
document.DiscoFunctions = {};
}
$(function () {
var jobId = parseInt('@(Model.Job.Id)');
//#region Attachments
var $Attachments = $('#Attachments');
var $attachmentOutput = $Attachments.find('.attachmentOutput');
let attachmentUploader = null;
@if (canAddAttachments)
{<text>
//#region Add Attachments
attachmentUploader = new document.Disco.AttachmentUploader($Attachments);
var $attachmentInput = $Attachments.find('.attachmentInput');
$attachmentInput.find('.online-upload').on('click', function () {
if ($(this).hasClass('disabled'))
alert('Disconnected from the Disco ICT Server, please refresh this page and try again');
else
attachmentUploader.onlineUpload();
});
if (window.location.protocol != 'https:') {
$attachmentInput.find('.photo')
.removeClass('enabled')
.addClass('disabled')
.attr('title', 'Capture Image: this functionality is only available over a HTTPS connection');
}
$attachmentInput.find('.photo').click(function () {
if (!$(this).hasClass('enabled'))
alert('This functionality is only available over a HTTPS connection');
else if ($(this).hasClass('disabled'))
alert('Disconnected from the Disco ICT Server, please refresh this page and try again');
else
attachmentUploader.uploadImage();
});
$attachmentInput.find('.upload').click(function () {
if ($(this).hasClass('disabled'))
alert('Disconnected from the Disco ICT Server, please refresh this page and try again');
else
attachmentUploader.uploadFiles();
});
var resourcesTab;
$(document).on('dragover', function () {
if (!resourcesTab) {
var tabs = $Attachments.closest('.ui-tabs');
resourcesTab = {
tabs: tabs,
resourcesIndex: tabs.children('ul.ui-tabs-nav').find('a[href="#jobDetailTab-Resources"]').closest('li').index()
};
}
var selectedIndex = resourcesTab.tabs.tabs('option', 'active');
if (resourcesTab.resourcesIndex !== selectedIndex)
resourcesTab.tabs.tabs('option', 'active', resourcesTab.resourcesIndex);
});
//#endregion
</text>}
@if (canRemoveAnyAttachments || canRemoveOwnAttachments)
{<text>
//#region Remove Attachments
var $dialogRemoveAttachment;
$attachmentOutput.find('span.remove').click(removeLocalAttachment);
function removeLocalAttachment() {
$this = $(this).closest('a');
if (!$dialogRemoveAttachment) {
$dialogRemoveAttachment = $('<div class="dialog" title="Remove this Attachment?"><p><i class="fa fa-exclamation-triangle fa-lg"></i>&nbsp;Are you sure?</p></div>')
.appendTo(document.body)
.dialog({
resizable: false,
height: 140,
modal: true,
autoOpen: false
});
}
async function removeAttachmentAsync(id) {
const body = new FormData();
body.append('__RequestVerificationToken', document.body.dataset.antiforgery);
body.append('id', id);
try {
const response = await fetch($Attachments.attr('data-removeurl'), {
body: body,
method: 'POST'
});
if (!response.ok) {
alert('Unable to remove attachment: ' + response.statusText);
}
$dialogRemoveAttachment.dialog("close");
} catch (e) {
alert('Unable to remove attachment: ' + e);
$dialogRemoveAttachment.dialog("close");
}
}
const attachmentId = $this.attr('data-attachmentid');
$dialogRemoveAttachment.dialog('option', 'buttons', {
"Remove": function () {
$dialogRemoveAttachment.dialog("option", "buttons", null);
removeAttachmentAsync(attachmentId);
},
Cancel: function () {
$dialogRemoveAttachment.dialog("close");
}
}).dialog('open');
return false;
}
//#endregion
</text>}
function addAttachment(key, quick) {
var data = { id: key };
$.ajax({
url: '@Url.Action(MVC.API.Job.Attachment())',
dataType: 'json',
data: data,
success: function (d) {
if (d.Result == 'OK') {
var a = d.Attachment;
if ($Attachments.hasClass('canRemoveAnyAttachments'))
buildAttachment(a, true, quick);
else if ($Attachments.hasClass('canRemoveOwnAttachments'))
buildAttachment(a, (a.AuthorId === $Attachments.attr('data-userid')), quick);
else
buildAttachment(a, false, quick);
if (attachmentUploader && attachmentUploader.onlineUploadCloseDialog) {
attachmentUploader.onlineUploadCloseDialog();
}
} else {
alert('Unable to add attachment: ' + d.Result);
}
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Unable to add attachment: ' + textStatus);
}
});
}
function buildAttachment(a, canRemove, quick) {
if (parseInt(a.ParentId) == jobId) {
var t = '<a><span class="icon"><img alt="Attachment Thumbnail" /></span><span class="comments"></span><span class="author"></span>';
if (canRemove)
t += '<span class="remove fa fa-times-circle"></span>';
t += '<span class="timestamp"></span></a>';
var e = $(t);
e.attr('data-attachmentid', a.Id).attr('data-mimetype', a.MimeType).attr('href', '@(Url.Action(MVC.API.Job.AttachmentDownload()))/' + a.Id);
e.find('.comments').text(a.Description);
e.find('.author').text(a.Author);
e.find('.timestamp').text(a.TimestampFull).attr('title', a.TimestampFull).livestamp(a.TimestampUnixEpoc);
if (canRemove)
e.find('.remove').click(removeLocalAttachment);
if (!quick)
e.hide();
$attachmentOutput.prepend(e);
document.DiscoFunctions.liveAfterUpdate();
if (!quick)
e.show('slow');
if (a.MimeType.toLowerCase().indexOf('image/') == 0)
e.shadowbox({ gallery: 'attachments', player: 'img', title: a.Description });
else
e.click(onDownload);
// Add Thumbnail
var buildThumbnail = function () {
var retryCount = 0;
var img = e.find('.icon img');
var setThumbnailUrl = function () {
img.attr('src', '@(Url.Action(MVC.API.Job.AttachmentThumbnail()))/' + a.Id + '?v=' + retryCount);
};
img.on('error', function () {
img.addClass('loading');
retryCount++;
if (retryCount < 6)
window.setTimeout(setThumbnailUrl, retryCount * 250);
});
img.on('load', function () {
img.removeClass('loading');
});
window.setTimeout(setThumbnailUrl, 100);
};
buildThumbnail();
}
}
function onDownload() {
var $this = $(this);
var url = $this.attr('href');
if ($.connection && $.connection.hub && $.connection.hub.transport &&
$.connection.hub.transport.name == 'foreverFrame') {
// SignalR active with foreverFrame transport - use popup window
window.open(url, '_blank', 'height=150,width=250,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no');
} else {
// use iFrame
if (!$attachmentDownloadHost) {
$attachmentDownloadHost = $('<iframe>')
.attr({ 'src': url, 'title': 'Attachment Download Host' })
.addClass('hidden')
.appendTo('body')
.contents();
} else {
$attachmentDownloadHost[0].location.href = url;
}
}
return false;
}
function removeAttachment(key) {
var $element = $attachmentOutput.find('a[data-attachmentid="' + key + '"]');
if ($element.length > 0) {
$element.hide(300).delay(300).queue(function () {
if ($element.attr('data-mimetype').toLowerCase().indexOf('image/') == 0)
Shadowbox.removeCache(this);
$element.remove();
document.DiscoFunctions.liveAfterUpdate();
});
}
}
$attachmentOutput.children('a').each(function () {
$this = $(this);
if ($this.attr('data-mimetype').toLowerCase().indexOf('image/') == 0)
$this.shadowbox({ gallery: 'attachments', player: 'img', title: $this.find('.comments').text() });
else
$this.click(onDownload);
});
// Add Globally Available Functions
document.DiscoFunctions.liveAddAttachment = addAttachment;
document.DiscoFunctions.liveRemoveAttachment = removeAttachment;
//#endregion
});
</script>
}
@if (canShowLogs || canShowAttachments)
{
<script>
$(function () {
var jobId = parseInt('@(Model.Job.Id)');
//#region LiveEvents
var hub = $.connection.jobUpdates;
// Map Functions
@if (canShowLogs)
{<text>
hub.client.addLog = document.DiscoFunctions.liveLoadComment;
hub.client.removeLog = document.DiscoFunctions.liveRemoveComment;
</text>}
@if (canShowAttachments)
{<text>
hub.client.addAttachment = document.DiscoFunctions.liveAddAttachment;
hub.client.removeAttachment = document.DiscoFunctions.liveRemoveAttachment;
document.DiscoFunctions.liveAfterUpdate = function () {
var tabLink = $('#jobDetailTab-ResourcesLink');
var attachmentCount = $('#Attachments').find('.attachmentOutput').children('a').length;
var tabHeading = tabLink.text();
tabHeading = tabHeading.substr(0, tabHeading.indexOf('[') + 1) + attachmentCount + ']';
tabLink.text(tabHeading);
}
</text>}
$.connection.hub.qs = { JobId: jobId };
$.connection.hub.reconnecting(function () {
$('#CommentsContainer').find('span.action').addClass('disabled');
$('#AttachmentsContainer').find('span.action.enabled').addClass('disabled');
});
$.connection.hub.reconnected(function () {
$('#CommentsContainer').find('span.action').removeClass('disabled');
$('#AttachmentsContainer').find('span.action.enabled').removeClass('disabled');
});
$.connection.hub.error(function (error) {
console.log('Server connection error: ' + error);
});
$.connection.hub.disconnected(function () {
// Disable UI
$('#CommentsContainer').find('textarea.commentInput').attr('readonly', 'readonly');
$('#CommentsContainer').find('span.action').addClass('disabled');
$('#AttachmentsContainer').find('span.action.enabled').addClass('disabled');
// Show Dialog Message
if ($('.disconnected-dialog').length == 0) {
$('<div>')
.addClass('dialog disconnected-dialog')
.html('<h3><span class="fa-stack fa-lg"><i class="fa fa-wifi fa-stack-1x"></i><i class="fa fa-ban fa-stack-2x error"></i></span>Disconnected from the Disco ICT Server</h3><div>This page is not receiving live updates. Please ensure you are connected to the server, then refresh this page to enable features.</div>')
.dialog({
resizable: false,
title: 'Disconnected',
width: 400,
modal: true,
buttons: {
'Refresh Now': function () {
$(this).dialog('option', 'buttons', null);
window.location.reload(true);
},
'Close': function () {
$(this).dialog('destroy');
}
}
});
}
});
// Start Connection
$.connection.hub.start(function () {
$('#CommentsContainer').find('span.action').removeClass('disabled');
$('#AttachmentsContainer').find('span.action.enabled').removeClass('disabled');
});
//#endregion
});
</script>
}