Add support for resolving comments (#221)
This commit is contained in:
Родитель
4ddbf48e24
Коммит
90f8df3bbd
|
@ -0,0 +1,96 @@
|
|||
# External variables:
|
||||
# ProjectFile - The project to build and test. This variable is defined in pipeline web ui because we want to be able to provide it at queue time and that isn't supported in yaml yet.
|
||||
# VersioningProps - A collection of extra msbuild versioning properties like OfficialBuildId, PreReleaseVersionLabel, and DotNetFinalVersionKind
|
||||
|
||||
trigger:
|
||||
- master
|
||||
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure-sdk-build-tools
|
||||
type: git
|
||||
name: internal/azure-sdk-build-tools
|
||||
|
||||
variables:
|
||||
DotNetCoreVersion: '2.1.503'
|
||||
WebProjectPath: 'src/dotnet/APIView/APIViewWeb/'
|
||||
NodeVersion: '12.x'
|
||||
|
||||
stages:
|
||||
- stage: 'Build_and_Test'
|
||||
jobs:
|
||||
- job: 'Build'
|
||||
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
|
||||
steps:
|
||||
- task: DotNetCoreInstaller@0
|
||||
displayName: 'Use .NET Core sdk $(DotNetCoreVersion)'
|
||||
inputs:
|
||||
version: '$(DotNetCoreVersion)'
|
||||
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '$(NodeVersion)'
|
||||
displayName: 'Use NodeJS $(NodeVersion)'
|
||||
|
||||
- script: |
|
||||
npm install -g npm@6.9.0
|
||||
displayName: "Install npm 6.9.0"
|
||||
|
||||
- script: |
|
||||
npm install $(WebProjectPath)
|
||||
displayName: "Install npm packages"
|
||||
|
||||
- task: Maven@3
|
||||
inputs:
|
||||
mavenPomFile: 'src/java/apiview-java-processor/pom.xml'
|
||||
goals: 'clean package'
|
||||
|
||||
- script: 'dotnet pack $(ProjectFile) -o $(Build.ArtifactStagingDirectory) -warnaserror $(VersioningProps)'
|
||||
displayName: 'Build and Package'
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_MULTILEVEL_LOOKUP: 0
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
condition: succeededOrFailed()
|
||||
displayName: 'Publish Artifacts'
|
||||
inputs:
|
||||
ArtifactName: packages
|
||||
|
||||
- job: 'Test'
|
||||
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
|
||||
steps:
|
||||
- task: DotNetCoreInstaller@0
|
||||
displayName: 'Use .NET Core sdk $(DotNetCoreVersion)'
|
||||
inputs:
|
||||
version: '$(DotNetCoreVersion)'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build & Test'
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_MULTILEVEL_LOOKUP: 0
|
||||
inputs:
|
||||
command: test
|
||||
projects: '$(ProjectFile)'
|
||||
arguments: --logger trx
|
||||
publishTestResults: false
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/*.trx'
|
||||
testRunTitle: 'Windows DotNet $(DotNetCoreVersion)'
|
||||
testResultsFormat: 'VSTest'
|
||||
mergeTestResults: true
|
||||
|
||||
- ${{ if ne(variables['System.TeamProject'], 'Public') }}:
|
||||
- template: pipelines/stages/net-release-blobfeed.yml@azure-sdk-build-tools
|
|
@ -471,3 +471,8 @@ local.settings.json
|
|||
# Java tooling
|
||||
*.iml
|
||||
|
||||
# Typescript output
|
||||
src/dotnet/APIView/APIViewWeb/wwwroot/js/*.js*
|
||||
|
||||
# npm
|
||||
*-lock.json
|
|
@ -1,7 +1,6 @@
|
|||
<Project Sdk="Microsoft.Build.Traversal">
|
||||
<ItemGroup>
|
||||
<ProjectReference
|
||||
Include="src\dotnet\**\*.csproj"
|
||||
Exclude="src\dotnet\Mgmt.CI.BuildTools\**\*.*" />
|
||||
<ProjectReference
|
||||
Include="src\dotnet\Azure.ClientSdk.Analyzers\**\*.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -13,11 +13,14 @@
|
|||
<WarningsAsErrors>NU5105</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.0.0-preview.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="3.6.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.9" />
|
||||
<PackageReference Include="Markdig.Signed" Version="0.17.1" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
using System.Linq;
|
||||
|
||||
namespace APIViewWeb.Models
|
||||
{
|
||||
public class AssemblyCommentsModel
|
||||
{
|
||||
public string AssemblyId { get; set; }
|
||||
public CommentModel[] Comments { get; set; }
|
||||
|
||||
public AssemblyCommentsModel() { }
|
||||
|
||||
public AssemblyCommentsModel(string assemblyId)
|
||||
{
|
||||
this.AssemblyId = assemblyId;
|
||||
this.Comments = new CommentModel[] { };
|
||||
}
|
||||
|
||||
public void AddComment(CommentModel comment)
|
||||
{
|
||||
Comments = Comments.Append(comment).ToArray();
|
||||
}
|
||||
|
||||
public void DeleteComment(string id)
|
||||
{
|
||||
Comments = Comments.Where(comment => comment.Id != id).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace APIViewWeb.Models
|
||||
{
|
||||
public class CommentModel
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string CommentId { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string ReviewId { get; set; }
|
||||
public string ElementId { get; set; }
|
||||
public string Comment { get; set; }
|
||||
public DateTime TimeStamp { get; set; }
|
||||
public string Username { get; set; }
|
||||
public bool IsResolve { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace APIViewWeb.Models
|
||||
{
|
||||
public class CommentThreadModel
|
||||
{
|
||||
public string AssemblyId { get; set; }
|
||||
public List<CommentModel> Comments { get; set; }
|
||||
public CommentThreadModel(string reviewId, string lineId, IEnumerable<CommentModel> comments)
|
||||
{
|
||||
ReviewId = reviewId;
|
||||
LineId = lineId;
|
||||
Comments = comments.Where(c => !c.IsResolve);
|
||||
var resolveComment = comments.FirstOrDefault(c => c.IsResolve);
|
||||
IsResolved = resolveComment != null;
|
||||
ResolvedBy = resolveComment?.Username;
|
||||
}
|
||||
|
||||
public string ReviewId { get; set; }
|
||||
public IEnumerable<CommentModel> Comments { get; set; }
|
||||
public string LineId { get; set; }
|
||||
public bool IsResolved { get; set; }
|
||||
public string ResolvedBy { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using APIViewWeb.Models;
|
||||
|
||||
namespace APIViewWeb
|
||||
{
|
||||
public class ReviewCommentsModel
|
||||
{
|
||||
private Dictionary<string, CommentThreadModel> _threads;
|
||||
|
||||
public ReviewCommentsModel(string reviewId, IEnumerable<CommentModel> comments)
|
||||
{
|
||||
_threads = comments.OrderBy(c => c.TimeStamp)
|
||||
.GroupBy(c => c.ElementId)
|
||||
.ToDictionary(c => c.Key ?? string.Empty, c => new CommentThreadModel(reviewId, c.Key, c));
|
||||
}
|
||||
|
||||
public IEnumerable<CommentThreadModel> Threads => _threads.Values;
|
||||
|
||||
public bool TryGetThreadForLine(string lineId, out CommentThreadModel threadModel)
|
||||
{
|
||||
return _threads.TryGetValue(lineId, out threadModel);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
<h2>This review can not be viewed anymore but here are the comments:</h2>
|
||||
|
||||
@foreach (var commentThread in Model.Comments)
|
||||
@foreach (var commentThread in Model.Comments.Threads)
|
||||
{
|
||||
<pre>@commentThread.Key</pre>
|
||||
<partial name="_CommentThreadPartial" model="@new APIViewWeb.Models.CommentThreadModel() { AssemblyId = Model.Id, Comments = commentThread.Value, LineId = commentThread.Key }" />
|
||||
<pre>@commentThread.LineId</pre>
|
||||
<partial name="_CommentThreadPartial" model="@commentThread" />
|
||||
}
|
|
@ -11,32 +11,21 @@ namespace APIViewWeb.Pages.Assemblies
|
|||
{
|
||||
public class LegacyReview: PageModel
|
||||
{
|
||||
private readonly CosmosCommentsRepository commentRepository;
|
||||
private CommentsManager _commentsManager;
|
||||
|
||||
public LegacyReview(CosmosCommentsRepository commentRepository)
|
||||
public LegacyReview(CommentsManager commentsManager)
|
||||
{
|
||||
this.commentRepository = commentRepository;
|
||||
_commentsManager = commentsManager;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public Dictionary<string, List<CommentModel>> Comments { get; set; }
|
||||
public ReviewCommentsModel Comments { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
Id = id;
|
||||
Comments = new Dictionary<string, List<CommentModel>>();
|
||||
|
||||
var assemblyComments = await commentRepository.GetCommentsAsync(id);
|
||||
|
||||
foreach (var comment in assemblyComments)
|
||||
{
|
||||
if (!Comments.TryGetValue(comment.ElementId, out _))
|
||||
Comments[comment.ElementId] = new List<CommentModel>() { comment };
|
||||
else
|
||||
Comments[comment.ElementId].Add(comment);
|
||||
}
|
||||
|
||||
Comments = await _commentsManager.GetReviewCommentsAsync(id);
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<tr class="code-line">
|
||||
<td class="line-comment-button-cell">
|
||||
@if (line.ElementId != null) {
|
||||
<button data-element-id="@line.ElementId" class="btn-link line-comment-button" type="button" style="text-decoration:none;">+</button>
|
||||
<button data-line-id="@line.ElementId" class="btn-link line-comment-button" type="button" style="text-decoration:none;">+</button>
|
||||
}
|
||||
</td>
|
||||
<td class="code"><span class="code-inner">@Html.Raw(line.DisplayString)</span></td>
|
||||
|
@ -58,10 +58,9 @@
|
|||
</td>
|
||||
}
|
||||
</tr>
|
||||
@foreach (var elementId in Model.Comments.Keys) {
|
||||
@if (elementId == line.ElementId) {
|
||||
<partial name="_CommentThreadPartial" model="@(new Models.CommentThreadModel() { AssemblyId = Model.Review.ReviewId, Comments = Model.Comments[elementId], LineId = elementId })" />
|
||||
}
|
||||
|
||||
@if (line.ElementId != null && Model.Comments.TryGetThreadForLine(line.ElementId, out var thread)) {
|
||||
<partial name="_CommentThreadPartial" model="thread" />
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
|
@ -70,14 +69,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="comment-form-template">
|
||||
<div class="comment-form border-top new-thread-comment">
|
||||
<form class="new-thread-comment-form comment" method="post" asp-route-id="@Model.Review.ReviewId">
|
||||
<input type="hidden" asp-for="Comment.ElementId" class="id-box" />
|
||||
<div class="new-comment-content">
|
||||
<textarea class="new-thread-comment-text form-control" asp-for="Comment.Comment" rows="3"></textarea>
|
||||
</div>
|
||||
<button type="submit" name="submit" value="Submit" class="comment-submit-button btn btn-outline-dark">Add Comment</button>
|
||||
<button type="button" name="cancel" value="Cancel" class="comment-cancel-button btn btn-outline-dark">Cancel</button>
|
||||
</form>
|
||||
<div class="comment-form border-top new-thread-comment">
|
||||
<form class="new-thread-comment-form comment" method="post" asp-route-id="@Model.Review.ReviewId">
|
||||
<input type="hidden" asp-for="Comment.ElementId" class="elementIdInput" />
|
||||
<div class="new-comment-content">
|
||||
<textarea class="new-thread-comment-text form-control" asp-for="Comment.Comment" rows="3"></textarea>
|
||||
</div>
|
||||
<button data-post-update="comments" type="submit" name="submit" value="Submit" class="comment-submit-button btn btn-outline-dark">Add Comment</button>
|
||||
<button type="button" name="cancel" value="Cancel" class="comment-cancel-button btn btn-outline-dark">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -17,46 +17,37 @@ namespace APIViewWeb.Pages.Assemblies
|
|||
|
||||
private readonly BlobCodeFileRepository _codeFileRepository;
|
||||
|
||||
private readonly CosmosCommentsRepository _commentRepository;
|
||||
private readonly CommentsManager _commentsManager;
|
||||
|
||||
public ReviewPageModel(
|
||||
ReviewManager manager,
|
||||
BlobCodeFileRepository codeFileRepository,
|
||||
CosmosCommentsRepository commentRepository)
|
||||
CommentsManager commentsManager)
|
||||
{
|
||||
_manager = manager;
|
||||
_codeFileRepository = codeFileRepository;
|
||||
_commentRepository = commentRepository;
|
||||
_commentsManager = commentsManager;
|
||||
}
|
||||
|
||||
public ReviewModel Review { get; set; }
|
||||
public CodeFile CodeFile { get; set; }
|
||||
public LineApiView[] Lines { get; set; }
|
||||
public Dictionary<string, List<CommentModel>> Comments { get; set; }
|
||||
public ReviewCommentsModel Comments { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public CommentModel Comment { get; set; }
|
||||
|
||||
public async Task<ActionResult> OnPostDeleteAsync(string id, string commentId, string elementId)
|
||||
{
|
||||
var comment = await _commentRepository.GetCommentAsync(id, commentId);
|
||||
await _commentRepository.DeleteCommentAsync(comment);
|
||||
await _commentsManager.DeleteCommentAsync(User, id, commentId);
|
||||
|
||||
return await CommentPartialAsync(id, elementId);
|
||||
}
|
||||
|
||||
private async Task<ActionResult> CommentPartialAsync(string id, string elementId)
|
||||
{
|
||||
var commentArray = await _commentRepository.GetCommentsAsync(id);
|
||||
List<CommentModel> comments = commentArray.Where(c => c.ElementId == elementId).ToList();
|
||||
|
||||
CommentThreadModel partialModel = new CommentThreadModel()
|
||||
{
|
||||
AssemblyId = id,
|
||||
Comments = comments,
|
||||
LineId = Comment.ElementId
|
||||
};
|
||||
|
||||
var comments = await _commentsManager.GetReviewCommentsAsync(id);
|
||||
comments.TryGetThreadForLine(elementId, out var partialModel);
|
||||
return new PartialViewResult
|
||||
{
|
||||
ViewName = "_CommentThreadPartial",
|
||||
|
@ -79,17 +70,7 @@ namespace APIViewWeb.Pages.Assemblies
|
|||
}
|
||||
|
||||
Lines = new CodeFileHtmlRenderer().Render(CodeFile).ToArray();
|
||||
Comments = new Dictionary<string, List<CommentModel>>();
|
||||
|
||||
var assemblyComments = await _commentRepository.GetCommentsAsync(id);
|
||||
|
||||
foreach (var comment in assemblyComments)
|
||||
{
|
||||
if (!Comments.TryGetValue(comment.ElementId, out _))
|
||||
Comments[comment.ElementId] = new List<CommentModel>() { comment };
|
||||
else
|
||||
Comments[comment.ElementId].Add(comment);
|
||||
}
|
||||
Comments = await _commentsManager.GetReviewCommentsAsync(id);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
@ -97,14 +78,28 @@ namespace APIViewWeb.Pages.Assemblies
|
|||
public async Task<ActionResult> OnPostAsync(string id)
|
||||
{
|
||||
Comment.TimeStamp = DateTime.UtcNow;
|
||||
Comment.Username = User.GetGitHubLogin();
|
||||
Comment.ReviewId = id;
|
||||
|
||||
await _commentRepository.UpsertCommentAsync(Comment);
|
||||
await _commentsManager.AddCommentAsync(User, Comment);
|
||||
|
||||
return await CommentPartialAsync(id, Comment.ElementId);
|
||||
}
|
||||
|
||||
|
||||
public async Task<ActionResult> OnPostResolveAsync(string id, string lineId)
|
||||
{
|
||||
await _commentsManager.ResolveConversation(User, id, lineId);
|
||||
|
||||
return await CommentPartialAsync(id, lineId);
|
||||
}
|
||||
|
||||
public async Task<ActionResult> OnPostUnresolveAsync(string id, string lineId)
|
||||
{
|
||||
await _commentsManager.UnresolveConversation(User, id, lineId);
|
||||
|
||||
return await CommentPartialAsync(id, lineId);
|
||||
}
|
||||
|
||||
public async Task<ActionResult> OnPostRefreshModelAsync(string id)
|
||||
{
|
||||
await _manager.UpdateReviewAsync(User, id);
|
||||
|
|
|
@ -7,51 +7,70 @@
|
|||
|
||||
@if (Model.Comments.Any())
|
||||
{
|
||||
<tr class="comment-box">
|
||||
<td class="comment-cell border-top border-bottom" colspan="4">
|
||||
<div class="border comment-holder rounded-1">
|
||||
<div class="comment-thread-contents">
|
||||
@foreach (var comment in Model.Comments)
|
||||
<tr class="comment-box" data-line-id="@Model.LineId">
|
||||
<td class="comment-cell border-top border-bottom" colspan="4">
|
||||
@if (Model.IsResolved)
|
||||
{
|
||||
@:This thread is marked resolved by @Model.ResolvedBy <a href="#" class="toggle-comments">(show)</a>
|
||||
}
|
||||
<div class="border comment-holder rounded-1 @(Model.IsResolved ? "comments-resolved" : "")">
|
||||
<div class="comment-thread-contents">
|
||||
@foreach (var comment in Model.Comments)
|
||||
{
|
||||
<div class="review-comment">
|
||||
<div class="unminimized-comment">
|
||||
<div class="comment-actions">
|
||||
<form class="comment-delete-button align-top" asp-page-handler="delete" method="post" asp-route-id="@Model.AssemblyId">
|
||||
<input type="hidden" name="commentId" value="@comment.Id" />
|
||||
<input type="hidden" name="elementId" value="@Model.LineId" />
|
||||
@if (comment.Username == @User.GetGitHubLogin())
|
||||
<div class="review-comment">
|
||||
<div class="unminimized-comment">
|
||||
<div class="comment-actions">
|
||||
<form class="comment-delete-button align-top" asp-page-handler="delete" method="post" asp-route-id="@Model.ReviewId">
|
||||
<input type="hidden" name="commentId" value="@comment.CommentId" />
|
||||
<input type="hidden" name="elementId" value="@Model.LineId" />
|
||||
@if (comment.Username == User.GetGitHubLogin())
|
||||
{
|
||||
<button id="@comment.Id" class="comment-delete-button-enabled btn align-top"><span class="comment-delete-text align-top">Delete</span></button>
|
||||
<button id="@comment.CommentId" data-post-update="comments" class="btn align-top"><span class="comment-delete-text align-top">Delete</span></button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button disabled class="btn align-top"><span class="comment-delete-text align-top">Delete</span></button>
|
||||
<button disabled class="btn align-top"><span class="comment-delete-text align-top">Delete</span></button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
<div class="media">
|
||||
<img username="@comment.Username" class="comment-icon align-self-start mr-3" height="28" width="28" />
|
||||
<div class="media-body comment-contents">
|
||||
<div>
|
||||
<strong class=" mt-0 comment-header author align-top">@comment.Username</strong>
|
||||
<span date="@comment.TimeStamp.ToLocalTime()" class="comment-timestamp align-top"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="media">
|
||||
<img username="@comment.Username" class="comment-icon align-self-start mr-3" height="28" width="28" />
|
||||
<div class="media-body comment-contents">
|
||||
<div>
|
||||
<strong class=" mt-0 comment-header author align-top">@comment.Username</strong>
|
||||
<span date="@comment.TimeStamp.ToLocalTime()" class="comment-timestamp align-top"></span>
|
||||
</div>
|
||||
@Html.FormatAsMarkdown(@comment.Comment)
|
||||
</div>
|
||||
</div>
|
||||
@Html.FormatAsMarkdown(@comment.Comment)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="review-thread-reply">
|
||||
<div class="reply-cell align-middle">
|
||||
<img avatar username="@User.GetGitHubLogin()" class="comment-icon" height="28" width="28" />
|
||||
<div class="review-thread-reply">
|
||||
<div class="reply-cell align-middle">
|
||||
<img avatar username="@User.GetGitHubLogin()" class="comment-icon" height="28" width="28" />
|
||||
</div>
|
||||
<div class="reply-cell col-12">
|
||||
<button type="button" class="review-thread-reply-button text-muted text-left form-control">Reply...</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reply-cell col-12">
|
||||
<button data-element-id="@Model.LineId" type="button" class="review-thread-reply-button text-muted text-left form-control">Reply...</button>
|
||||
|
||||
<div class="m-2">
|
||||
@if (Model.IsResolved)
|
||||
{
|
||||
<form method="post" asp-page-handler="Unresolve" asp-route-id="@Model.ReviewId">
|
||||
<button data-post-update="comments" type="submit" name="submit" value="Submit" class="thread-resolve-button btn btn-outline-dark">Unresolve</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-page-handler="Resolve" asp-route-id="@Model.ReviewId">
|
||||
<button data-post-update="comments" type="submit" name="submit" value="Submit" class="thread-resolve-button btn btn-outline-dark">Resolve</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using APIViewWeb.Models;
|
||||
|
||||
namespace APIViewWeb
|
||||
{
|
||||
public class CommentsManager
|
||||
{
|
||||
private readonly CosmosCommentsRepository _commentsRepository;
|
||||
|
||||
public CommentsManager(CosmosCommentsRepository commentsRepository)
|
||||
{
|
||||
_commentsRepository = commentsRepository;
|
||||
}
|
||||
|
||||
public async Task<ReviewCommentsModel> GetReviewCommentsAsync(string reviewId)
|
||||
{
|
||||
var comments = await _commentsRepository.GetCommentsAsync(reviewId);
|
||||
|
||||
return new ReviewCommentsModel(reviewId, comments);
|
||||
}
|
||||
|
||||
public async Task AddCommentAsync(ClaimsPrincipal user, CommentModel comment)
|
||||
{
|
||||
comment.Username = user.GetGitHubLogin();
|
||||
comment.TimeStamp = DateTime.Now;
|
||||
|
||||
await _commentsRepository.UpsertCommentAsync(comment);
|
||||
}
|
||||
|
||||
public async Task DeleteCommentAsync(ClaimsPrincipal user, string reviewId, string commentId)
|
||||
{
|
||||
var comment = await _commentsRepository.GetCommentAsync(reviewId, commentId);
|
||||
await _commentsRepository.DeleteCommentAsync(comment);
|
||||
}
|
||||
|
||||
public async Task ResolveConversation(ClaimsPrincipal user, string reviewId, string lineId)
|
||||
{
|
||||
await AddCommentAsync(user, new CommentModel()
|
||||
{
|
||||
IsResolve = true,
|
||||
ReviewId = reviewId,
|
||||
ElementId = lineId
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UnresolveConversation(ClaimsPrincipal user, string reviewId, string lineId)
|
||||
{
|
||||
|
||||
var comments = await _commentsRepository.GetCommentsAsync(reviewId, lineId);
|
||||
foreach (var comment in comments)
|
||||
{
|
||||
if (comment.IsResolve)
|
||||
{
|
||||
await _commentsRepository.DeleteCommentAsync(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,15 +21,7 @@ namespace APIViewWeb
|
|||
|
||||
public async Task<IEnumerable<CommentModel>> GetCommentsAsync(string reviewId)
|
||||
{
|
||||
var allReviews = new List<CommentModel>();
|
||||
var itemQueryIterator = _commentsContainer.GetItemQueryIterator<CommentModel>($"SELECT * FROM Comments c WHERE c.ReviewId = '{reviewId}'");
|
||||
while (itemQueryIterator.HasMoreResults)
|
||||
{
|
||||
var result = await itemQueryIterator.ReadNextAsync();
|
||||
allReviews.AddRange(result.Resource);
|
||||
}
|
||||
|
||||
return allReviews;
|
||||
return await GetCommentsFromQueryAsync($"SELECT * FROM Comments c WHERE c.ReviewId = '{reviewId}'");
|
||||
}
|
||||
|
||||
public async Task UpsertCommentAsync(CommentModel commentModel)
|
||||
|
@ -37,9 +29,9 @@ namespace APIViewWeb
|
|||
await _commentsContainer.UpsertItemAsync(commentModel, new PartitionKey(commentModel.ReviewId));
|
||||
}
|
||||
|
||||
public async Task DeleteCommentAsync(CommentModel reviewModel)
|
||||
public async Task DeleteCommentAsync(CommentModel commentModel)
|
||||
{
|
||||
await _commentsContainer.DeleteItemAsync<CommentModel>(reviewModel.Id, new PartitionKey(reviewModel.ReviewId));
|
||||
await _commentsContainer.DeleteItemAsync<CommentModel>(commentModel.CommentId, new PartitionKey(commentModel.ReviewId));
|
||||
}
|
||||
|
||||
public async Task DeleteCommentsAsync(string reviewId)
|
||||
|
@ -54,5 +46,24 @@ namespace APIViewWeb
|
|||
{
|
||||
return await _commentsContainer.ReadItemAsync<CommentModel>(commentId, new PartitionKey(reviewId));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CommentModel>> GetCommentsAsync(string reviewId, string lineId)
|
||||
{
|
||||
return await GetCommentsFromQueryAsync($"SELECT * FROM Comments c WHERE c.ReviewId = '{reviewId}' AND c.ElementId = '{lineId}'");
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<CommentModel>> GetCommentsFromQueryAsync(string query)
|
||||
{
|
||||
var allReviews = new List<CommentModel>();
|
||||
var itemQueryIterator = _commentsContainer.GetItemQueryIterator<CommentModel>(query);
|
||||
while (itemQueryIterator.HasMoreResults)
|
||||
{
|
||||
var result = await itemQueryIterator.ReadNextAsync();
|
||||
allReviews.AddRange(result.Resource);
|
||||
}
|
||||
|
||||
return allReviews;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -10,12 +10,10 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using APIViewWeb.Pages.Assemblies;
|
||||
using APIViewWeb.Respositories;
|
||||
|
||||
namespace APIViewWeb
|
||||
|
@ -53,6 +51,7 @@ namespace APIViewWeb
|
|||
services.AddSingleton<CosmosCommentsRepository>();
|
||||
|
||||
services.AddSingleton<ReviewManager>();
|
||||
services.AddSingleton<CommentsManager>();
|
||||
|
||||
services.AddSingleton<ILanguageService, JsonLanguageService>();
|
||||
services.AddSingleton<ILanguageService, CSharpLanguageService>();
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "asp.net",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"jquery": "3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "4.0.2",
|
||||
"@types/jquery": "3.3.31"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "none",
|
||||
"noEmitOnError": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"jquery"
|
||||
],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015.iterable"
|
||||
],
|
||||
"target": "es5",
|
||||
"outFile": "wwwroot/js/site.js"
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
"wwwroot/js/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -179,6 +179,10 @@ form.comment {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comments-resolved {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.commentable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -245,6 +249,7 @@ code-inner {
|
|||
background-color: #F6F8FA;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 1px solid #E1E4E8;
|
||||
border-bottom: 1px solid #E1E4E8;
|
||||
padding: 8px 16px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
$(() => {
|
||||
let commentFormTemplate = $("#comment-form-template");
|
||||
|
||||
$(document).on("click", ".commentable", e => {
|
||||
showCommentBox(e.target.id);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", ".line-comment-button", e => {
|
||||
showCommentBox(getLineId(e.target));
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", ".comment-cancel-button", e => {
|
||||
hideCommentBox(getLineId(e.target));
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", "[data-post-update='comments']", e => {
|
||||
const form = <HTMLFormElement><any>$(e.target).closest("form");
|
||||
let lineId = getLineId(e.target);
|
||||
let commentRow = getCommentBox(lineId);
|
||||
let serializedForm = form.serializeArray();
|
||||
serializedForm.push({ name: "lineId", value: lineId });
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: $(form).prop("action"),
|
||||
data: $.param(serializedForm)
|
||||
}).done(partialViewResult => {
|
||||
updateCommentThread(commentRow, partialViewResult);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", ".review-thread-reply-button", e => {
|
||||
showCommentBox(getLineId(e.target));
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", ".toggle-comments", e => {
|
||||
toggleComments(getLineId(e.target));
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
function getLineId(element) {
|
||||
return $(element).closest("[data-line-id]").data("line-id");
|
||||
}
|
||||
|
||||
function toggleComments(id) {
|
||||
getCommentBox(id).find(".comment-holder").toggle();
|
||||
}
|
||||
|
||||
function getCommentBox(id) {
|
||||
return $(`.comment-box[data-line-id='${id}']`);
|
||||
}
|
||||
|
||||
function hideCommentBox(id) {
|
||||
var thisRow = $(document.getElementById(id)).parents(".code-line").first();
|
||||
let diagnosticsRow = thisRow.next();
|
||||
let nextRow = diagnosticsRow.next();
|
||||
nextRow.find(".review-thread-reply").show();
|
||||
nextRow.find(".comment-form").hide();
|
||||
}
|
||||
|
||||
function showCommentBox(id) {
|
||||
let thisRow = $(document.getElementById(id)).parents(".code-line").first();
|
||||
let diagnosticsRow = thisRow.next();
|
||||
let nextRow = diagnosticsRow.next();
|
||||
let commentBox = nextRow.find(".comment-form");
|
||||
|
||||
if (commentBox.length === 0) {
|
||||
commentBox = commentFormTemplate.children().clone();
|
||||
|
||||
var thread = nextRow.find(".comment-thread-contents");
|
||||
if (thread.length > 0) {
|
||||
thread.after(commentBox);
|
||||
}
|
||||
else {
|
||||
commentBox.insertAfter(diagnosticsRow).wrap(`<tr class="comment-box" data-line-id="${id}">`).wrap("<td colspan=\"2\">");
|
||||
}
|
||||
}
|
||||
|
||||
commentBox.show();
|
||||
commentBox.find(".elementIdInput").val(id);
|
||||
commentBox.find(".new-thread-comment-text").focus();
|
||||
nextRow.find(".review-thread-reply").hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateCommentThread(commentBox, partialViewResult) {
|
||||
partialViewResult = $.parseHTML(partialViewResult);
|
||||
$(commentBox).replaceWith(partialViewResult);
|
||||
return false;
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
addEventListener("load", () => {
|
||||
// Show file name when file is selected
|
||||
$(".custom-file-input").on("change", function() {
|
||||
const fileName = (<HTMLInputElement>this).files[0].name;
|
||||
$(this).next(".custom-file-label").html(fileName);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
addEventListener("load", () => {
|
||||
$(".nav-list-toggle").click(function () {
|
||||
$(this).parents(".nav-list-group").first().toggleClass("nav-list-collapsed");
|
||||
});
|
||||
});
|
|
@ -1,110 +0,0 @@
|
|||
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
// for details on configuring this project to bundle and minify static web assets.
|
||||
|
||||
// Write your Javascript code.
|
||||
$(function () {
|
||||
let commentFormTemplate = $("#comment-form-template");
|
||||
attachEventHandlers(document);
|
||||
|
||||
$(document).find(".nav-list-toggle").click(function() {
|
||||
$(this).parents(".nav-list-group").first().toggleClass("nav-list-collapsed");
|
||||
});
|
||||
|
||||
$(document).find(".commentable").click(function () {
|
||||
showCommentBox(this.id);
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).find(".line-comment-button").click(function () {
|
||||
showCommentBox($(this).data("element-id"));
|
||||
return false;
|
||||
});
|
||||
|
||||
function attachEventHandlers(element, id=null) {
|
||||
let thisRow = $(document.getElementById(id)).parents(".code-line").first();
|
||||
let diagnosticsRow = thisRow.next();
|
||||
let commentsRow = diagnosticsRow.next();
|
||||
|
||||
$(element).find(".comment-cancel-button").click(function () {
|
||||
hideCommentBox(id);
|
||||
return false;
|
||||
});
|
||||
$(element).find(".comment-submit-button").off().click(function () {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
data: element.find("form").serialize()
|
||||
}).done(function (partialViewResult) {
|
||||
updateCommentThread(commentsRow, partialViewResult);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$(element).find(".review-thread-reply-button").click(function () {
|
||||
showCommentBox($(this).data("element-id"));
|
||||
});
|
||||
|
||||
$(element).find(".comment-delete-button-enabled").click(function () {
|
||||
deleteComment(this.id);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function hideCommentBox(id) {
|
||||
var thisRow = $(document.getElementById(id)).parents(".code-line").first();
|
||||
let diagnosticsRow = thisRow.next();
|
||||
let nextRow = diagnosticsRow.next();
|
||||
nextRow.find(".review-thread-reply").show();
|
||||
nextRow.find(".comment-form").hide();
|
||||
}
|
||||
|
||||
function showCommentBox(id) {
|
||||
let thisRow = $(document.getElementById(id)).parents(".code-line").first();
|
||||
let diagnosticsRow = thisRow.next();
|
||||
let nextRow = diagnosticsRow.next();
|
||||
let commentBox = nextRow.find(".comment-form");
|
||||
|
||||
if (commentBox.length == 0) {
|
||||
commentBox = commentFormTemplate.children().clone();
|
||||
|
||||
var thread = nextRow.find(".comment-thread-contents");
|
||||
if (thread.length > 0) {
|
||||
thread.after(commentBox);
|
||||
}
|
||||
else {
|
||||
commentBox.insertAfter(diagnosticsRow).wrap("<tr>").wrap("<td colspan=\"2\">");
|
||||
}
|
||||
}
|
||||
|
||||
commentBox.show();
|
||||
commentBox.find(".id-box").val(id);
|
||||
commentBox.find(".new-thread-comment-text").focus();
|
||||
attachEventHandlers(commentBox, id);
|
||||
nextRow.find(".review-thread-reply").hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateCommentThread(commentBox, partialViewResult) {
|
||||
partialViewResult = $.parseHTML(partialViewResult);
|
||||
$(commentBox).replaceWith(partialViewResult);
|
||||
attachEventHandlers(partialViewResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteComment(id) {
|
||||
let button = document.getElementById(id);
|
||||
let commentBox = $(button).parents(".comment-box").first();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "?handler=delete",
|
||||
data: $(button).parents("form").serialize()
|
||||
}).done(function (partialViewResult) {
|
||||
updateCommentThread(commentBox, partialViewResult);
|
||||
});
|
||||
}
|
||||
|
||||
// Show file name when file is selected
|
||||
$('.custom-file-input').on('change', function() {
|
||||
var fileName = this.files[0].name;
|
||||
$(this).next('.custom-file-label').html(fileName);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче