diff --git a/devdocs/specs/core-display-format-template-system.txt b/devdocs/specs/core-display-format-template-system.txt index ead3b3d8..1bd3211d 100644 --- a/devdocs/specs/core-display-format-template-system.txt +++ b/devdocs/specs/core-display-format-template-system.txt @@ -1,8 +1,14 @@ DISPLAY FORMAT TEMPLATE SYSTEM SPECS OVERVIEW -Narrow grids on small screens as well as picklists need to show a single field of information as required by that shop for selection, recognition etc so templated to format multiple return fields into one +RAVEN uses a user customizable template to determine what fields in which order get sent to the client when they display a list or select from a drop down + - Users with BIZ rights can set the template + - There is one template per list (not user based but global in nature) + - Template affects both "MINI" single column display of list for small devices and large format wide mode with multiple columns for larger devices + - In MINI list format displays they choose which columns go into the one single column for display that is returned + - In regular list format display they choose which columns are returned and which order separately from MINI format display +Narrow grids on small screens as well as picklists need to show a single field of information as required by that shop for selection, recognition etc so templated to format multiple return fields into one Grids need a way to ensure you can see what you need on small devices as well they need to show the columns desired on larger devices PickLists need a way to format what is returned for picking (with template) @@ -21,6 +27,11 @@ none relevant REQUIREMENTS Client + - Template editor: + - One area is for what goes into a single column in "MINI" mode for that list + - Another separate area on the same form is for selecting which columns are returned in a wide list and in what order + - Everyone gets the same template selections, i.e. there is only one template per list type + - This differs from v7 in which each user could select the columns to display and the order but then they needed to for filtering and sort order and stuff - Main grid lists < 600px show only a single column based on template - Selection drop down boxes in forms for selecting other objects - Template editor under global settings for all main list objects @@ -46,7 +57,7 @@ Server Back AND front end - Need to handle changes in fields gracefully i.e. a new field added in an update, a field removed in an update cleans out the template when detected etc - - Should send the customized templated display name field to the reports as well as all the regular including name fields + - REPORTS (FUTURE) Should send the customized templated display name field to the reports as well as all the regular including name fields - This is because users will likely want that for many reports - Kind of a calculated field - Default templates come with Raven, user can customize further \ No newline at end of file diff --git a/devdocs/specs/core-main-grids.txt b/devdocs/specs/core-main-grids.txt index e01afc31..437d4048 100644 --- a/devdocs/specs/core-main-grids.txt +++ b/devdocs/specs/core-main-grids.txt @@ -3,7 +3,7 @@ MAIN GRID SPECS OVERVIEW Main grids show all the stuff people have entered in Raven and are used for selection to edit/ view, filter and sort for reporting and mass operations. Proposed system is a hybrid grid system that has two modes to take into account the width of the display device: - - Xtra Small (XS ) < 600 pixels wide (my phone is 393px) + - MINI mode (XS ) < 600 pixels wide (my Pixel 3a phone is 393px) - In this mode only one column shows and it's formatted according to the Display Format Template - DisplayFormatTemplate is specd in the core-display-format-template-system.txt doc - So the user chooses which columns to show in a small factor themselves, default is name only or equivalent @@ -17,19 +17,18 @@ REQUIREMENTS Client - Client needs to tell the server which form of list is required, i.e. it's client window size when fetching a list - - HYBRID BIMODAL: XS or LARGE (for now but consider maybe a medium down the road in planning) - - in XS mode - - the client sends "viewport=xs" to the client with the grid data request + - HYBRID BIMODAL: MINI or default which is large (for now but consider maybe a medium down the road in planning) + - in MINI mode + - the client sends "mini=true" to the client with the grid data request - Client receives two field list back (id and display field), displays the single field - in large mode - - the client sends no viewport= parameter as it's optional and understood to be full size + - the client sends no mini= parameter as it's optional and understood to be full size - the list comes back as an object with not only the actual columns but also a separate property listing the set of columns in order to be displayed - Also their data type - Also need what type of object if openable with an url - - Client expects an arbitrary set of columns in an arbitrary order defined by server so doesn't have a pre-ordained set of things. - - HMMM... Maybe I only need a single list object that adapts to the data being sent back!!!!! - - The client needs to be able to handle more columns than fit horizontally - - The client needs to not wrap any column vertically but ellipse.. it instead + - Client expects an arbitrary set of columns in an arbitrary order defined by server so doesn't have a pre-ordained set of things. + - The client needs to be able to handle more columns than fit horizontally + - The client needs to not wrap any column vertically but ellipse.. it instead - Client accepts a list of fields and types etc and generates the grid and populates the data dynamically - This means that I can make a general purpose Vue grid component, plunk it down on a form and then wrap it with what is unique about that form diff --git a/devdocs/todo.txt b/devdocs/todo.txt index 2284cff2..fd517868 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -20,7 +20,9 @@ REALLY MAKING MORE PROGRESS WHEN CLIENT DEV DRIVES BACKEND DEV, STICK TO THAT!! ----------------------- GRID LISTS TODO NOW: - + - Return JSON and internally work with JSON so the list can return dynamic object + - https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/ + - https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-3-0#fast-built-in-json-support - Combine PickList route and WidgetList route into single "List" route with an internal nameof "WidgetList" - if mini=true then it sends back the two column result templated - Otherwise it sends back the full list (templated) diff --git a/server/AyaNova/Controllers/WidgetController.cs b/server/AyaNova/Controllers/WidgetController.cs index 7217b389..19861310 100644 --- a/server/AyaNova/Controllers/WidgetController.cs +++ b/server/AyaNova/Controllers/WidgetController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.JsonPatch; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; using AyaNova.Models; using AyaNova.Api.ControllerHelpers; @@ -109,45 +110,16 @@ namespace AyaNova.Api.Controllers }); } - /// - /// Get paged list of widgets + /// Get list for selection / viewing /// - /// Required roles: - /// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited - /// - /// Paged collection of widgets with paging data - [HttpGet("ListWidgets", Name = nameof(ListWidgets))]//We MUST have a "Name" defined or we can't get the link for the pagination, non paged urls don't need a name - public async Task ListWidgets([FromQuery] ListOptions pagingOptions) - { - if (serverState.IsClosed) - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - - //Instantiate the business object handler - WidgetBiz biz = WidgetBiz.GetBiz(ct, HttpContext); - - if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) - return StatusCode(403, new ApiNotAuthorizedResponse()); - - if (!ModelState.IsValid) - return BadRequest(new ApiErrorResponse(ModelState)); - - ApiPagedResponse pr = await biz.GetManyAsync(Url, nameof(ListWidgets), pagingOptions); - return Ok(new ApiOkWithPagingResponse(pr)); - } - - - - /// - /// Get pick list - /// - /// Required roles: Any + /// Required roles: Any (some roles might have restrictions on exact fields that are returned) /// /// /// Paging, filtering and sorting options - /// Paged id/name collection with paging data - [HttpGet("PickList", Name = nameof(WidgetPickList))] - public ActionResult WidgetPickList([FromQuery] ListOptions pagingOptions) + /// Collection with paging data + [HttpGet("List", Name = nameof(WidgetList))] + public ActionResult WidgetList([FromQuery] ListOptions pagingOptions) { if (serverState.IsClosed) return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); @@ -158,11 +130,66 @@ namespace AyaNova.Api.Controllers //Instantiate the business object handler WidgetBiz biz = WidgetBiz.GetBiz(ct, HttpContext); - ApiPagedResponse pr = biz.GetPickList(Url, nameof(WidgetPickList), pagingOptions); - return Ok(new ApiOkWithPagingResponse(pr)); + // ApiPagedResponse pr = biz.GetList(Url, nameof(WidgetList), pagingOptions); + // return Ok(new ApiOkWithPagingResponse(pr)); + JObject j = biz.GetList(Url, nameof(WidgetList), pagingOptions).Result; + return Ok(j); } + // /// + // /// Get paged list of widgets + // /// + // /// Required roles: + // /// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited + // /// + // /// Paged collection of widgets with paging data + // [HttpGet("ListWidgets", Name = nameof(ListWidgets))]//We MUST have a "Name" defined or we can't get the link for the pagination, non paged urls don't need a name + // public async Task ListWidgets([FromQuery] ListOptions pagingOptions) + // { + // if (serverState.IsClosed) + // return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + // //Instantiate the business object handler + // WidgetBiz biz = WidgetBiz.GetBiz(ct, HttpContext); + + // if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + // return StatusCode(403, new ApiNotAuthorizedResponse()); + + // if (!ModelState.IsValid) + // return BadRequest(new ApiErrorResponse(ModelState)); + + // ApiPagedResponse pr = await biz.GetManyAsync(Url, nameof(ListWidgets), pagingOptions); + // return Ok(new ApiOkWithPagingResponse(pr)); + // } + + + + // /// + // /// Get pick list + // /// + // /// Required roles: Any + // /// + // /// + // /// Paging, filtering and sorting options + // /// Paged id/name collection with paging data + // [HttpGet("PickList", Name = nameof(WidgetPickList))] + // public ActionResult WidgetPickList([FromQuery] ListOptions pagingOptions) + // { + // if (serverState.IsClosed) + // return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + // if (!ModelState.IsValid) + // return BadRequest(new ApiErrorResponse(ModelState)); + + // //Instantiate the business object handler + // WidgetBiz biz = WidgetBiz.GetBiz(ct, HttpContext); + + // ApiPagedResponse pr = biz.GetPickList(Url, nameof(WidgetPickList), pagingOptions); + // return Ok(new ApiOkWithPagingResponse(pr)); + // } + + /// /// Put (update) widget /// @@ -287,9 +314,9 @@ namespace AyaNova.Api.Controllers Widget o = await biz.CreateAsync(inObj); if (o == null) return BadRequest(new ApiErrorResponse(biz.Errors)); - else + else return CreatedAtAction(nameof(WidgetController.GetWidget), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); - + } diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index f682c1d7..7a07e6d1 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.JsonPatch; +using Newtonsoft.Json.Linq; using EnumsNET; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; @@ -287,8 +288,9 @@ namespace AyaNova.Biz return f; } + //get many (paged) - internal async Task> GetManyAsync(IUrlHelper Url, string routeName, ListOptions pagingOptions) + internal async Task GetList(IUrlHelper Url, string routeName, ListOptions pagingOptions) { pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit; @@ -332,27 +334,86 @@ namespace AyaNova.Biz var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); - ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); - return pr; + +//TODO: BUILD THE RETURN BASED ON TEMPLATE, ListOpti + + //New return code + dynamic ret = new JObject(); + ret.items = items; + ret.pageLinks = pageLinks; + + return ret; + // ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); + // return pr; } - /// - /// Get PickList - /// - /// - /// - /// - /// - internal ApiPagedResponse GetPickList(IUrlHelper Url, string routeName, ListOptions pagingOptions) - { - pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset; - pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit; - var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, FilterOptions(), "awidget"); - var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, ret.TotalRecordCount).PagingLinksObject(); - ApiPagedResponse pr = new ApiPagedResponse(ret.Items, pageLinks); - return pr; - } + // //get many (paged) + // internal async Task> GetManyAsync(IUrlHelper Url, string routeName, ListOptions pagingOptions) + // { + // pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset; + // pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit; + + // //BUILD THE QUERY + // //base query + // var q = "SELECT *, xmin FROM AWIDGET "; + + // //GET THE FILTER / SORT + // if (pagingOptions.DataFilterId > 0) + // { + // var TheFilter = await ct.DataFilter.FirstOrDefaultAsync(x => x.Id == pagingOptions.DataFilterId); + + // //BUILD WHERE AND APPEND IT + // q = q + FilterSqlCriteriaBuilder.DataFilterToSQLCriteria(TheFilter, WidgetBiz.FilterOptions(), UserId); + + // //BUILD ORDER BY AND APPEND IT + // q = q + FilterSqlOrderByBuilder.DataFilterToSQLOrderBy(TheFilter); + // } + // else + // { + // //GET DEFAULT ORDER BY + // q = q + FilterSqlOrderByBuilder.DefaultGetManyOrderBy(); + // } + + + // #pragma warning disable EF1000 + + // var items = await ct.Widget + // .FromSqlRaw(q) + // .AsNoTracking() + // .Skip(pagingOptions.Offset.Value) + // .Take(pagingOptions.Limit.Value) + // .ToArrayAsync(); + + // var totalRecordCount = await ct.Widget + // .FromSqlRaw(q) + // .AsNoTracking() + // .CountAsync(); + // #pragma warning restore EF1000 + + // var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); + + // ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); + // return pr; + // } + + + // /// + // /// Get PickList + // /// + // /// + // /// + // /// + // /// + // internal ApiPagedResponse GetPickList(IUrlHelper Url, string routeName, ListOptions pagingOptions) + // { + // pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset; + // pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit; + // var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, FilterOptions(), "awidget"); + // var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, ret.TotalRecordCount).PagingLinksObject(); + // ApiPagedResponse pr = new ApiPagedResponse(ret.Items, pageLinks); + // return pr; + // }