Add Option for Hiding Left Navigation (#3686)

* Add option for hiding left navigation

* Improve reviews page command bar

* Persist page settings as user preferences

* Smart update cache

* Add auto mapper for mapping between models
This commit is contained in:
Chidozie Ononiwu (His Righteousness) 2022-08-01 14:16:50 -07:00 коммит произвёл GitHub
Родитель 13a0b3bf6f
Коммит 8e661c62b1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 511 добавлений и 340 удалений

Просмотреть файл

@ -22,6 +22,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" /> <PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
<PackageReference Include="CsvHelper" Version="27.2.1" /> <PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.12.0-beta4" /> <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.12.0-beta4" />

Просмотреть файл

@ -571,6 +571,7 @@ code {
cursor: pointer; cursor: pointer;
width: 1%; width: 1%;
white-space: nowrap; white-space: nowrap;
padding: 0px;
} }
.line-details table { .line-details table {

Просмотреть файл

@ -1,33 +1 @@
import Split from "split.js"; 
addEventListener("load", () => {
$(".nav-list-toggle").click(function () {
$(this).parents(".nav-list-group").first().toggleClass("nav-list-collapsed");
});
});
$(() => {
/* 992px matches bootstrap col-lg min-width */
($('.namespace-view') as any).stickySidebar({ minWidth: 992 });
/* Split left and right review panes using split.js */
const rl = $('#review-left');
const rr = $('#review-right');
if (rl.length && rr.length) {
Split(['#review-left', '#review-right'], {
direction: 'horizontal',
sizes: [17, 83],
elementStyle: (dimension, size, gutterSize) => {
return {
'flex-basis': `calc(${size}% - ${gutterSize}px`
}
},
gutterStyle: (dimension, gutterSize) => {
return {
'flex-basis': `${gutterSize}px`
}
}
});
}
});

Просмотреть файл

@ -1,4 +1,6 @@
$(() => { import Split from "split.js";
$(() => {
const SEL_DOC_CLASS = ".documentation"; const SEL_DOC_CLASS = ".documentation";
const SHOW_DOC_CHECK_COMPONENT = "#show-documentation-component"; const SHOW_DOC_CHECK_COMPONENT = "#show-documentation-component";
const SHOW_DOC_CHECKBOX = ".show-doc-checkbox"; const SHOW_DOC_CHECKBOX = ".show-doc-checkbox";
@ -6,15 +8,73 @@
const SHOW_DIFFONLY_CHECKBOX = ".show-diffonly-checkbox"; const SHOW_DIFFONLY_CHECKBOX = ".show-diffonly-checkbox";
const SHOW_DIFFONLY_HREF = ".show-diffonly"; const SHOW_DIFFONLY_HREF = ".show-diffonly";
const HIDE_LINE_NUMBERS = "#hide-line-numbers"; const HIDE_LINE_NUMBERS = "#hide-line-numbers";
const HIDE_LEFT_NAVIGATION = "#hide-left-navigation";
hideCheckboxIfNoDocs(); hideCheckboxIfNoDocs();
/* FUNCTIONS
--------------------------------------------------------------------------------------------------------------------------------------------------------*/
function hideCheckboxIfNoDocs() { function hideCheckboxIfNoDocs() {
if ($(SEL_DOC_CLASS).length == 0) { if ($(SEL_DOC_CLASS).length == 0) {
$(SHOW_DOC_CHECK_COMPONENT).hide(); $(SHOW_DOC_CHECK_COMPONENT).hide();
} }
} }
function splitReviewPageContent() {
/* Split left and right review panes using split.js */
const rl = $('#review-left');
const rr = $('#review-right');
if (rl.length && rr.length) {
Split(['#review-left', '#review-right'], {
direction: 'horizontal',
sizes: [17, 83],
elementStyle: (dimension, size, gutterSize) => {
return {
'flex-basis': `calc(${size}% - ${gutterSize}px`
}
},
gutterStyle: (dimension, gutterSize) => {
return {
'flex-basis': `${gutterSize}px`
}
}
});
}
}
// Updated Page Setting by Updating UserPreference
function updatePageSettings(callBack) {
var hideLineNumbers = $(HIDE_LINE_NUMBERS).prop("checked");
var hideLeftNavigation = $(HIDE_LEFT_NAVIGATION).prop("checked");
var uri = `?handler=updatepagesettings&hideLineNumbers=${hideLineNumbers}&hideLeftNavigation=${hideLeftNavigation}`;
$.ajax({
type: "GET",
url: uri
}).done(callBack());
}
/* ADD EVENT LISTENER FOR TOGGLING LEFT NAVIGATION
--------------------------------------------------------------------------------------------------------------------------------------------------------*/
addEventListener("load", () => {
$(".nav-list-toggle").click(function () {
$(this).parents(".nav-list-group").first().toggleClass("nav-list-collapsed");
});
});
/* SPLIT REVIEW PAGE CONTENT
--------------------------------------------------------------------------------------------------------------------------------------------------------*/
/* 992px matches bootstrap col-lg min-width */
($('.namespace-view') as any).stickySidebar({ minWidth: 992 });
if (!$("#review-left").hasClass("d-none"))
{
// Only Add Split gutter if left navigation is not hidden
splitReviewPageContent();
}
/* TOGGLE PAGE OPTIONS
--------------------------------------------------------------------------------------------------------------------------------------------------------*/
$(SHOW_DOC_CHECKBOX).on("click", e => { $(SHOW_DOC_CHECKBOX).on("click", e => {
$(SHOW_DOC_HREF)[0].click(); $(SHOW_DOC_HREF)[0].click();
}); });
@ -24,17 +84,32 @@
}); });
$(HIDE_LINE_NUMBERS).on("click", e => { $(HIDE_LINE_NUMBERS).on("click", e => {
$(".line-number").toggleClass("d-none"); updatePageSettings(function(){
}); $(".line-number").toggleClass("d-none");
/* DIFF BUTTON (UPDATES REVIEW PAGE ON CLICK)
--------------------------------------------------------------------------------------------------------------------------------------------------------*/
$('.diff-button').each(function(index, value){
$(this).on('click', function () {
window.location.href = $(this).val() as string;
}); });
}); });
$(HIDE_LEFT_NAVIGATION).on("click", e => {
updatePageSettings(function(){
var leftContainer = $("#review-left");
var rightContainer = $("#review-right");
var gutter = $(".gutter-horizontal");
if (leftContainer.hasClass("d-none")) {
leftContainer.removeClass("d-none");
rightContainer.removeClass("col-12");
rightContainer.addClass("col-10");
splitReviewPageContent();
}
else {
leftContainer.addClass("d-none");
rightContainer.css("flex-basis", "100%");
gutter.remove();
rightContainer.removeClass("col-10");
rightContainer.addClass("col-12");
}
});
});
/* DROPDOWN FILTER FOR REVIEW, REVISIONS AND DIFF (UPDATES REVIEW PAGE ON CHANGE) /* DROPDOWN FILTER FOR REVIEW, REVISIONS AND DIFF (UPDATES REVIEW PAGE ON CHANGE)
--------------------------------------------------------------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------------------------------------------------------------*/
@ -59,7 +134,6 @@
var caretDirection = caretClasses ? caretClasses.split(' ').filter(c => c.startsWith('fa-angle-'))[0] : ""; var caretDirection = caretClasses ? caretClasses.split(' ').filter(c => c.startsWith('fa-angle-'))[0] : "";
var foldableClassPrefix = headingRowClasses ? headingRowClasses.split(' ').filter(c => c.endsWith('-heading'))[0].replace("-heading", "") : ""; var foldableClassPrefix = headingRowClasses ? headingRowClasses.split(' ').filter(c => c.endsWith('-heading'))[0].replace("-heading", "") : "";
if (triggeringClass == "row-fold-caret" && caretDirection == "fa-angle-down") { if (triggeringClass == "row-fold-caret" && caretDirection == "fa-angle-down") {
var classesOfRowsToHide = [`${foldableClassPrefix}-content`]; var classesOfRowsToHide = [`${foldableClassPrefix}-content`];

Просмотреть файл

@ -1,5 +1,4 @@
$(() => { $(() => {
// Search
const defaultPageSize = 50; const defaultPageSize = 50;
const reviewsFilterPartial = $( '#reviews-filter-partial' ); const reviewsFilterPartial = $( '#reviews-filter-partial' );
const languageFilter = $( '#language-filter-bootstraps-select' ); const languageFilter = $( '#language-filter-bootstraps-select' );
@ -77,10 +76,9 @@
}); });
} }
// Triggers partial page update to retriev properties for poulating filter dropdowns // Fetches data for populating dropdown options
function updateFilterDropDown(filter, query) function updateFilterDropDown(filter, query)
{ {
// update tags dropdown select
var uri = `?handler=reviews${query}`; var uri = `?handler=reviews${query}`;
var urlParams = new URLSearchParams(location.search); var urlParams = new URLSearchParams(location.search);
if (urlParams.has(query)) if (urlParams.has(query))
@ -97,20 +95,20 @@
}); });
} }
// Update content of dropdown on page load // Fetch content of dropdown on page load
$(document).ready(function() { $(document).ready(function() {
updateFilterDropDown(languageFilter, "languages"); updateFilterDropDown(languageFilter, "languages"); // Pulls languages data from DB
addPaginationEventHandlers(); addPaginationEventHandlers();
}); });
// Update list of reviews when any dropdown is changed
// Update when any dropdown is changed
[languageFilter, stateFilter, statusFilter, typeFilter].forEach(function(value, index) { [languageFilter, stateFilter, statusFilter, typeFilter].forEach(function(value, index) {
value.on('hidden.bs.select', function() { value.on('hidden.bs.select', function() {
updateListedReviews(); updateListedReviews();
}); });
}); });
// Update list of reviews based on search input
searchBox.on('input', _.debounce(function(e) { searchBox.on('input', _.debounce(function(e) {
updateListedReviews(); updateListedReviews();
}, 600)); }, 600));
@ -119,6 +117,7 @@
updateListedReviews(); updateListedReviews();
}); });
// Reset list of reviews as well as filters
resetButton.on('click', function(e) { resetButton.on('click', function(e) {
(<any>languageFilter).selectpicker('deselectAll'); (<any>languageFilter).selectpicker('deselectAll');
(<any>stateFilter).selectpicker('deselectAll').selectpicker('val', 'Open'); (<any>stateFilter).selectpicker('deselectAll').selectpicker('val', 'Open');

Просмотреть файл

@ -0,0 +1,21 @@
using APIViewWeb.Models;
using AutoMapper;
using Microsoft.CodeAnalysis.Diagnostics;
namespace APIViewWeb.Helpers
{
public class AutoMapperProfiles : Profile
{
public AutoMapperProfiles()
{
CreateMap<UserPreferenceModel, UserPreferenceModel>()
.ForMember(dest => dest.UserName, opt => opt.MapFrom((src, dest) => src.UserName != null ? src.UserName : dest.UserName))
.ForMember(dest => dest.Language, opt => opt.MapFrom((src, dest) => src.Language != null ? src.Language : dest.Language))
.ForMember(dest => dest.FilterType, opt => opt.MapFrom((src, dest) => src.FilterType != null ? src.FilterType : dest.FilterType))
.ForMember(dest => dest.State, opt => opt.MapFrom((src, dest) => src.State != null ? src.State : dest.State))
.ForMember(dest => dest.Status, opt => opt.MapFrom((src, dest) => src.Status != null ? src.Status : dest.Status))
.ForMember(dest => dest.HideLineNumbers, opt => opt.MapFrom((src, dest) => src.HideLineNumbers != null ? src.HideLineNumbers : dest.HideLineNumbers))
.ForMember(dest => dest.HideLeftNavigation, opt => opt.MapFrom((src, dest) => src.HideLeftNavigation != null ? src.HideLeftNavigation : dest.HideLeftNavigation));
}
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using CsvHelper.Configuration.Attributes; using CsvHelper.Configuration.Attributes;
namespace APIViewWeb.Models namespace APIViewWeb.Models
@ -12,5 +13,13 @@ namespace APIViewWeb.Models
public IEnumerable<string> Language { get; set; } public IEnumerable<string> Language { get; set; }
[Name("FilterType")] [Name("FilterType")]
public IEnumerable<ReviewType> FilterType { get; set; } public IEnumerable<ReviewType> FilterType { get; set; }
[Name("State")]
public IEnumerable<string> State { get; set; }
[Name("Status")]
public IEnumerable<string> Status { get; set; }
[Name("HideLineNumbers")]
public bool? HideLineNumbers { get; set; }
[Name("HideLeftNavigation")]
public bool? HideLeftNavigation { get; set; }
} }
} }

Просмотреть файл

@ -38,6 +38,20 @@ namespace APIViewWeb.Pages.Assemblies
List<string> search = null, List<string> languages=null, List<string> state =null, List<string> search = null, List<string> languages=null, List<string> state =null,
List<string> status =null, List<string> type =null, int pageNo=1, int pageSize=_defaultPageSize, string sortField=_defaultSortField) List<string> status =null, List<string> type =null, int pageNo=1, int pageSize=_defaultPageSize, string sortField=_defaultSortField)
{ {
if (search.Count == 0 && languages.Count == 0 && state.Count == 0 && status.Count == 0 && type.Count == 0)
{
UserPreferenceModel userPreference = _preferenceCache.GetUserPreferences(User.GetGitHubLogin());
if (userPreference != null)
{
languages = userPreference.Language.ToList();
state = userPreference.State.ToList();
status = userPreference.Status.ToList();
type = new List<string>();
if (userPreference.FilterType.Contains(ReviewType.Manual)) { type.Add("Manual"); }
if (userPreference.FilterType.Contains(ReviewType.Automatic)) { type.Add("Automatic"); }
if (userPreference.FilterType.Contains(ReviewType.PullRequest)) { type.Add("PullRequest"); }
}
}
await RunGetRequest(search, languages, state, status, type, pageNo, pageSize, sortField); await RunGetRequest(search, languages, state, status, type, pageNo, pageSize, sortField);
} }
@ -51,6 +65,14 @@ namespace APIViewWeb.Pages.Assemblies
public async Task<PartialViewResult> OnGetReviewsLanguagesAsync(List<string> selectedLanguages = null) public async Task<PartialViewResult> OnGetReviewsLanguagesAsync(List<string> selectedLanguages = null)
{ {
if (selectedLanguages.Count == 0)
{
UserPreferenceModel userPreference = _preferenceCache.GetUserPreferences(User.GetGitHubLogin());
if (userPreference != null)
{
selectedLanguages = userPreference.Language.ToList();
}
}
ReviewsProperties.Languages.All = await _manager.GetReviewPropertiesAsync("Revisions[0].Files[0].Language"); ReviewsProperties.Languages.All = await _manager.GetReviewPropertiesAsync("Revisions[0].Files[0].Language");
selectedLanguages = selectedLanguages.Select(x => HttpUtility.UrlDecode(x)).ToList(); selectedLanguages = selectedLanguages.Select(x => HttpUtility.UrlDecode(x)).ToList();
ReviewsProperties.Languages.Selected = selectedLanguages; ReviewsProperties.Languages.Selected = selectedLanguages;
@ -128,11 +150,12 @@ namespace APIViewWeb.Pages.Assemblies
if (type.Contains("Automatic")) { filterTypes.Add((int)ReviewType.Automatic); } if (type.Contains("Automatic")) { filterTypes.Add((int)ReviewType.Automatic); }
if (type.Contains("PullRequest")) { filterTypes.Add((int)ReviewType.PullRequest); } if (type.Contains("PullRequest")) { filterTypes.Add((int)ReviewType.PullRequest); }
_preferenceCache.UpdateUserPreference(new UserPreferenceModel() _preferenceCache.UpdateUserPreference(new UserPreferenceModel {
{
UserName = User.GetGitHubLogin(), UserName = User.GetGitHubLogin(),
FilterType = filterTypes.Cast<ReviewType>().ToList(), FilterType = filterTypes.Cast<ReviewType>().ToList(),
Language = languages Language = languages,
State = state,
Status = status
}); });
bool? isApproved = null; bool? isApproved = null;

Просмотреть файл

@ -6,268 +6,94 @@
} }
<div class="container-fluid"> <div class="container-fluid">
<div class="row mt-2 mb-0 pl-1"> <div class="row mx-1 px-0 py-2 border-bottom">
<div class="col"> <div class="col-lg-12 d-flex px-0">
<h3 class="mb-0"> @if (Model.Review.Language != null)
{
var imageSource = String.Empty;
@switch (Model.Review.Language.ToLower())
{
case "c#":
imageSource = "icons/csharp-original.svg";
break;
case "javascript":
imageSource = "icons/javascript-original.svg";
break;
case "python":
imageSource = "icons/python-original.svg";
break;
case "c":
imageSource = "icons/c-original.svg";
break;
case "c++":
imageSource = "icons/cplusplus-original.svg";
break;
case "go":
imageSource = "icons/go-original.svg";
break;
case "java":
imageSource = "icons/java-original.svg";
break;
case "swift":
imageSource = "icons/swift-original.svg";
break;
case "kotlin":
imageSource = "icons/kotlin-original.svg";
break;
case "json":
imageSource = "icons/json-original.svg";
break;
case "swagger":
imageSource = "icons/swagger-original.svg";
break;
}
if (String.IsNullOrEmpty(imageSource))
{
<span class="badge badge-info"><em>@Model.Review.Language</em></span>
}
else
{
<span><img class="mr-2 mt-1 p-1 border rounded" src="~/@imageSource" width="34" alt="@Model.Review.Language" /></span>
}
}
<p class="h6 mb-0 mr-auto">
@if (Model.Review.ServiceName != null) @if (Model.Review.ServiceName != null)
{ {
@Model.Review.ServiceName @Model.Review.ServiceName
<span>&nbsp;|&nbsp;</span>
} }
<br>
<small class="text-muted">@Model.Review.PackageDisplayName</small> <small class="text-muted">@Model.Review.PackageDisplayName</small>
@if (Model.Review.Language != null) </p>
<div class="my-1">
@if (Model.Revision.Approvers.Count > 0)
{ {
@switch (Model.Review.Language.ToLower()) var approvers = String.Join(", ", Model.Revision.Approvers);
{ <button type="button" class="btn btn-sm shadow-sm btn-success" data-placement="bottom" data-trigger="focus" data-toggle="popover" data-title="Approvers" data-content="@approvers">
case "c#": <i class="fas fa-check-circle"></i> APPROVED
<span><img class="mx-1" src="~/icons/csharp-original.svg" width="40" alt="@Model.Review.Language"></span> <span class="badge badge-light">@Model.Revision.Approvers.Count</span>
break; </button>
case "javascript": }
<span><img class="mx-1" src="~/icons/javascript-original.svg" width="40" alt="@Model.Review.Language"></span> else
break; {
case "python": <button type="button" class="btn btn-light btn-sm shadow-sm">
<span><img class="mx-1" src="~/icons/python-original.svg" width="40" alt="@Model.Review.Language"></span> PENDING
break; </button>
case "c":
<span><img class="mx-1" src="~/icons/c-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "c++":
<span><img class="mx-1" src="~/icons/cplusplus-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "go":
<span><img class="mx-1" src="~/icons/go-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "java":
<span><img class="mx-1" src="~/icons/java-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "swift":
<span><img class="mx-1" src="~/icons/swift-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "kotlin":
<span><img class="mx-1" src="~/icons/kotlin-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "json":
<span><img class="mx-1" src="~/icons/json-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
case "swagger":
<span><img class="mx-1" src="~/icons/swagger-original.svg" width="40" alt="@Model.Review.Language"></span>
break;
default:
<span class="badge badge-info"><em>@Model.Review.Language</em></span>
break;
}
} }
</h3>
</div>
</div>
<div class="row mb-1">
<div class="col-lg-6 my-1">
<div class="row no-gutters">
<div class="col-md-4 mt-1 p-0 mr-md-1">
<div class="input-group input-group-sm m-0 p-0 shadow-sm" id="review-select-box">
<div class="input-group-prepend">
<label class="input-group-text" for="review-bootstraps-select">Review</label>
</div>
<select class="selectpicker show-tick p-0" data-style="btn-light btn-sm border rounded-left-0 m-0" data-live-search="true" data-size="10" data-width="calc(100% - 60px)" data-container="body" id="review-bootstraps-select">
@foreach(var review in Model.ReviewsForPackage)
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = review.ReviewId
});
if (review.ReviewId == Model.Review.ReviewId)
{
<option selected value="@urlValue" data-subtext="Type: @review.FilterType | Language: @review.Language | Author: @review.Author">@review.DisplayName</option>
}
else
{
<option value="@urlValue" data-subtext="Type: @review.FilterType | Language: @review.Language | Author: @review.Author">@review.DisplayName</option>
}
}
</select>
</div>
</div>
<div class="col-md-4 mt-1 p-0 mx-md-1">
<div class="input-group input-group-sm m-0 p-0 shadow-sm" id="revision-select-box">
<div class="input-group-prepend">
<label class="input-group-text" for="revisions-bootstraps-select">Revision</label>
</div>
<select class="selectpicker show-tick p-0" data-style="btn-light btn-sm border rounded-left-0 m-0" data-live-search="true" data-size="10" data-width="calc(100% - 68px)" data-container="body" id="revisions-bootstraps-select">
@foreach (var revision in Model.Review.Revisions.Reverse())
{
var approvedBadge = "<i class='fas fa-check-circle text-success ml-2'></i>";
var optionName = revision.IsApproved ? $"{@revision.DisplayName} {@approvedBadge}" : @revision.DisplayName;
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @revision.RevisionId,
doc = @Model.ShowDocumentation
});
if (@revision.DisplayName == @Model.Revision.DisplayName)
{
<option selected value="@urlValue" data-content="@optionName"></option>
}
else
{
<option value="@urlValue" data-content="@optionName"></option>
}
}
</select>
</div>
</div>
<div class="col-md-3 mt-1 p-0 ml-md-1">
@if (@Model.PreviousRevisions.Any())
{
<div class="input-group input-group-sm m-0 p-0 shadow-sm" id="diff-select-box">
<div class="input-group-prepend" for="diff-bootstraps-select">
@if (Model.DiffRevisionId != null)
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @Model.Revision.RevisionId,
diffRevisionId = @Model.DiffRevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly
});
<button class="btn btn-outline-secondary diff-button" type="button" value="@urlValue" aria-label="Diff Button">Diff</button>
}
else
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @Model.Revision.RevisionId,
diffRevisionId = @Model.PreviousRevisions.Last().RevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly
});
<button class="btn btn-outline-secondary diff-button" type="button" value="@urlValue" aria-label="Diff Button">Diff</button>
}
</div>
<select class="selectpicker show-tick" data-style="btn-light btn-sm border border-left rounded-left-0" data-size="10" data-width="calc(100% - 39px)" data-live-search="true" data-container="body" aria-label="Diff Review" id="diff-bootstraps-select">
if (@Model.DiffRevisionId == null)
{
<option value="" selected></option>
}
@foreach (var revision in Model.PreviousRevisions.Reverse())
{
var approvedBadge = "<span class='badge badge-pill badge-success ml-1 p-1'><i class='fas fa-check-circle'></i> APPROVED</span>";
var optionName = revision.IsApproved ? $"{@revision.DisplayName} {@approvedBadge}" : @revision.DisplayName;
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
diffRevisionId = @revision.RevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly,
revisionId = @Model.Revision.RevisionId
});
if (@Model.DiffRevisionId != null)
{
if (@Model.DiffRevisionId == @revision.RevisionId)
{
<option value="@urlValue" selected data-content="@optionName"></option>
}
else
{
<option value="@urlValue" data-content="@optionName"></option>
}
}
else
{
if (@Model.DiffRevisionId != @revision.RevisionId)
{
<option value="@urlValue" data-content="@optionName"></option>
}
}
}
</select>
</div>
}
</div>
</div> </div>
</div> <div class="my-1 ml-2">
<div class="col-lg-6 my-1"> @{
@if (Model.Review.FilterType != ReviewType.Automatic) var popOverContent = $"<b>{Model.ActiveConversations}</b> active revision threads.<br><b>{Model.TotalActiveConversations}</b> total active threads.<br>"
{ + $"<b>Current Revision:</b> <em>{@Model.Revision.DisplayName}</em>";
<div class="float-right ml-2 my-1"> @if (Model.DiffRevisionId != null)
<form asp-resource="@Model.Review" class="form-inline" method="post" asp-page-handler="ToggleClosed">
@if (Model.Review.IsClosed)
{
<button type="submit" class="btn btn-sm border shadow-sm btn-light">
<i class="fa fa-sm fa-undo" aria-hidden="true"></i>&nbsp;&nbsp;Reopen
</button>
}
else
{
<button type="submit" class="btn btn-sm border shadow-sm btn-light">
<i class="far fa-window-close" aria-hidden="true"></i>&nbsp;&nbsp;Close
</button>
}
</form>
</div>
}
<div class="float-right ml-2 my-1">
<form asp-resource="@Model.Review" class="form-inline" method="post" asp-page-handler="ToggleSubscribed">
@if (Model.Review.GetUserEmail(User) != null)
{ {
if (Model.Review.IsUserSubscribed(User)) popOverContent += $"<br><b>Current Diff:</b> <em>{@Model.DiffRevision?.DisplayName}</em>";
{
<button type="submit" class="btn border btn-sm shadow-sm btn-light">
<i class="far fa-minus-square" aria-hidden="true"></i>&nbsp;&nbsp;Unsubscribe
</button>
}
else
{
<button type="submit" class="btn border btn-sm shadow-sm btn-light">
<i class="far fa-plus-square" aria-hidden="true"></i>&nbsp;&nbsp;Subscribe
</button>
}
} }
else <button type="button" class="btn btn-info btn-sm shadow-sm" data-placement="bottom" data-trigger="focus" data-toggle="popover" data-html="true" data-title="Page Info" data-content="@popOverContent">
{ <i class="far fa-comment-alt mr-2"></i><span class="badge badge-light">@Model.ActiveConversations / @Model.TotalActiveConversations</span>
<button type="submit" class="btn btn-sm shadow-sm btn-light" disabled data-placement="bottom" data-toggle="tooltip" title="Link a microsoft.com email to your Github account to subscribe"> </button>
<i class="fa fa-plus-square" aria-hidden="true"></i>&nbsp;&nbsp;Subscribe }
</button>
}
</form>
</div> </div>
<div class="dropdown d-inline-block float-right ml-2 my-1"> <div class="my-1">
<a class="btn btn-light btn-sm border shadow-sm dropdown-toggle" href="#" role="button" data-toggle="dropdown">
<i class="fas fa-sm fa-sliders-h"></i>&nbsp;&nbsp;Options
</a>
<div class="dropdown-menu dropdown-menu-right">
<span class="dropdown-item checkbox">
<label>
<input type="checkbox" checked="checked" id="show-comments-checkbox">
&nbsp;Show Comments
</label>
</span>
<span class="dropdown-item checkbox">
<label>
<input type="checkbox" checked="checked" id="show-system-comments-checkbox">
&nbsp;Show System Comments
</label>
</span>
<span class="dropdown-item" id="show-documentation-component">
<input asp-for="@Model.ShowDocumentation" class="show-doc-checkbox">
<a class="text-dark show-document" asp-all-route-data=@Model.GetRoutingData(diffRevisionId: Model.DiffRevisionId, showDocumentation: !Model.ShowDocumentation, showDiffOnly: Model.ShowDiffOnly, revisionId: Model.Revision.RevisionId)>
<label>&nbsp;Show Documentation</label>
</a>
</span>
<span class="dropdown-item checkbox">
<label>
<input type="checkbox" id="hide-line-numbers">
&nbsp;Hide Line Number
</label>
</span>
@if (!String.IsNullOrEmpty(Model.DiffRevisionId))
{
<span class="dropdown-item">
<input asp-for="@Model.ShowDiffOnly" class="show-diffonly-checkbox">
<a class="text-dark show-diffonly" asp-all-route-data=@Model.GetRoutingData(diffRevisionId: Model.DiffRevisionId, showDocumentation: Model.ShowDocumentation, showDiffOnly: !Model.ShowDiffOnly, revisionId: Model.Revision.RevisionId)>
<label>&nbsp;Show Only Diff</label>
</a>
</span>
}
</div>
</div>
<div class="float-right my-1">
<form asp-resource="@Model.Review" class="form-inline" asp-page-handler="ToggleApproval" method="post" asp-requirement="@ApproverRequirement.Instance"> <form asp-resource="@Model.Review" class="form-inline" asp-page-handler="ToggleApproval" method="post" asp-requirement="@ApproverRequirement.Instance">
<input type="hidden" name="revisionId" value="@Model.Revision.RevisionId" /> <input type="hidden" name="revisionId" value="@Model.Revision.RevisionId" />
@if (Model.DiffRevision == null || Model.DiffRevision.Approvers.Count > 0) @if (Model.DiffRevision == null || Model.DiffRevision.Approvers.Count > 0)
@ -302,39 +128,237 @@
} }
</form> </form>
</div> </div>
<div class="float-right ml-2 my-1"> <div class="dropdown d-inline-block my-1 ml-2">
@{ <a class="btn btn-light btn-sm border shadow-sm dropdown-toggle" href="#" role="button" data-toggle="dropdown">
var popOverContent = $"<b>{Model.ActiveConversations}</b> active revision threads.<br><b>{Model.TotalActiveConversations}</b> total active threads.<br>" <i class="fas fa-sm fa-sliders-h"></i>&nbsp;&nbsp;Options
+ $"<b>Current Revision:</b> <em>{@Model.Revision.DisplayName}</em>"; </a>
@if (Model.DiffRevisionId != null) <div class="dropdown-menu dropdown-menu-right">
<span class="dropdown-item checkbox">
<label>
<input type="checkbox" checked="checked" id="show-comments-checkbox">
&nbsp;Show Comments
</label>
</span>
<span class="dropdown-item checkbox">
<label>
<input type="checkbox" checked="checked" id="show-system-comments-checkbox">
&nbsp;Show System Comments
</label>
</span>
<span class="dropdown-item" id="show-documentation-component">
<input asp-for="@Model.ShowDocumentation" class="show-doc-checkbox">
<a class="text-dark show-document" asp-all-route-data=@Model.GetRoutingData(diffRevisionId: Model.DiffRevisionId, showDocumentation: !Model.ShowDocumentation, showDiffOnly: Model.ShowDiffOnly, revisionId: Model.Revision.RevisionId)>
<label>&nbsp;Show Documentation</label>
</a>
</span>
<span class="dropdown-item checkbox">
<label>
@if (Model.GetUserPreference().HideLineNumbers == true)
{
<input type="checkbox" id="hide-line-numbers" checked>
}
else
{
<input type="checkbox" id="hide-line-numbers" >
}
&nbsp;Hide Line Number
</label>
</span>
<span class="dropdown-item checkbox">
<label>
@if (Model.GetUserPreference().HideLeftNavigation == true)
{
<input type="checkbox" id="hide-left-navigation" checked>
}
else
{
<input type="checkbox" id="hide-left-navigation">
}
&nbsp;Hide Left Navigation
</label>
</span>
@if (!String.IsNullOrEmpty(Model.DiffRevisionId))
{ {
popOverContent += $"<br><b>Current Diff:</b> <em>{@Model.DiffRevision?.DisplayName}</em>"; <span class="dropdown-item">
<input asp-for="@Model.ShowDiffOnly" class="show-diffonly-checkbox">
<a class="text-dark show-diffonly" asp-all-route-data=@Model.GetRoutingData(diffRevisionId: Model.DiffRevisionId, showDocumentation: Model.ShowDocumentation, showDiffOnly: !Model.ShowDiffOnly, revisionId: Model.Revision.RevisionId)>
<label>&nbsp;Show Only Diff</label>
</a>
</span>
} }
<button type="button" class="btn btn-info btn-sm shadow-sm" data-placement="bottom" data-trigger="focus" data-toggle="popover" data-html="true" data-title="Page Info" data-content="@popOverContent"> </div>
<i class="far fa-comment-alt mr-2"></i><span class="badge badge-light">@Model.ActiveConversations / @Model.TotalActiveConversations</span> </div>
<div class="my-1 ml-2">
<form asp-resource="@Model.Review" class="form-inline" method="post" asp-page-handler="ToggleSubscribed">
@if (Model.Review.GetUserEmail(User) != null)
{
if (Model.Review.IsUserSubscribed(User))
{
<button type="submit" class="btn border btn-sm shadow-sm btn-light">
<i class="far fa-minus-square" aria-hidden="true"></i>&nbsp;&nbsp;Unsubscribe
</button>
}
else
{
<button type="submit" class="btn border btn-sm shadow-sm btn-light">
<i class="far fa-plus-square" aria-hidden="true"></i>&nbsp;&nbsp;Subscribe
</button>
}
}
else
{
<button type="submit" class="btn btn-sm shadow-sm btn-light" disabled data-placement="bottom" data-toggle="tooltip" title="Link a microsoft.com email to your Github account to subscribe">
<i class="fa fa-plus-square" aria-hidden="true"></i>&nbsp;&nbsp;Subscribe
</button> </button>
} }
</div> </form>
<div class="float-right ml-2 my-1">
@if (Model.Revision.Approvers.Count > 0)
{
var approvers = String.Join(", ", Model.Revision.Approvers);
<button type="button" class="btn btn-sm shadow-sm btn-success" data-placement="bottom" data-trigger="focus" data-toggle="popover" data-title="Approvers" data-content="@approvers">
<i class="fas fa-check-circle"></i> APPROVED
<span class="badge badge-light">@Model.Revision.Approvers.Count</span>
</button>
}
else
{
<button type="button" class="btn btn-light btn-sm shadow-sm">
PENDING
</button>
}
</div> </div>
@if (Model.Review.FilterType != ReviewType.Automatic)
{
<div class="my-1 ml-2">
<form asp-resource="@Model.Review" class="form-inline" method="post" asp-page-handler="ToggleClosed">
@if (Model.Review.IsClosed)
{
<button type="submit" class="btn btn-sm border shadow-sm btn-light">
<i class="fa fa-sm fa-undo" aria-hidden="true"></i>&nbsp;&nbsp;Reopen
</button>
}
else
{
<button type="submit" class="btn btn-sm border shadow-sm btn-light">
<i class="far fa-window-close" aria-hidden="true"></i>&nbsp;&nbsp;Close
</button>
}
</form>
</div>
}
</div> </div>
</div> </div>
<div class="row mx-1 px-0 pt-0 pb-2">
<form>
<div class="form-row">
@* Hide until tags is implemented
<div class="form-group col-md-4 mb-0">
<label class="mb-0 ml-1"><small>REVIEW:</small></label>
<select class="selectpicker show-tick show-menu-arrow shadow-sm" data-style="btn-light btn-sm border" data-selected-text-format="value" data-live-search="true" data-size="10" data-width="100%" data-container="body" id="review-bootstraps-select" data-tick-icon="fa-solid fa-check">
@foreach(var review in Model.ReviewsForPackage)
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = review.ReviewId
});
if (review.ReviewId == Model.Review.ReviewId)
{
<option selected style="font-size: 0.8em;" value="@urlValue" data-subtext="Type: @review.FilterType | Language: @review.Language | Author: @review.Author">@review.DisplayName</option>
}
else
{
<option style="font-size: 0.8em;" value="@urlValue" data-subtext="Type: @review.FilterType | Language: @review.Language | Author: @review.Author">@review.DisplayName</option>
}
}
</select>
</div>
*@
<div class="form-group col-md-6 mb-0">
<label class="mb-0 ml-1"><small>REVISION:</small></label>
<select class="selectpicker show-tick show-menu-arrow shadow-sm" data-style="btn-light btn-sm border" data-selected-text-format="value" data-live-search="true" data-size="10" data-width="100%" data-container="body" id="revisions-bootstraps-select" data-tick-icon="fa-solid fa-check">
@foreach (var revision in Model.Review.Revisions.Reverse())
{
var approvedBadge = "<i class='fas fa-check-circle text-success ml-2'></i>";
var optionName = revision.IsApproved ? $"{@revision.DisplayName} {@approvedBadge}" : @revision.DisplayName;
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @revision.RevisionId,
doc = @Model.ShowDocumentation
});
if (@revision.DisplayName == @Model.Revision.DisplayName)
{
<option selected style="font-size: 0.8em;" value="@urlValue" data-content="@optionName"></option>
}
else
{
<option style="font-size: 0.8em;" value="@urlValue" data-content="@optionName"></option>
}
}
</select>
</div>
<div class="form-group col-md-6 mb-0">
@if (@Model.PreviousRevisions.Any())
{
@if (Model.DiffRevisionId != null)
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @Model.Revision.RevisionId,
diffRevisionId = @Model.DiffRevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly
});
<label class="mb-0 ml-1"><a href="@urlValue"><small>DIFF WITH:</small></a></label>
}
else
{
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
revisionId = @Model.Revision.RevisionId,
diffRevisionId = @Model.PreviousRevisions.Last().RevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly
});
<label class="mb-0 ml-1"><a href="@urlValue"><small>DIFF:</small></a></label>
}
<select class="selectpicker show-tick show-menu-arrow shadow-sm" data-style="btn-light btn-sm border border-left" data-size="10" data-width="100%" data-live-search="true" data-container="body" aria-label="Diff Review" id="diff-bootstraps-select" data-tick-icon="fa-solid fa-check">
if (@Model.DiffRevisionId == null)
{
<option style="font-size: 0.8em;" value="" selected></option>
}
@foreach (var revision in Model.PreviousRevisions.Reverse())
{
var approvedBadge = "<i class='fas fa-check-circle text-success ml-2'></i>";
var optionName = revision.IsApproved ? $"{@revision.DisplayName} {@approvedBadge}" : @revision.DisplayName;
var urlValue = @Url.ActionLink("Review", "Assemblies", new {
id = @Model.Review.ReviewId,
diffRevisionId = @revision.RevisionId,
doc = @Model.ShowDocumentation,
diffOnly = @Model.ShowDiffOnly,
revisionId = @Model.Revision.RevisionId
});
if (@Model.DiffRevisionId != null)
{
if (@Model.DiffRevisionId == @revision.RevisionId)
{
<option style="font-size: 0.8em;" value="@urlValue" selected data-content="@optionName"></option>
}
else
{
<option style="font-size: 0.8em;" value="@urlValue" data-content="@optionName"></option>
}
}
else
{
if (@Model.DiffRevisionId != @revision.RevisionId)
{
<option style="font-size: 0.8em;" value="@urlValue" data-content="@optionName"></option>
}
}
}
</select>
}
</div>
</div>
</form>
</div>
<div class="row px-3" data-review-id="@Model.Review.ReviewId" data-revision-id="@Model.Revision.RevisionId" data-language="@Model.Review.Language"> <div class="row px-3" data-review-id="@Model.Review.ReviewId" data-revision-id="@Model.Revision.RevisionId" data-language="@Model.Review.Language">
<div id="review-left" class="col-2 rounded-1 border"> @{
var reviewLeftDisplay = String.Empty;
var reviewRightSize = "10";
if (Model.GetUserPreference().HideLeftNavigation == true)
{
reviewLeftDisplay = "d-none";
reviewRightSize = "12";
}
}
<div id="review-left" class="col-2 rounded-1 border @reviewLeftDisplay">
<div class="namespace-view"> <div class="namespace-view">
@if (Model.CodeFile != null) @if (Model.CodeFile != null)
{ {
@ -343,9 +367,12 @@
</div> </div>
</div> </div>
<div id="review-right" class="col-10 rounded-1 border"> <div id="review-right" class="col-@reviewRightSize rounded-1 border">
<table class="code-window"> <table class="code-window">
<tbody> <tbody>
@{
TempData["UserPreference"] = Model.GetUserPreference();
}
@foreach (var line in Model.Lines) @foreach (var line in Model.Lines)
{ {
<partial name="_CodeLine" model="@line" /> <partial name="_CodeLine" model="@line" />

Просмотреть файл

@ -239,6 +239,17 @@ namespace APIViewWeb.Pages.Assemblies
await _manager.ToggleApprovalAsync(User, id, revisionId); await _manager.ToggleApprovalAsync(User, id, revisionId);
return RedirectToPage(new { id = id }); return RedirectToPage(new { id = id });
} }
public IActionResult OnGetUpdatePageSettings(bool hideLineNumbers = false, bool hideLeftNavigation = false)
{
_preferenceCache.UpdateUserPreference(new UserPreferenceModel() {
UserName = User.GetGitHubLogin(),
HideLeftNavigation = hideLeftNavigation,
HideLineNumbers = hideLineNumbers
});
return new EmptyResult();
}
public Dictionary<string, string> GetRoutingData(string diffRevisionId = null, bool? showDocumentation = null, bool? showDiffOnly = null, string revisionId = null) public Dictionary<string, string> GetRoutingData(string diffRevisionId = null, bool? showDocumentation = null, bool? showDiffOnly = null, string revisionId = null)
{ {
var routingData = new Dictionary<string, string>(); var routingData = new Dictionary<string, string>();
@ -248,5 +259,10 @@ namespace APIViewWeb.Pages.Assemblies
routingData["diffOnly"] = (showDiffOnly ?? false).ToString(); routingData["diffOnly"] = (showDiffOnly ?? false).ToString();
return routingData; return routingData;
} }
public UserPreferenceModel GetUserPreference()
{
return _preferenceCache.GetUserPreferences(User.GetGitHubLogin());
}
} }
} }

Просмотреть файл

@ -1,6 +1,7 @@
@using ApiView @using ApiView
@using APIView.DIff @using APIView.DIff
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using APIViewWeb.Models;
@model APIViewWeb.Models.CodeLineModel @model APIViewWeb.Models.CodeLineModel
@{ @{
bool isRemoved = Model.Kind == DiffLineKind.Removed; bool isRemoved = Model.Kind == DiffLineKind.Removed;
@ -22,14 +23,23 @@
isContent = foldableClassParts.Any(x => x.EndsWith("-content")); isContent = foldableClassParts.Any(x => x.EndsWith("-content"));
hiddenSectionClass = isContent ? "d-none" : ""; hiddenSectionClass = isContent ? "d-none" : "";
} }
var userPreference = TempData["UserPreference"] as UserPreferenceModel;
} }
<tr class="code-line @lineClass @foldableClass @hiddenSectionClass" data-line-id="@(isRemoved ? string.Empty : Model.CodeLine.ElementId)"> <tr class="code-line @lineClass @foldableClass @hiddenSectionClass" data-line-id="@(isRemoved ? string.Empty : Model.CodeLine.ElementId)">
<td class="line-details"> <td class="line-details">
<table> <table>
<tr> <tr>
<td class="line-number @lineClass"><span>@Model.LineNumber</span></td> @if(userPreference.HideLineNumbers == true)
<td class="line-details-button-cell"> {
<td class="line-number @lineClass d-none"><span>@Model.LineNumber</span></td>
}
else
{
<td class="line-number @lineClass"><span>@Model.LineNumber</span></td>
}
<td class="line-details-button-cell @lineClass">
@if (!isRemoved && Model.CodeLine.ElementId != null) @if (!isRemoved && Model.CodeLine.ElementId != null)
{ {
<a class="line-comment-button">+</a> <a class="line-comment-button">+</a>
@ -39,7 +49,7 @@
<a class="line-comment-button" style="visibility: hidden;">+</a> // Added for visual consistency <a class="line-comment-button" style="visibility: hidden;">+</a> // Added for visual consistency
} }
</td> </td>
<td class="line-details-button-cell"> <td class="line-details-button-cell @lineClass">
@if (isHeading) @if (isHeading)
{ {
<span class="row-fold-caret"><i class="fa-solid fa-angle-right"></i></span> <span class="row-fold-caret"><i class="fa-solid fa-angle-right"></i></span>

Просмотреть файл

@ -3,21 +3,42 @@ using Microsoft.Extensions.Caching.Memory;
using APIViewWeb.Models; using APIViewWeb.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AutoMapper;
namespace APIViewWeb.Repositories namespace APIViewWeb.Repositories
{ {
public class UserPreferenceCache public class UserPreferenceCache
{ {
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly IMapper _mapper;
public UserPreferenceCache(IMemoryCache cache) public UserPreferenceCache(IMemoryCache cache, IMapper mapper)
{ {
_cache = cache; _cache = cache;
_mapper = mapper;
} }
public void UpdateUserPreference(UserPreferenceModel preference) public void UpdateUserPreference(UserPreferenceModel preference)
{ {
_cache.Set(preference.UserName, preference); UserPreferenceModel existingPreference = GetUserPreferences(preference.UserName);
if (existingPreference == null)
{
_cache.Set(preference.UserName, preference);
}
else
{
_mapper.Map<UserPreferenceModel, UserPreferenceModel>(preference, existingPreference);
_cache.Set(preference.UserName, existingPreference);
}
}
public UserPreferenceModel GetUserPreferences(string userName)
{
if (_cache.TryGetValue(userName, out UserPreferenceModel _preference))
{
return _preference;
}
return null;
} }
public IEnumerable<string> GetLangauge(string userName) public IEnumerable<string> GetLangauge(string userName)

Просмотреть файл

@ -194,6 +194,7 @@ namespace APIViewWeb
services.AddSingleton<IAuthorizationHandler, PullRequestPermissionRequirementHandler>(); services.AddSingleton<IAuthorizationHandler, PullRequestPermissionRequirementHandler>();
services.AddHostedService<ReviewBackgroundHostedService>(); services.AddHostedService<ReviewBackgroundHostedService>();
services.AddHostedService<PullRequestBackgroundHostedService>(); services.AddHostedService<PullRequestBackgroundHostedService>();
services.AddAutoMapper(Assembly.GetExecutingAssembly());
} }
private static async Task<string> GetMicrosoftEmailAsync(OAuthCreatingTicketContext context) private static async Task<string> GetMicrosoftEmailAsync(OAuthCreatingTicketContext context)

Различия файлов скрыты, потому что одна или несколько строк слишком длинны