diff --git a/.gitignore b/.gitignore index 51e12f0401e..fa5555525ff 100644 --- a/.gitignore +++ b/.gitignore @@ -486,7 +486,7 @@ local.settings.json *.iml # Typescript output -src/dotnet/APIView/APIViewWeb/wwwroot/js/*.js* +src/dotnet/APIView/APIViewWeb/wwwroot/*.js* # npm *-lock.json diff --git a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj index 77ef7fcc536..bf4367f495b 100644 --- a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj +++ b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/dotnet/APIView/APIViewWeb/Client/css/site.scss b/src/dotnet/APIView/APIViewWeb/Client/css/site.scss index 50d5b8f25aa..f8cad9ff5c8 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/css/site.scss +++ b/src/dotnet/APIView/APIViewWeb/Client/css/site.scss @@ -599,11 +599,16 @@ mark { text-overflow: ellipsis; } +#reviews-table-search-container { + min-width: 100px; + max-width: 40vw; +} + .bottom-right-floating { position:fixed; z-index: 10; - bottom:130px; - right:70px; + bottom:10%; + right:5%; } .btn-circle { diff --git a/src/dotnet/APIView/APIViewWeb/Client/package.json b/src/dotnet/APIView/APIViewWeb/Client/package.json index 7f37935d19b..56651c5f491 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/package.json +++ b/src/dotnet/APIView/APIViewWeb/Client/package.json @@ -10,14 +10,15 @@ "watch": "webpack --watch" }, "dependencies": { + "acorn": "^8.0.0", "core-js": "^3.3.2", "jquery": "3.6.0", "split.js": "^1.5.11", + "underscore": "^1.13.4", "vue": "^2.6.10", "vue-class-component": "^7.0.2", "vue-property-decorator": "^8.3.0", - "vuex": "^3.0.1", - "acorn": "^8.0.0" + "vuex": "^3.0.1" }, "devDependencies": { "@types/jquery": "3.3.31", diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/api.ts b/src/dotnet/APIView/APIViewWeb/Client/src/api.ts index 455a4e76661..a82dcb6d177 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/api.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/api.ts @@ -1,4 +1,5 @@ $(() => { + // Hide Line Numbers $(document).on("click", "#hide-line-numbers", e => { $(".line-number").toggleClass("line-number-hidden", e.target.checked); }); diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/main.ts b/src/dotnet/APIView/APIViewWeb/Client/src/main.ts index d5f9531b6e4..e69de29bb2d 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/main.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/main.ts @@ -1,7 +0,0 @@ -import "./comments.ts"; -import "./revisions.ts"; -import "./file-input.ts"; -import "./navbar.ts"; -import "./review.ts"; -import "./reviews.ts"; -import "./api.ts"; diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/reviews.ts b/src/dotnet/APIView/APIViewWeb/Client/src/reviews.ts index 947af13811d..24dd46e4006 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/reviews.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/reviews.ts @@ -1,188 +1,130 @@ $(() => { // Search - const languageSelect = $("#reviews-table-language-filter"); - const searchBox = $('#reviews-table-search-box'); - const searchContext = $('.review-name') as any; - const serviceGroupRows = $('.service-group-row'); - const packageGroupRows = $('.package-group-row'); - const reviewHeaderRows = $('.review-rows-header'); - const packageDataRows = $('.package-data-row'); + const defaultPageSize = 50; + const reviewsFilterPartial = $( '#reviews-filter-partial' ); + const languageFilter = $( '#language-filter-bootstraps-select' ); + const stateFilter = $( '#state-filter-bootstraps-select' ); + const statusFilter = $( '#status-filter-bootstraps-select' ); + const typeFilter = $( '#type-filter-bootstraps-select' ); + const searchBox = $( '#reviews-table-search-box' ); + const searchButton = $( '#reviews-search-button' ); + const resetButton = $( '#reset-filter-button' ); + + // Import underscorejs + var _ = require('underscore'); // Enable tooltip ($('[data-toggle="tooltip"]')).tooltip(); - function toggleServiceGroup(serviceRow, state) { - var serviceGroupTag = serviceRow[0].id; - var serviceRowIcon = serviceRow.find('i').first(); - var packageGroupRow = $(`.${serviceGroupTag}`); - if (state == "closed") - { - packageGroupRow.removeClass('hidden-row'); - serviceRowIcon.removeClass('fa-angle-right').addClass('fa-angle-down'); - serviceRow.addClass('shadow-sm'); - } - else - { - packageGroupRow.addClass('hidden-row'); - serviceRowIcon.removeClass('fa-angle-down').addClass('fa-angle-right'); - serviceRow.removeClass('shadow-sm'); - } - } + // Computes the uri string using the values of search, pagination and various filters + // Invokes partial page update to list of reviews using ajax + // Updates the uri displayed on the client + function updateListedReviews({ pageNo = 1, pageSize = defaultPageSize } = {}) + { + var uri = '?handler=reviewspartial'; + var searchQuery = searchBox.val() as string; - function togglePackageGroup(packageRow, state) { - var packageGroupTag = packageRow[0].id; - var packageRowIcon = packageRow.find('i').first(); - var packageDataRows = $(`.${packageGroupTag}`); - if (state == "closed") - { - packageDataRows.removeClass('hidden-row'); - packageRowIcon.removeClass('fa-angle-right').addClass('fa-angle-down'); - packageRow.addClass('shadow-sm'); - } - else + if (searchQuery != null && searchQuery.trim() != '') { - packageDataRows.addClass('hidden-row'); - packageRowIcon.removeClass('fa-angle-down').addClass('fa-angle-right'); - packageRow.removeClass('shadow-sm'); - } - } - - function filterReviews(){ - // highlight matching text using mark.js framework and hide rows that don't match - const searchText = (searchBox.val() as string).toUpperCase(); - searchContext.closest('tr').removeClass('hidden-row').unmark(); - if(searchText) - { - searchContext.mark(searchText, { - done: function () { - searchContext.not(':has(mark)').closest('tr').addClass('hidden-row'); - serviceGroupRows.addClass('hidden-row'); - packageGroupRows.addClass('hidden-row'); - reviewHeaderRows.addClass('hidden-row'); - } + var searchTerms = searchQuery.trim().split(/\s+/); + searchTerms.forEach(function(value, index){ + uri = uri + '&search=' + encodeURIComponent(value); }); } - else - { - serviceGroupRows.removeClass('hidden-row').addClass('shadow-sm'); - serviceGroupRows.find('i').removeClass('fa-angle-right').addClass('fa-angle-down'); - packageGroupRows.removeClass('hidden-row').addClass('shadow-sm'); - packageGroupRows.find('i').removeClass('fa-angle-right').addClass('fa-angle-down'); - reviewHeaderRows.removeClass('hidden-row').addClass('shadow-sm'); - } - } - // Expand all Service Groups - $('#expand-all-service-groups-btn').on('click', function () { - if (!(searchBox.val() as string)) - { - serviceGroupRows.each(function(index, value){ - toggleServiceGroup($(this), "closed"); - }); - } - }); + languageFilter.children(":selected").each(function() { + uri = uri + '&languages=' + encodeURIComponent(`${$(this).val()}`); + }); + + stateFilter.children(":selected").each(function() { + uri = uri + '&state=' + encodeURIComponent(`${$(this).val()}`); + }); - // Expand all Groups - $('#expand-all-groups-btn').on('click', function () { - if (!(searchBox.val() as string)) - { - serviceGroupRows.each(function(index, value){ - toggleServiceGroup($(this), "closed"); - }); - packageGroupRows.each(function(index, value){ - togglePackageGroup($(this), "closed"); - }); - } - }); + statusFilter.children(":selected").each(function() { + uri = uri + '&status=' + encodeURIComponent(`${$(this).val()}`); + }); - // Collapse all Groups - $('#collapse-all-groups-btn').on('click', function () { - if (!(searchBox.val() as string)) - { - serviceGroupRows.each(function(index, value){ - toggleServiceGroup($(this), "opened"); - }); - packageGroupRows.each(function(index, value){ - togglePackageGroup($(this), "opened"); - }); - } - }); + typeFilter.children(":selected").each(function() { + uri = uri + '&type=' + encodeURIComponent(`${$(this).val()}`); + }); - // Clear all filters - $('#clear-all-filters').on('click', function() { - if (languageSelect.val() != "") - { - languageSelect.val("").trigger('change'); - } - if (searchBox.val() != "") - { - searchBox.val("").trigger('input'); - } - }); + uri = uri + '&pageNo=' + encodeURIComponent(pageNo); + uri = uri + '&pageSize=' + encodeURIComponent(pageSize); + uri = encodeURI(uri); - // Toggle individual service Group - $('#reviews-table tbody').on('click', '.service-group-row', function() { - var serviceRowIcon = $(this).find('i').first(); - var serviceGroupID = $(this).first()[0].id; - if (serviceRowIcon.hasClass('fa-angle-right')) - { - toggleServiceGroup($(this), "closed"); - } - else - { - $(`.package-group-row.${serviceGroupID}`).each(function(index, value){ - var packageRowIcon = $(this).find('i').first(); - if (packageRowIcon.hasClass(`fa-angle-down`)) + $.ajax({ + url: uri + }).done(function(partialViewResult) { + reviewsFilterPartial.html(partialViewResult); + history.pushState({}, '', uri.replace('handler=reviewspartial&', '')); + addPaginationEventHandlers(); // This ensures that the event handlers are re-added after ajax refresh + }); + } + + // Add custom behaviour and event to pagination buttons + function addPaginationEventHandlers() + { + $( '.page-link' ).each(function() { + $(this).on('click', function(event){ + event.preventDefault(); + var linkParts = $(this).prop('href').split('/'); + var pageNo = linkParts[linkParts.length - 1]; + if (pageNo !== null && pageNo !== undefined) { - togglePackageGroup($(this), "opened"); + updateListedReviews({ pageNo: pageNo }); } }); - toggleServiceGroup($(this), "opened"); + }); + } + + // Triggers partial page update to retriev properties for poulating filter dropdowns + function updateFilterDropDown(filter, query) + { + // update tags dropdown select + var uri = `?handler=reviews${query}`; + var urlParams = new URLSearchParams(location.search); + if (urlParams.has(query)) + { + urlParams.getAll(query).forEach(function(value, index) { + uri = uri + `&selected${query}=` + encodeURIComponent(value); + }); } + $.ajax({ + url: uri + }).done(function(partialViewResult) { + filter.html(partialViewResult); + (filter).selectpicker('refresh'); + }); + } + + // Update content of dropdown on page load + $(document).ready(function() { + updateFilterDropDown(languageFilter, "languages"); + addPaginationEventHandlers(); }); - // Toggle individual package Group - $('#reviews-table tbody').on('click', '.package-group-row', function() { - var packageRowIcon = $(this).find('i').first(); - if (packageRowIcon.hasClass('fa-angle-right')) - { - togglePackageGroup($(this), "closed"); - } - else - { - togglePackageGroup($(this), "opened"); - } + + // Update when any dropdown is changed + [languageFilter, stateFilter, statusFilter, typeFilter].forEach(function(value, index) { + value.on('hidden.bs.select', function() { + updateListedReviews(); + }); }); - // If already populated from navigating back, filter again - if (searchBox.val()) { - filterReviews(); - } + searchBox.on('input', _.debounce(function(e) { + updateListedReviews(); + }, 300)); - searchBox.on('input', function() { - setTimeout(filterReviews, 300); + searchButton.on('click', function() { + updateListedReviews(); }); - // Filter by language - languageSelect.on('change', function(e) { - var filterText = $(this).val() as string; - if (filterText == "") - { - packageDataRows.removeClass('hidden-row-via-filter'); - } - else - { - packageDataRows.each(function (index, value) { - let langImageAlt = value.children[0].children[0].getAttribute("alt"); - if (langImageAlt != null && langImageAlt.match(RegExp(filterText))) - { - $(this).removeClass('hidden-row-via-filter'); - } - else - { - $(this).addClass('hidden-row-via-filter'); - } - }); - } + resetButton.on('click', function(e) { + (languageFilter).selectpicker('deselectAll'); + (stateFilter).selectpicker('deselectAll').selectpicker('val', 'Open'); + (statusFilter).selectpicker('deselectAll'); + (typeFilter).selectpicker('deselectAll'); + searchBox.val(''); + updateListedReviews(); }); }); diff --git a/src/dotnet/APIView/APIViewWeb/Client/webpack.config.js b/src/dotnet/APIView/APIViewWeb/Client/webpack.config.js index a1400c27bf2..e0348a79000 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/webpack.config.js +++ b/src/dotnet/APIView/APIViewWeb/Client/webpack.config.js @@ -5,10 +5,17 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { mode: "production", - entry: [ - './src/main.ts', - './css/site.scss' - ], + entry: { + comments: './src/comments.ts', + revisions: './src/revisions.ts', + fileInput: './src/file-input.ts', + navbar: './src/navbar.ts', + review: './src/review.ts', + reviews: './src/reviews.ts', + main: './src/main.ts', + api: './src/api.ts', + site: './css/site.scss' + }, devtool: 'source-map', module: { rules: [ @@ -53,7 +60,7 @@ module.exports = { extensions: [ '.tsx', '.ts', '.js' ], }, output: { - filename: 'site.js', + filename: '[name].js', path: path.resolve(__dirname, '../wwwroot'), }, } \ No newline at end of file diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs index db8e97fcc70..b25258b3ba5 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs @@ -73,7 +73,6 @@ public string DisplayName } } - [JsonIgnore] public DateTime LastUpdated => Revisions.LastOrDefault()?.CreationDate ?? CreationDate; [JsonIgnore] diff --git a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs index 235f01b0c84..bb5713520e5 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs @@ -1,4 +1,6 @@ -using CsvHelper.Configuration.Attributes; +using System.Collections; +using System.Collections.Generic; +using CsvHelper.Configuration.Attributes; namespace APIViewWeb.Models { @@ -7,8 +9,8 @@ public class UserPreferenceModel [Name("UserName")] public string UserName { get; set; } [Name("Language")] - public string Language { get; set; } + public IEnumerable Language { get; set; } [Name("FilterType")] - public ReviewType FilterType { get; set; } + public IEnumerable FilterType { get; set; } } } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml index aa9b65f4d33..8b6e284d4dc 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml @@ -6,57 +6,35 @@ @{ ViewData["Title"] = "Reviews"; } +@section Scripts +{ + +}