diff --git a/StatCan.OrchardCore.sln b/StatCan.OrchardCore.sln index 4081461c1..20deb1875 100644 --- a/StatCan.OrchardCore.sln +++ b/StatCan.OrchardCore.sln @@ -94,7 +94,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{E8DD6EC8-A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatCan.OrchardCore.Scheduling", "src\Apps\StatCan.OrchardCore.Scheduling\StatCan.OrchardCore.Scheduling.csproj", "{56F32388-650F-433C-919F-8C265F99411D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatCan.OrchardCore.Configuration", "src\Modules\StatCan.OrchardCore.Configuration\StatCan.OrchardCore.Configuration.csproj", "{DEA6A849-3230-43DE-A9A0-3C22DA8E316E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatCan.OrchardCore.Configuration", "src\Modules\StatCan.OrchardCore.Configuration\StatCan.OrchardCore.Configuration.csproj", "{DEA6A849-3230-43DE-A9A0-3C22DA8E316E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatCan.OrchardCore.Vuetify", "src\Modules\StatCan.OrchardCore.Vuetify\StatCan.OrchardCore.Vuetify.csproj", "{2383A37A-3D99-4060-B147-F4EB81FA826D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatCan.OrchardCore.Menu", "src\Modules\StatCan.OrchardCore.Menu\StatCan.OrchardCore.Menu.csproj", "{DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatCan.OrchardCore.Radar", "src\Apps\StatCan.OrchardCore.Radar\StatCan.OrchardCore.Radar.csproj", "{B94484A0-D043-4D37-AE5C-82B729D56CA3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -466,6 +472,42 @@ Global {DEA6A849-3230-43DE-A9A0-3C22DA8E316E}.Release|x64.Build.0 = Release|Any CPU {DEA6A849-3230-43DE-A9A0-3C22DA8E316E}.Release|x86.ActiveCfg = Release|Any CPU {DEA6A849-3230-43DE-A9A0-3C22DA8E316E}.Release|x86.Build.0 = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|x64.ActiveCfg = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|x64.Build.0 = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|x86.ActiveCfg = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Debug|x86.Build.0 = Debug|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|Any CPU.Build.0 = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|x64.ActiveCfg = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|x64.Build.0 = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|x86.ActiveCfg = Release|Any CPU + {2383A37A-3D99-4060-B147-F4EB81FA826D}.Release|x86.Build.0 = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|x64.Build.0 = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Debug|x86.Build.0 = Debug|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|Any CPU.Build.0 = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|x64.ActiveCfg = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|x64.Build.0 = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|x86.ActiveCfg = Release|Any CPU + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB}.Release|x86.Build.0 = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|x64.Build.0 = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|x86.ActiveCfg = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Debug|x86.Build.0 = Debug|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|Any CPU.Build.0 = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|x64.ActiveCfg = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|x64.Build.0 = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|x86.ActiveCfg = Release|Any CPU + {B94484A0-D043-4D37-AE5C-82B729D56CA3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -506,6 +548,9 @@ Global {E8DD6EC8-AA09-440F-937A-1901293389E1} = {8EA5EBF0-41C6-11EA-890A-ED68CBADDC1F} {56F32388-650F-433C-919F-8C265F99411D} = {E8DD6EC8-AA09-440F-937A-1901293389E1} {DEA6A849-3230-43DE-A9A0-3C22DA8E316E} = {5E638520-41E8-11EA-885A-BDD3BB7B4F92} + {2383A37A-3D99-4060-B147-F4EB81FA826D} = {8BEC45F6-4F23-4994-9959-50C1DB93ABC3} + {DDD0D2BF-B04E-42F1-ABCC-A5D3290692FB} = {8BEC45F6-4F23-4994-9959-50C1DB93ABC3} + {B94484A0-D043-4D37-AE5C-82B729D56CA3} = {E8DD6EC8-AA09-440F-937A-1901293389E1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8FF197F3-C3E2-4D83-80AC-D59BE36DD4AF} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets.json b/src/Apps/StatCan.OrchardCore.Radar/Assets.json new file mode 100644 index 000000000..a6fc48ec2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets.json @@ -0,0 +1,23 @@ +[ + { + "inputs": ["Assets/scss/*.scss"], + "watch": ["Assets/scss/*"], + "output": "wwwroot/css/radar.css" + }, + { + "copy": true, + "inputs": ["Assets/images/*"], + "output": "wwwroot/css/images/@" + }, + { + "inputs": ["Assets/js/*.js"], + "output": "wwwroot/js/@.js" + }, + { + "vue": true, + "file": "Assets/js/vue-components/RadarComponents.js", + "output": "wwwroot/js/vue-components", + "name": "radar-vue-components", + "watch": ["Assets/js/vue-components/components/*"] + } +] diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada-light.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada-light.svg new file mode 100644 index 000000000..e2570ea53 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada-light.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada.svg new file mode 100644 index 000000000..936bd1156 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/canada.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/di-logo.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/di-logo.svg new file mode 100644 index 000000000..1a439baed --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/di-logo.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc-light.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc-light.svg new file mode 100644 index 000000000..c1bee5f5c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc-light.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc.svg new file mode 100644 index 000000000..5e59977b4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/goc.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_communities.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_communities.svg new file mode 100644 index 000000000..40e7b3801 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_communities.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_events.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_events.svg new file mode 100644 index 000000000..26562ce11 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_events.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_experiments.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_experiments.svg new file mode 100644 index 000000000..015470954 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_experiments.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_opportunities.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_opportunities.svg new file mode 100644 index 000000000..ee5b28de4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_opportunities.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_topics.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_topics.svg new file mode 100644 index 000000000..3f09ba7b7 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/icon_topics.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/radar-icon-white.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/radar-icon-white.svg new file mode 100644 index 000000000..a9cd6f7d2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/radar-icon-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples-white.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples-white.svg new file mode 100644 index 000000000..ea7ef8adb --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples.svg b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples.svg new file mode 100644 index 000000000..890c02968 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/images/ripples.svg @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/js/table.js b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/table.js new file mode 100644 index 000000000..dcd6c3a57 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/table.js @@ -0,0 +1,99 @@ +const attachSorter = function() { + const tables = document.querySelectorAll("#entity-table"); + for (let table of tables) { + const headers = table.querySelectorAll("th"); + const tableBody = table.querySelector("tbody"); + const rows = tableBody.querySelectorAll("tr"); + + // Track sort directions + const directions = Array.from(headers).map(function(header) { + return ""; + }); + + // Transform the content of given cell in given column + const transform = function(index, content) { + // Get the data type of column + const type = headers[index].getAttribute("data-type"); + + if (type === "name") { + content = content.childNodes[0].innerHTML; + } else if (type === "number") { + content = content.childNodes[0].childNodes[2].innerHTML; + } else if (type === "date-range") { + content = content.childNodes[0].innerHTML.split("-")[0]; + } + + switch (type) { + case "number": + return parseFloat(content); + case "date": + return Date.parse(content); + case "string": + default: + return content; + } + }; + + const sortColumn = function(index) { + // Get the current direction + const direction = directions[index] || "asc"; + + // A factor based on the direction + const multiplier = direction === "asc" ? 1 : -1; + + const newRows = Array.from(rows); + + newRows.sort(function(rowA, rowB) { + const cellA = rowA.querySelectorAll("td")[index]; + const cellB = rowB.querySelectorAll("td")[index]; + + const a = transform(index, cellA); + const b = transform(index, cellB); + + switch (true) { + case a > b: + return 1 * multiplier; + case a < b: + return -1 * multiplier; + case a === b: + return 0; + } + }); + + // Remove old rows + [].forEach.call(rows, function(row) { + tableBody.removeChild(row); + }); + + // Reverse the direction + directions[index] = direction === "asc" ? "desc" : "asc"; + + // Append new row + newRows.forEach(function(newRow) { + tableBody.appendChild(newRow); + }); + }; + + [].forEach.call(headers, function(header, index) { + header.addEventListener("click", function() { + sortColumn(index); + }); + }); + } +}; + +const attachSearchClear = function() { + // Hook into the rendered button + const clearButton = document.getElementById("search-clear-button"); + if (clearButton) { + clearButton.addEventListener("click", function() { + document.getElementById("search-input").value = ""; + document.getElementById("search-form").submit(); + }); + } +}; + +window.addEventListener("DOMContentLoaded", function() { + attachSorter(); + attachSearchClear(); +}); diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/RadarComponents.js b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/RadarComponents.js new file mode 100644 index 000000000..704c12b2d --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/RadarComponents.js @@ -0,0 +1,7 @@ +import Tabs from "./components/Tabs.vue"; +import Tab from "./components/Tab.vue"; +import GlobalSearch from "./components/GlobalSearch.vue"; + +Vue.component("tabs", Tabs); +Vue.component("tab", Tab); +Vue.component("global-search", GlobalSearch); diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/GlobalSearch.vue b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/GlobalSearch.vue new file mode 100644 index 000000000..0972f50a6 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/GlobalSearch.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tab.vue b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tab.vue new file mode 100644 index 000000000..f01a639d7 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tab.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tabs.vue b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tabs.vue new file mode 100644 index 000000000..16afb03d8 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/js/vue-components/components/Tabs.vue @@ -0,0 +1,68 @@ + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/appbar.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/appbar.scss new file mode 100644 index 000000000..82fe70575 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/appbar.scss @@ -0,0 +1,26 @@ +@import "colors"; + +.appbar { + background-color: $primary !important; +} + +.menu-button { + background-color: $secondary !important; + width: 38px !important; + height: 38px !important; + margin-left: 1rem; + color: #ffffff !important; +} + +.app-bar-username-container { + margin-left: 1rem; +} + +.app-bar-buttons-container { + margin-right: 0.5rem; + display: flex; +} + +a { + text-decoration: none; +} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/colors.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/colors.scss new file mode 100644 index 000000000..2801c1982 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/colors.scss @@ -0,0 +1,8 @@ +$primary: #252561; +$primary-variant1: #151537; +$card-hover: #3B3B8A; +$secondary: #f9663b; +$secondary-variant1: #fdc5b5; +$secondary-variant2: #feece6; +$background: #efeff4; +$background-variant1: #efeff461; \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/details-view.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/details-view.scss new file mode 100644 index 000000000..b6c522eb2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/details-view.scss @@ -0,0 +1,138 @@ +@import "colors"; + +.details-section { + padding-top: 3rem; +} + +.header { + padding: 3rem 7rem; + background: transparent + linear-gradient(180deg, rgba($primary, 0.38), rgba($secondary, 0.38)) 0% + no-repeat; +} + +.details-name { + text-align: left; + font-weight: 300; + color: $primary; + overflow-wrap: anywhere; +} + +.details-description { + margin-top: 1rem; + color: $primary-variant1; +} + +.topics-container { + margin-top: 2rem !important; +} + +.icon-cell { + display: flex; + align-items: center; +} + +.icon-text { + margin-left: 1rem; +} + +.topics-list-container { + display: flex; +} + +.options-button { + width: 3rem !important; + height: 3rem !important; +} + +.details-view-options-button { + background-color: #ffffff !important; +} + +.workspace-bubble { + margin-left: 2rem; + background-color: $background; + border: 2px solid $primary; + border-radius: 50%; + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 23px; +} + +.workspace-bubble-text { + color: $primary-variant1; +} + +.workspace-add-button { + width: 3rem !important; + height: 3rem !important; + margin-left: 1rem; + background-color: #ffffff !important; +} + +.workspace-button-icon { + font-size: 40px !important; +} + +.workspace-add-button:hover { + background-color: $secondary-variant1 !important; +} + +.member-table { + width: 50%; +} + +.attendees-table { + width: 20%; +} + +.table-title-container { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.sections-container { + padding: 0 7rem 0 7rem; +} + +.section { + margin-bottom: 2rem; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), + linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.details-main-icon { + font-size: 9rem !important; + color: $primary !important; +} + +.details-attribute-icon { + font-size: 2.5rem !important; + color: #000000 !important; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/footer.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/footer.scss new file mode 100644 index 000000000..a7088b884 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/footer.scss @@ -0,0 +1,91 @@ +@import "colors"; + +.footer-bottom { + background: $primary; + border-top: 3px solid $secondary; + display: flex; + justify-content: space-between; + padding: 3rem 6rem 2rem 6rem; +} + +.ripple { + background-color: $primary; + background-size: cover; + background-repeat: no-repeat; + background-position: center bottom -623px; +} + +.footer { + background-color: $primary; +} + +.menus-container { + display: flex; + flex-grow: 5; + justify-content: space-evenly; +} + +.menu-container { + display: flex; + flex-direction: column; +} + +.menu-title { + color: $secondary; + margin-bottom: 1rem; +} + +.zigzag-footer { + position: relative; + padding: 0; + background: #ffffff; +} + +.zigzag-footer::after { + content: " "; + display: block; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 32px; + background: linear-gradient(-145deg, #ffffff 16px, transparent 0), + linear-gradient(145deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.ripple-container { + padding: 5rem 2rem 5rem 2rem; + display: flex; + width: 100%; + background-image: url("images/ripples.svg"); +} + +.menu-link { + margin-bottom: 1rem; +} + +.menu-link:hover { + text-decoration: underline; +} + +.logo-container { + display: flex; + flex-direction: column; + justify-content: center; + flex-grow: 1; +} + +.logo { + display: flex; + flex-direction: column; + justify-content: end; + align-items: flex-start; + flex-grow: 1 +} + +.logo-text { + margin-left: 0.5rem; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/global-search.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/global-search.scss new file mode 100644 index 000000000..4ad1e0f06 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/global-search.scss @@ -0,0 +1,49 @@ +@import "colors"; + +.global-search-container { + display: flex; + align-items: center; + display: flex; + height: 2.2rem; + padding-left: 6px; + width: fit-content; + border-radius: 4px; + background-color: #00000070; + position: relative; +} + +.global-search { + color: #ffffff; +} + +input:focus { + outline: none; +} + +.dropdown-list { + position: absolute; + width: 100%; + max-height: 500px; + margin-top: 4px; + overflow-y: auto; + background: #ffffff; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + top: 40px; + padding: 1rem; +} + +.dropdown-item { + color: #000000; + padding: 1rem 0 1rem 0; +} + +.dropdown-item:hover { + background-color: #f1f1f1; +} + +.result-caption { + color: $primary; + display: flex; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/icon.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/icon.scss new file mode 100644 index 000000000..7ef09fe0c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/icon.scss @@ -0,0 +1,13 @@ +@import "colors"; + +.page-header-image { + margin-left: auto; +} + +.icon-primary { + color: $primary !important; +} + +.icon-secondary { + color: $secondary !important; +} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page-card.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page-card.scss new file mode 100644 index 000000000..266f821b5 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page-card.scss @@ -0,0 +1,110 @@ +@import "colors"; + +.card-background { + background-color: $background; +} + +.trend-card-body { + display: flex; + flex-direction: column; + margin-top: 1.25rem; +} + +.trend-card-body-icon { + font-size: 9rem !important; +} + +.trend-card-body-container { + margin-left: 0.25rem; +} + +.trend-card-body-topic-container { + margin-bottom: 0.5rem; +} + +.trend-container { + display: flex; + align-items: center; +} + +.card { + display: flex; + flex-direction: column; + height: 100%; +} + +.card-title { + background-color: $primary; + display: flex; + flex-direction: column; +} + +.card-title:hover { + background-color: $card-hover; +} + +.card-title-caption { + display: flex; + margin: 1.25rem 0 2.5rem 1.25rem; +} + +.card-icon { + font-size: 4rem !important; + margin-right: 0.75rem; +} + +.card-body { + padding: 1rem 0 0 6rem; + margin-bottom: 1.5rem; + flex-grow: 1; +} + +.card-button { + margin-bottom: 1rem; + margin-left: 1rem; + color: $secondary !important; +} + +.card-item-link { + color: #000000 !important; +} + +.card-item-link:hover { + color: $primary !important; + text-decoration: underline; +} + +.card-item-link-container { + margin-bottom: 0.75rem; +} + +.card-button-link { + width: fit-content; +} + +.card-title { + display: flex; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-dark { + background: linear-gradient(-40deg, $background 16px, transparent 0), + linear-gradient(40deg, $background 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page.scss new file mode 100644 index 000000000..1cdae1023 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/landing-page.scss @@ -0,0 +1,114 @@ +@import "colors"; + +.max-wdith { + max-width: 1200px; +} + +.light-font { + font-weight: lighter; + color: #000000de; + opacity: 1; +} + +.header-list { + display: flex; + flex-direction: column; +} + +.list-item { + display: flex; + margin-bottom: 8px; +} + +.list-item-text { + font-size: 1.125rem; +} + +.name { + font-size: 3.5rem; + color: $primary; + font-weight: 300; + white-space: nowrap; +} + +.description { + font-size: 25px; + font-weight: 300; + margin: auto; + line-height: normal; +} + +.list { + display: flex; + justify-content: center; +} + +.section-title { + color: $primary; +} + +.section-background { + background: $background-variant1; +} + +.front-footer { + background-color: $primary !important; + padding: 1.75rem 0 2rem 0; +} + +.front-footer:hover { + background-color: $card-hover !important; +} + +.front-footer-card-icon { + font-size: 7.5rem !important; + margin-bottom: 0.5rem; +} + +.front-footer-card-title { + margin-bottom: 0.25rem; +} + +.header-footer { + display: flex; + justify-content: space-between; + padding: 0 10rem 0 10rem; + margin-top: 1.75rem; + flex-wrap: wrap; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), + linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.rippleFront { + background-color: #cfece9; + background-image: url("images/ripples-white.svg"), linear-gradient(rgba($primary, 1), rgba($secondary, 0.7)); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + padding: 12rem 0rem 3rem 0rem; +} + +a { + text-decoration: none; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/list-view.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/list-view.scss new file mode 100644 index 000000000..c912a2e60 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/list-view.scss @@ -0,0 +1,98 @@ +@import "colors"; + +.list-header { + padding: 3rem 13rem 4rem 13rem; + background: transparent + linear-gradient(180deg, rgba($primary, 0.38), rgba($secondary, 0.38)) 0% + no-repeat; +} + +.details-section { + padding-top: 3rem; +} + +.list-title { + color: $primary; +} + +.title-container { + display: flex; + align-items: center; +} + +.bubble { + margin-left: 2rem; + background-color: $background; + border: 2px solid $primary; + border-radius: 50%; + width: 5rem; + height: 5rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 38px; +} + +.bubble-text { + color: $primary-variant1; +} + +.table-container { + padding: 0 2rem 0 2rem; + width: 80%; + margin: auto; +} + +.search-container { + margin: 1rem 0 1.5rem 0; + display: flex; + background: #f6fcfb; + align-items: center; + border-bottom: 2px solid $primary; + display: flex; + height: 2.2rem; + padding-left: 1rem; + width: fit-content; +} + +.search-icon { + margin-right: 1.25rem; + color: #000000 !important; +} + +input:focus { + outline: none; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), + linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +a { + text-decoration: none; + color: black !important; +} + +.list-view-options-button { + background-color: rgba(255, 255, 255, 0); +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table-row.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table-row.scss new file mode 100644 index 000000000..36b209da2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table-row.scss @@ -0,0 +1,33 @@ +@import "colors"; + +.table-row { + background-color: $secondary-variant2; + height: 48px; + border-bottom: 1px solid $secondary-variant1; + cursor: pointer; + overflow-wrap: anywhere; +} + +.table-row:hover { + background: $secondary-variant1; +} + +.name-col { + text-align: left; + padding-left: 1rem; +} + +.table-col { + text-align: center; +} + +.table-icon-cell { + display: flex; + align-items: center; + justify-content: center; +} + +a { + text-decoration: none; + color: #000000 !important; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table.scss new file mode 100644 index 000000000..0d72f28a3 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/table.scss @@ -0,0 +1,20 @@ +@import "colors"; + +.table-header { + border-bottom: 2px solid $primary; +} + +.table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; +} + +.name-col { + text-align: left; + padding-left: 1rem; +} + +th:hover { + cursor: pointer; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/topic.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/topic.scss new file mode 100644 index 000000000..3213d80b4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/topic.scss @@ -0,0 +1,47 @@ +@import "colors"; + +.topic { + background: $secondary 0% 0% no-repeat padding-box; + border-radius: 75px; + display: inline-block; + padding: 2px 10px 0px 9px; + margin-right: 0.9rem; + color: #FFFFFF; + width: fit-content; + display: flex; + border: $secondary solid 2px; +} + +.topic:hover { + border: $primary solid 2px; +} + +.topic-tag-icon { + margin-right: 0.25rem; + font-size: 1.25rem; +} + +.label { + overflow: hidden; + text-overflow: ellipsis; +} + +a { + text-decoration: none; +} + +.form-topic-field { + margin-bottom: 0.6rem; +} + +.form-topic-field-remove-button:hover { + cursor: pointer; +} + +.multiselect__option--highlight { + background-color: $primary !important; +} + +.multiselect__option--highlight::after { + background-color: $primary !important; +} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/vue-components.scss b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/vue-components.scss new file mode 100644 index 000000000..576b0cb86 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Assets/scss/vue-components.scss @@ -0,0 +1,64 @@ +@import "colors"; + +.tabs { + display: flex; +} + +.selected { + color: #212121; + background-color: $secondary-variant1; + border-bottom: 2px solid $primary; +} + +.selected:hover { + background-color: $secondary-variant1; +} + +.unselected { + color: #212121; + background-color: $secondary-variant2; + border-bottom: 2px solid #ffffff; +} + +.unselected:hover { + background-color: $secondary-variant1; +} + +.tab-button { + padding: 0.5rem 1rem; +} + +.tab-header-container { + display: flex; +} + +.count { + border-radius: 11px; + display: flex; + height: 22px; + min-width: 22px; + margin-left: 1rem; + padding: 0 0.4rem 0 0.4rem; +} +.count-selected { + background: $background; + border: 1px solid $primary; +} +.count-unselected { + background-color: #eeeeee; + border: 1px solid #9c9c9c; +} + +.count-label { + margin: auto; + text-align: center; + font-size: 12px; +} + +.count-label-Selected { + color: #227069; +} + +.count-label-Unselected { + color: #212121; +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Constants.cs b/src/Apps/StatCan.OrchardCore.Radar/Constants.cs new file mode 100644 index 000000000..4ade6f870 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Constants.cs @@ -0,0 +1,49 @@ +namespace StatCan.OrchardCore.Radar +{ + public static class Constants + { + public static class Features + { + public const string Radar = "StatCan.OrchardCore.Radar"; + } + + public static class ContentTypes + { + // Taxonomies + public const string Topic = "Topic"; + public const string ProposalType = "ProposalType"; + public const string ProjectType = "ProjectType"; + public const string CommunityType = "CommunityType"; + + // Entities + public const string Artifact = "Artifact"; + public const string Proposal = "Proposal"; + public const string Project = "Project"; + public const string Event = "Event"; + public const string Community = "Community"; + + public const string ProjectMember = "ProjectMember"; + public const string EventOrganizer = "EventOrganizer"; + public const string CommunityMember = "CommunityMember"; + + // Parts + public const string RadarEntityPart = "RadarEntityPart"; + public const string RadarFormPart = "RadarFormPart"; + public const string RadarPermissionPart = "RadarPermissionPart"; + + // Pages + public const string LandingPage = "LandingPage"; + public const string EntityCard = "EntityCard"; + public const string TrendingCard = "TrendingCard"; + public const string LandingPageHeaderList = "LandingPageHeaderList"; + public const string LandingPageFooterCard = "LandingPageFooterCard"; + public const string AppBar = "AppBar"; + public const string NavigationDrawer = "NavigationDrawer"; + public const string Footer = "Footer"; + public const string Form = "Form"; + + // Custom User Settings + public const string UserProfile = "UserProfile"; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Controllers/FormController.cs b/src/Apps/StatCan.OrchardCore.Radar/Controllers/FormController.cs new file mode 100644 index 000000000..42e1bd052 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Controllers/FormController.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Security.Claims; +using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using OrchardCore.ContentManagement; +using OrchardCore.Queries; +using OrchardCore.ContentManagement.Display; +using OrchardCore.DisplayManagement.ModelBinding; +using Etch.OrchardCore.ContentPermissions.Services; +using OrchardCore.ContentLocalization.Models; +using OrchardCore.Shortcodes.Services; +using OrchardCore.ContentLocalization; +using OrchardCore.Localization; +using StatCan.OrchardCore.Radar.Services; + +using Permissions = OrchardCore.Contents.Permissions; + +namespace StatCan.OrchardCore.Radar.Controllers +{ + /* + The Controller contains two types of endpoints. The first type are the + view endpoints for the edit form of the contents. These endpoints builds + and return the form shape. The second type are the api endpoints that are + related to searching. For instance, the TopicSearch action is responsible + for search. Note that permission checking must be done in each of the + endpoints. The ContentDelete api endpoint is a special case since it + performs mutation. + */ + public class FormController : Controller + { + private readonly IContentManager _contentManager; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IQueryManager _queryManager; + private readonly IUpdateModelAccessor _updateModelAccessor; + private readonly IShortcodeService _shortcodeService; + private IContentItemDisplayManager _contentItemDisplayManager; + private readonly IAuthorizationService _authorizationService; + private readonly IContentPermissionsService _contentPermissionsService; + private readonly ILocalizationService _localizationService; + private readonly IContentLocalizationManager _contentLocalizationManager; + private readonly TaxonomyManager _taxonomyManager; + private readonly BagItemManager _bagItemManager; + private readonly EntitySearcher _entitySearcher; + + public FormController(IContentManager contentManager, IHttpContextAccessor httpContextAccessor, + IQueryManager queryManager, IContentItemDisplayManager contentItemDisplayManager, + IUpdateModelAccessor updateModelAccessor, IShortcodeService shortcodeService, IAuthorizationService authorizationService, + IContentPermissionsService contentPermissionsService, TaxonomyManager taxonomyManager, BagItemManager bagItemManager, + ILocalizationService localizationService, IContentLocalizationManager contentLocalizationManager, EntitySearcher entitySearcher) + { + _contentManager = contentManager; + _httpContextAccessor = httpContextAccessor; + _queryManager = queryManager; + _contentItemDisplayManager = contentItemDisplayManager; + _updateModelAccessor = updateModelAccessor; + _shortcodeService = shortcodeService; + _authorizationService = authorizationService; + _contentPermissionsService = contentPermissionsService; + _localizationService = localizationService; + _contentLocalizationManager = contentLocalizationManager; + _taxonomyManager = taxonomyManager; + _bagItemManager = bagItemManager; + _entitySearcher = entitySearcher; + } + + public async Task Form(string entityType, string id) + { + if (!await CanEditContent(id)) + { + return Redirect("not-found"); + } + + // Builds the form shape. It is assumed that there + // will ever be 1 form content item + var form = await GetFormAsync(GetFormNameFromType(entityType)); + + if (form == null) + { + return Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found"); + } + + + var formShape = await _contentItemDisplayManager.BuildDisplayAsync(form, _updateModelAccessor.ModelUpdater, "Detail"); + + return View(formShape); + } + + // For handling contents that are contained by other contents + public async Task FormContained(string parentType, string parentId, string childType, string id) + { + // Contained items following the same permission as the parent + if (!await CanEditContent(parentId)) + { + return Redirect("not-found"); + } + + // Builds the form shape. It is assumed that there + // will ever be 1 form content item + var form = await GetFormAsync(GetFormNameFromType(childType)); + + if (form == null) + { + return Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found"); + } + + + var formShape = await _contentItemDisplayManager.BuildDisplayAsync(form, _updateModelAccessor.ModelUpdater, "Detail"); + + return View("Form", formShape); + } + + // Api actions for forms + public async Task TopicSearch(string term) + { + if (!User.Identity.IsAuthenticated) + { + return Unauthorized(); + } + + ICollection> topics = new LinkedList>(); + var terms = await _entitySearcher.SearchTaxonomyAsync("Topics", term); + + foreach (var topic in terms) + { + // Delocalize the topic name + var topicName = await _shortcodeService.ProcessAsync(topic.DisplayText); + + var valuePair = new Dictionary() + { + {"value", topic.ContentItemId}, + {"label", topicName} + }; + + topics.Add(valuePair); + } + + return new ObjectResult(topics); + } + + public async Task UserSearch(string term) + { + if (!User.Identity.IsAuthenticated) + { + return Unauthorized(); + } + + ICollection> users = new LinkedList>(); + + // Each topic needs to be retrived from the taxonomy term + var userQuery = await _queryManager.GetQueryAsync("AllUsersSQL"); + var userResult = await _queryManager.ExecuteQueryAsync(userQuery, new Dictionary()); + + if (term != null) + { + if (userResult != null) + { + foreach (JObject user in userResult.Items) + { + var userName = user["NormalizedUserName"].Value().ToLower(); + + if (userName.Contains(term, StringComparison.OrdinalIgnoreCase)) + { + var optionPair = new Dictionary() + { + {"value", user["UserId"].Value() }, + {"label", userName} + }; + + users.Add(optionPair); + } + } + } + } + + return new ObjectResult(users); + } + + public async Task EntitySearch(string type, string term) + { + if (!User.Identity.IsAuthenticated) + { + return Unauthorized(); + } + + var contentItems = await _entitySearcher.SearchContentItemsAsync(type, term); + + // Convert result to form format + ICollection> entities = new LinkedList>(); + + foreach (var contentItem in contentItems) + { + + var localizationPart = contentItem.As(); + + var optionPair = new Dictionary() + { + {"value", localizationPart.LocalizationSet}, + {"label", contentItem.DisplayText} + }; + + entities.Add(optionPair); + } + + return new ObjectResult(entities); + } + + + [HttpPost] + public async Task ContentDelete([FromForm] string entityType, [FromForm] string id, [FromForm] string parentId, [FromForm] string successUrl, [FromForm] string failUrl) + { + if (entityType == "Topic") + { + if (!IsUserAdmin()) + { + return Redirect(failUrl); + } + + await _taxonomyManager.DeleteTaxonomyAsync("Topics", id); + + return Redirect(successUrl); + } + + if (entityType == "Artifact") + { + if (!await CanEditContent(parentId) || !await _authorizationService.AuthorizeAsync(User, Permissions.DeleteContent)) + { + return Redirect(failUrl); + } + + await _bagItemManager.DeleteBagItemAsync("Workspace", parentId, id); + } + + if (!await CanEditContent(id) || !await _authorizationService.AuthorizeAsync(User, Permissions.DeleteContent)) + { + return Redirect(failUrl); + } + + // If a content gets deleted then all of its localized version are deleted + var contentItem = await _contentManager.GetAsync(id); + + var supportedCultures = await _localizationService.GetSupportedCulturesAsync(); + var localizationSet = contentItem.As().LocalizationSet; + + foreach (var supportedCulture in supportedCultures) + { + var localizedContent = await _contentLocalizationManager.GetContentItemAsync(localizationSet, supportedCulture); + + await _contentManager.RemoveAsync(localizedContent); + + } + + return Redirect(successUrl); + } + + private async Task CanEditContent(string id) + { + return IsUserAdmin() || await IsOwner(id); + } + + private bool IsUserAdmin() + { + var user = _httpContextAccessor.HttpContext.User; + + if (!user.Identity.IsAuthenticated) + { + return false; + } + + if (user.IsInRole("Administrator")) + { + return true; + } + + return false; + } + + private async Task IsOwner(string id) + { + var user = _httpContextAccessor.HttpContext.User; + + if (!user.Identity.IsAuthenticated) + { + return false; + } + + var contentItem = await _contentManager.GetAsync(id); + + if (contentItem == null) + { + return false; + } + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier).ToString(); + + return contentItem.Owner == userId; + } + + private async Task GetFormAsync(string formName) + { + var query = await _queryManager.GetQueryAsync("FormQuerySQL"); + var result = await _queryManager.ExecuteQueryAsync(query, new Dictionary { { "type", formName } }); + + if (result == null) + { + return null; + } + + var id = (result.Items.First() as JObject).GetValue("ContentItemId").ToString(); + + var contentItem = await _contentManager.GetAsync(id); + + return contentItem; + } + + private string GetFormNameFromType(string type) + { + if (type == "topics") + { + return "Topic Form"; + } + else if (type == "artifacts") + { + return "Artifact Form"; + } + else if (type == "projects") + { + return "Project Form"; + } + else if (type == "communities") + { + return "Community Form"; + } + else if (type == "events") + { + return "Event Form"; + } + else if (type == "proposals") + { + return "Proposal Form"; + } + + return ""; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Controllers/ListController.cs b/src/Apps/StatCan.OrchardCore.Radar/Controllers/ListController.cs new file mode 100644 index 000000000..8b6c3e7b7 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Controllers/ListController.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Display; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Contents; +using StatCan.OrchardCore.Radar.Services; + +namespace StatCan.OrchardCore.Radar.Controllers +{ + + /* + Builds list views and handles list search. + */ + public class ListController : Controller + { + private readonly IContentItemDisplayManager _contentItemDisplayManager; + private readonly IUpdateModelAccessor _updateModelAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly EntitySearcher _entitySearcher; + + public ListController( + IContentItemDisplayManager contentItemDisplayManager, + IUpdateModelAccessor updateModelAccessor, + IAuthorizationService authorizationService, + EntitySearcher entitySearcher) + { + _contentItemDisplayManager = contentItemDisplayManager; + _updateModelAccessor = updateModelAccessor; + _authorizationService = authorizationService; + _entitySearcher = entitySearcher; + } + + [HttpGet] + public async Task List(string searchText = null) + { + if (!await CanViewList()) + { + return Redirect("not-found"); + } + + var type = (string)RouteData.DataTokens["type"]; + + return View(type, await GetContents(type, searchText)); + } + + [HttpPost] + public IActionResult Search([FromForm] string type, [FromForm] string searchText) + { + return RedirectToRoute(type + "ListView", new { searchText = searchText }); + } + + [HttpGet] + public async Task GlobalSearch(string searchText = "") + { + if (!User.Identity.IsAuthenticated) + { + return Unauthorized(); + } + + var results = await _entitySearcher.SearchContentItemsAsync("*", searchText); + + return new ObjectResult(results); + } + + private async Task> GetShapes(IEnumerable contentItems) + { + var shapes = await Task.WhenAll(contentItems.Select(async contentItem => + { + return await _contentItemDisplayManager.BuildDisplayAsync(contentItem, _updateModelAccessor.ModelUpdater, "Summary"); + })); + + return shapes; + } + + private async Task> GetContents(string type, string searchText) + { + if (String.IsNullOrWhiteSpace(searchText)) + { + searchText = "*"; + ViewData["searchText"] = ""; + } + else + { + ViewData["searchText"] = searchText; + } + + var contentItems = await _entitySearcher.SearchContentItemsAsync(type, searchText); + return await GetShapes(contentItems); + } + + private async Task CanViewList() + { + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ListContent)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarFormPartDisplayDriver.cs b/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarFormPartDisplayDriver.cs new file mode 100644 index 000000000..821ba02fa --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarFormPartDisplayDriver.cs @@ -0,0 +1,151 @@ +using System; +using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.ContentManagement.Display.Models; +using OrchardCore.DisplayManagement.Views; +using StatCan.OrchardCore.Radar.Models; +using StatCan.OrchardCore.Radar.Services; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Drivers +{ + public class RadarFormPartDisplayDriver : ContentPartDisplayDriver + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly FormValueProvider _formValueProvider; + private readonly FormOptionsProvider _formOptionsProvider; + + public RadarFormPartDisplayDriver(IHttpContextAccessor httpContextAccessor, FormValueProvider formValueProvider, FormOptionsProvider formOptionsProvider) + { + _httpContextAccessor = httpContextAccessor; + _formValueProvider = formValueProvider; + _formOptionsProvider = formOptionsProvider; + } + + public override IDisplayResult Display(RadarFormPart part, BuildPartDisplayContext context) + { + var requestPath = _httpContextAccessor.HttpContext.Request.Path; + + var parentType = ""; + var entityType = ""; + + if (IsFormPath(requestPath)) + { + bool pathResolved = TryExtractParentAndChildTypeFromPath(requestPath, out parentType, out entityType) || TryExtractTypeFromPath(requestPath, out entityType); + + if (!pathResolved) + { + return null; + } + } + else + { + return null; + } + + return Initialize(GetDisplayShapeType(context), async model => + { + FormModel initialValues = null; + + if (!string.IsNullOrEmpty(parentType)) + { + // Speical case for entities that have parents + (string parentId, string childId) = ExtractParentAndChildIdFromPath(requestPath); + + initialValues = await _formValueProvider.GetInitialValuesAsync(entityType, parentId, childId); + } + else + { + var id = ExtractIdFromPath(requestPath); + initialValues = await _formValueProvider.GetInitialValuesAsync(entityType, id); + } + + if (initialValues == null) + { + _httpContextAccessor.HttpContext.Response.StatusCode = 404; + _httpContextAccessor.HttpContext.Response.Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found", false); + } + + var options = await _formOptionsProvider.GetOptionsAsync(entityType); + + model.InitialValues = JsonConvert.SerializeObject(initialValues).ToString(); + model.Options = JsonConvert.SerializeObject(options).ToString(); + }).Location("Detail", "FormValue:1"); + } + + // Tries to extract the type of entity. The path is expected to have the form /{entity}/{create,update}/{id} + private bool TryExtractTypeFromPath(string path, out string entityName) + { + var values = path.Substring(1).Split("/"); + + // < 2 means some values are missing + if (values.Length < 2) + { + entityName = ""; + return false; + } + + entityName = values[0]; + + return true; + } + + // Extracts the type of the parent entities. Used for contents that belong to other contents + private bool TryExtractParentAndChildTypeFromPath(string path, out string parentName, out string childName) + { + var values = path.Substring(1).Split("/"); + + // < 4 means some values are missing + if (values.Length < 4) + { + parentName = ""; + childName = ""; + + return false; + } + + parentName = values[0]; + childName = values[2]; + + return true; + } + + private string ExtractIdFromPath(string path) + { + var values = path.Substring(1).Split("/"); + + // < 3 means some values are missing + if (values.Length < 3) + { + return null; + } + + return values[values.Length - 1]; + } + + private (string, string) ExtractParentAndChildIdFromPath(string path) + { + var values = path.Substring(1).Split("/"); + + // < 5 means some values are missing + if (values.Length < 4) + { + return (null, null); + } + else if(values.Length < 5) + { + return (values[1], null); + } + + return (values[1], values[values.Length - 1]); + } + + private bool IsFormPath(string path) + { + var values = path.Split("/"); + + return Array.Exists(values, pathValue => pathValue == "create" || pathValue == "update"); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarPermissionPartDisplayDriver.cs b/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarPermissionPartDisplayDriver.cs new file mode 100644 index 000000000..8746d6901 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Drivers/RadarPermissionPartDisplayDriver.cs @@ -0,0 +1,71 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.ContentManagement.Display.Models; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.ContentManagement; +using StatCan.OrchardCore.Radar.Models; +using StatCan.OrchardCore.Radar.Helpers; + +namespace StatCan.OrchardCore.Radar.Drivers +{ + public class RadarPermissionPartDisplayDriver : ContentPartDisplayDriver + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IContentManager _contentManager; + + public RadarPermissionPartDisplayDriver(IHttpContextAccessor httpContextAccessor, IContentManager contentManager) + { + _httpContextAccessor = httpContextAccessor; + _contentManager = contentManager; + } + + // General idea is that only owner can see own draft item and admin can see everything + public override async Task DisplayAsync(RadarPermissionPart part, BuildPartDisplayContext context) + { + var user = _httpContextAccessor.HttpContext.User; + if (user == null) + { + _httpContextAccessor.HttpContext.Response.StatusCode = 404; + _httpContextAccessor.HttpContext.Response.Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found", false); + } + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier).ToString(); + + if (!string.IsNullOrEmpty(part.ParentContentItemId)) + { + var parent = await _contentManager.GetAsync(part.ParentContentItemId); + + var parentPermission = parent.As(); + + if (!parentPermission.Published && !(userId == parentPermission.Owner) && !user.IsInRole("Administrator")) + { + _httpContextAccessor.HttpContext.Response.StatusCode = 404; + _httpContextAccessor.HttpContext.Response.Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found", false); + } + } + + if (part.Published) + { + return null; + } + + // Assume admin owns everything + if (user.IsInRole("Administrator")) + { + return null; + } + + if (!(userId == part.Owner)) + { + _httpContextAccessor.HttpContext.Response.StatusCode = 404; + _httpContextAccessor.HttpContext.Response.Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found", false); + } + + return null; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Filters/ResourceInjectionFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Filters/ResourceInjectionFilter.cs new file mode 100644 index 000000000..411285b5a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Filters/ResourceInjectionFilter.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using OrchardCore.ResourceManagement; +using System.Threading.Tasks; + +namespace StatCan.OrchardCore.Radar.Filters +{ + /* + Because Radar is implemented as a module and uses the Vuetify theme therefore Radar related + stylesheets must be injected in the every view. In order to avoid redundancy, this filter + is used to inject Radar stylesheets on every view except admin. + */ + public class ResourceInjectionFilter : IAsyncResultFilter + { + private readonly IResourceManager _resourceManager; + + public ResourceInjectionFilter(IResourceManager resourceManager) => _resourceManager = resourceManager; + + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + string url = context.HttpContext.Request.Path; + // Don't inject into the admin views + if (context.Result is PartialViewResult || url.ToLower().Contains("admin")) + { + await next(); + + return; + } + + _resourceManager.RegisterResource("stylesheet", "Radar-styles"); + + await next(); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/ArtifactFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ArtifactFormModel.cs new file mode 100644 index 000000000..753fa0cf6 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ArtifactFormModel.cs @@ -0,0 +1,10 @@ +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class ArtifactFormModel : FormModel + { + public string ParentId { get; set; } + public string Url { get; set; } + + public string PublishStatus { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/CommunityFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/CommunityFormModel.cs new file mode 100644 index 000000000..9eb731096 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/CommunityFormModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class CommunityFormModel : EntityFormModel + { + public ICollection> CommunityMembers { get; set; } + + public IDictionary Type { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormModel.cs new file mode 100644 index 000000000..5b9442114 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormModel.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class EntityFormModel : FormModel + { + public ICollection> Topics { get; set; } + public ICollection> RelatedEntities { get; set; } + + public string PublishStatus { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormOptionModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormOptionModel.cs new file mode 100644 index 000000000..397b30321 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EntityFormOptionModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class EntityFormOptionModel : FormOptionModel + { + public ICollection> TypeOptions { get; set; } + + public string[] PublishOptions { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/EventFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EventFormModel.cs new file mode 100644 index 000000000..76a5de462 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/EventFormModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class EventFormModel : EntityFormModel + { + public string StartDate { get; set; } + public string EndDate { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + public ICollection> Attendees { get; set; } + public ICollection> EventOrganizers { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormModel.cs new file mode 100644 index 000000000..f0a6d20bd --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormModel.cs @@ -0,0 +1,10 @@ +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class FormModel + { + public string Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string[] Roles { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormOptionModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormOptionModel.cs new file mode 100644 index 000000000..af58c8889 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/FormOptionModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class FormOptionModel + { + public ICollection RoleOptions { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProjectFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProjectFormModel.cs new file mode 100644 index 000000000..fdf83cf6b --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProjectFormModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class ProjectFormModel : EntityFormModel + { + public ICollection> ProjectMembers { get; set; } + + public IDictionary Type { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProposalFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProposalFormModel.cs new file mode 100644 index 000000000..16e07df92 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/ProposalFormModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class ProposalFormModel : EntityFormModel + { + public IDictionary Type { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/FormModels/TopicFormModel.cs b/src/Apps/StatCan.OrchardCore.Radar/FormModels/TopicFormModel.cs new file mode 100644 index 000000000..ff6f735df --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/FormModels/TopicFormModel.cs @@ -0,0 +1,7 @@ +namespace StatCan.OrchardCore.Radar.FormModels +{ + public class TopicFormModel : FormModel + { + + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Handlers/RadarPermissionPartHandler.cs b/src/Apps/StatCan.OrchardCore.Radar/Handlers/RadarPermissionPartHandler.cs new file mode 100644 index 000000000..80c28dc1b --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Handlers/RadarPermissionPartHandler.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.Flows.Models; +using StatCan.OrchardCore.Radar.Models; + +namespace StatCan.OrchardCore.Radar.Handlers +{ + public class RadarPermissionPartHandler : ContentPartHandler + { + // Adds a back reference to the parent on the child. Used for permission checking since child inherits parent permission. + public override Task UpdatedAsync(UpdateContentContext context, RadarPermissionPart instance) + { + context.ContentItem.Content.RadarPermissionPart.ContentItemId = context.ContentItem.ContentItemId; + context.ContentItem.Content.RadarPermissionPart.ContentType = context.ContentItem.ContentType; + context.ContentItem.Content.RadarPermissionPart.Owner = context.ContentItem.Owner; + context.ContentItem.Content.RadarPermissionPart.Published = context.ContentItem.ContentType == "Artifact" ? context.ContentItem.Published : context.ContentItem.Content.RadarEntityPart.Publish.Value; + + var workspace = context.ContentItem.Get("Workspace"); + + if (workspace != null) + { + foreach (var artifact in workspace.ContentItems) + { + artifact.ContentItem.Content.RadarPermissionPart.ParentContentItemId = context.ContentItem.ContentItemId; + } + + context.ContentItem.Apply("Workspace", workspace); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Helpers/Ownership.cs b/src/Apps/StatCan.OrchardCore.Radar/Helpers/Ownership.cs new file mode 100644 index 000000000..49968c590 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Helpers/Ownership.cs @@ -0,0 +1,26 @@ +using System.Security.Claims; +using OrchardCore.ContentManagement; + +namespace StatCan.OrchardCore.Radar.Helpers +{ + public static class Ownership + { + public static bool IsOwner(ContentItem contentItem, ClaimsPrincipal user) + { + if(user == null) + { + return false; + } + + // Assume admin owns everything + if (user.IsInRole("Administrator")) + { + return true; + } + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier).ToString(); + + return userId == contentItem.Owner; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Indexes/RadarFormPartIndex.cs b/src/Apps/StatCan.OrchardCore.Radar/Indexes/RadarFormPartIndex.cs new file mode 100644 index 000000000..ef8f07ff1 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Indexes/RadarFormPartIndex.cs @@ -0,0 +1,53 @@ +using System; +using OrchardCore.ContentManagement; +using YesSql.Indexes; +using StatCan.OrchardCore.Radar.Models; + +namespace StatCan.OrchardCore.Radar.Indexes +{ + public class RadarFormPartIndex : MapIndex + { + public string DisplayText { get; set; } + + public string ContentItemId { get; set; } + + public string ContentType { get; set; } + + public bool Published { get; set; } + + public DateTime? PublishedUtc { get; set; } + } + + public class RadarFormPartIndexProvider : IndexProvider + { + public override void Describe(DescribeContext context) + { + context.For() + .Map(contentItem => + { + var radarFormPart = contentItem.As(); + + // Ignore if does not use RadarFormPart + if (radarFormPart == null) + { + return null; + } + + // Ignore previous versions of the content item + if (!contentItem.Latest) + { + return null; + } + + return new RadarFormPartIndex + { + DisplayText = contentItem.DisplayText, + ContentItemId = contentItem.ContentItemId, + ContentType = contentItem.ContentType, + Published = contentItem.Published, + PublishedUtc = contentItem.PublishedUtc + }; + }); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentOwnershipFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentOwnershipFilter.cs new file mode 100644 index 000000000..99b33c08a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentOwnershipFilter.cs @@ -0,0 +1,52 @@ +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; +using OrchardCore.ContentManagement; +using OrchardCore.Liquid; +using System; +using System.Threading.Tasks; + +namespace StatCan.OrchardCore.Radar.Liquid +{ + public class ContentOwnershipFilter : ILiquidFilter + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public ContentOwnershipFilter(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + var item = input.ToObjectValue() as ContentItem; + + if (item == null) + { + throw new ArgumentException("ContentItem missing while calling 'is_owner' filter"); + } + + var user = _httpContextAccessor.HttpContext.User; + + if (!user.Identity.IsAuthenticated) + { + return new ValueTask(BooleanValue.False); + } + + if (user.IsInRole("Administrator")) + { + return new ValueTask(BooleanValue.True); + } + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier).ToString(); + + if(userId == item.ContentItemId) + { + return new ValueTask(BooleanValue.True); + } + + return new ValueTask(BooleanValue.False); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentUpdateUrlFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentUpdateUrlFilter.cs new file mode 100644 index 000000000..eeabf8b8a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ContentUpdateUrlFilter.cs @@ -0,0 +1,85 @@ +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Routing; +using OrchardCore.ContentManagement; +using OrchardCore.Liquid; +using System; +using System.Threading.Tasks; + +namespace StatCan.OrchardCore.Radar.Liquid +{ + public class ContentUpdateUrlFilter : ILiquidFilter + { + private readonly IUrlHelperFactory _urlHelperFactory; + private readonly IHttpContextAccessor _httpContextAccessor; + + public ContentUpdateUrlFilter(IUrlHelperFactory urlHelperFactory, IHttpContextAccessor httpContextAccessor) + { + _urlHelperFactory = urlHelperFactory; + _httpContextAccessor = httpContextAccessor; + } + + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + var item = input.ToObjectValue() as ContentItem; + + if (item == null) + { + throw new ArgumentException("ContentItem missing while calling 'content_update_url' filter"); + } + + var urlHelper = _urlHelperFactory.GetUrlHelper(context.ViewContext); + string url = ""; + + if (item.ContentType == "Project") + { + url = $"~/projects/update/{item.ContentItemId}"; + } + else if (item.ContentType == "Proposal") + { + url = $"~/proposals/update/{item.ContentItemId}"; + } + else if (item.ContentType == "Community") + { + url = $"~/communities/update/{item.ContentItemId}"; + } + else if (item.ContentType == "Event") + { + url = $"~/events/update/{item.ContentItemId}"; + } + else if (item.ContentType == "Topic") + { + url = $"~/topics/update/{item.ContentItemId}"; + } + else if (item.ContentType == "Artifact") + { + url = GetArtifactPath(_httpContextAccessor.HttpContext.Request.Path, item); + } + + if(!(bool)arguments["relative"].ToObjectValue()) + { + return new ValueTask(new StringValue((urlHelper).Content(url))); + } + + return FluidValue.Create(url, context.Options); + } + + // The logic here is purely based on the structure of the url. + private string GetArtifactPath(string path, ContentItem artifact) + { + string[] pathValues = path.Substring(1).Split("/"); + + if (pathValues.Length == 2) + { + return $"{pathValues[pathValues.Length - 1]}/artifacts/update/{artifact.ContentItemId}"; + } + else if(pathValues.Length == 4) + { + return $"update/{artifact.ContentItemId}"; + } + + return ""; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Liquid/ListOnlyFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ListOnlyFilter.cs new file mode 100644 index 000000000..1b0ebd0e8 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ListOnlyFilter.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Fluid; +using Fluid.Values; +using OrchardCore.ContentManagement; +using OrchardCore.Liquid; +using System.Collections; +using System.Collections.Generic; + +namespace StatCan.OrchardCore.Radar.Liquid +{ + public class ListOnlyFilter : ILiquidFilter + { + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + var items = input.ToObjectValue() as IEnumerable; + var contentItems = new List(); + + var limit = Decimal.ToInt32(arguments["limit"].ToNumberValue()); + var num = 0; + + foreach(var item in items) + { + var contentItem = item as ContentItem; + + if (contentItem == null) + { + if (item is JObject jObject) + { + contentItem = jObject.ToObject(); + } + } + + contentItems.Add(contentItem); + num++; + + if(num == limit) + { + break; + } + } + + return FluidValue.Create(contentItems, context.Options); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Liquid/ParentContentItemIdFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ParentContentItemIdFilter.cs new file mode 100644 index 000000000..3dea0d684 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Liquid/ParentContentItemIdFilter.cs @@ -0,0 +1,42 @@ +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Http; +using OrchardCore.Liquid; +using System; +using System.Threading.Tasks; + +namespace StatCan.OrchardCore.Radar.Liquid +{ + public class ParentContentItemIdFilter : ILiquidFilter + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public ParentContentItemIdFilter(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + var path = input.ToObjectValue() as string; + + if (path == null) + { + throw new ArgumentException("Path missing while calling 'parent_contentitem_id' filter"); + } + + string[] pathValues = path.Substring(1).Split("/"); + + if (pathValues.Length == 2) + { + return new ValueTask(StringValue.Create(pathValues[pathValues.Length - 1])); + } + else if (pathValues.Length == 4) + { + return new ValueTask(StringValue.Create(pathValues[1])); + } + + return new ValueTask(StringValue.Empty); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Liquid/RemoveUnviewableContentFilter.cs b/src/Apps/StatCan.OrchardCore.Radar/Liquid/RemoveUnviewableContentFilter.cs new file mode 100644 index 000000000..6416a56e2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Liquid/RemoveUnviewableContentFilter.cs @@ -0,0 +1,78 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement; +using OrchardCore.Liquid; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.Models; + +namespace StatCan.OrchardCore.Radar.Liquid +{ + public class RemoveUnviewableContentFilter : ILiquidFilter + { + + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IContentManager _contentManager; + + public RemoveUnviewableContentFilter(IHttpContextAccessor httpContextAccessor, IContentManager contentManager) + { + _httpContextAccessor = httpContextAccessor; + _contentManager = contentManager; + } + + public async ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + var user = _httpContextAccessor.HttpContext.User; + if (user == null) + { + _httpContextAccessor.HttpContext.Response.StatusCode = 404; + _httpContextAccessor.HttpContext.Response.Redirect($"{_httpContextAccessor.HttpContext.Request.PathBase}/not-found", false); + } + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier).ToString(); + + var items = input.ToObjectValue() as IEnumerable; + var contentItems = new List(); + + foreach (var item in items) + { + var contentItem = item as ContentItem; + + if (contentItem == null) + { + if (item is JObject jObject) + { + contentItem = jObject.ToObject(); + } + } + + var permissionPart = contentItem.As(); + + if (!string.IsNullOrEmpty(permissionPart.ParentContentItemId)) + { + var parent = await _contentManager.GetAsync(permissionPart.ParentContentItemId); + + var parentPermission = parent.As(); + + if (!parentPermission.Published && !(userId == parentPermission.Owner) && !user.IsInRole("Administrator")) + { + continue; + } + } + + if (!permissionPart.Published && !(userId == permissionPart.Owner) && !user.IsInRole("Administrator")) + { + continue; + } + + contentItems.Add(contentItem); + } + + return FluidValue.Create(contentItems, context.Options); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Manifest.cs b/src/Apps/StatCan.OrchardCore.Radar/Manifest.cs new file mode 100644 index 000000000..97ff61d2f --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Manifest.cs @@ -0,0 +1,57 @@ +using OrchardCore.Modules.Manifest; +using StatCan.OrchardCore.Radar; +using static StatCan.OrchardCore.Manifest.StatCanManifestConstants; + +[assembly: Module( + Name = "Digital Radar", + Author = DigitalInnovationTeam, + Website = DigitalInnovationWebsite, + Version = Version, + Description = "The Digital Radar platform", + Category = "Applications" +)] + +[assembly: Feature( + Id = Constants.Features.Radar, + Name = "Radar", + Description = "The Digital Radar Application", + Category = "Applications", + Dependencies = new[] + { + "OrchardCore.Admin", + "OrchardCore.Alias", + "OrchardCore.Autoroute", + "OrchardCore.Contents", + "OrhcardCore.ContentManagement", + "OrchardCore.ContentLocalization", + "OrchardCore.ContentLocalization.ContentCulturePicker", + "OrchardCore.ContentFields", + "OrchardCore.Features", + "OrchardCore.Flows", + "OrchardCore.HomeRoute", + "OrchardCore.Indexing", + "OrchardCore.Liquid", + "OrchardCore.Lucene", + "OrchardCore.Layers", + "OrchardCore.Localization", + "OrchardCore.Media", + "OrchardCore.Menu", + "OrchardCore.Queries", + "OrchardCore.Queries.Sql", + "OrchardCore.Resources", + "OrchardCore.Roles", + "OrchardCore.Settings", + "OrchardCore.Shortcodes", + "OrchardCore.Taxonomies", + "OrchardCore.Themes", + "OrchardCore.Title", + "OrchardCore.Users", + "OrchardCore.Users.CustomUserSettings", + "OrchardCore.Users.Registration", + "OrchardCore.Widgets", + "StatCan.OrchardCore.ContentPermissions.Indexing", + "StatCan.OrchardCore.DisplayHelpers", + "StatCan.OrchardCore.Menu", + "StatCan.OrchardCore.VueForms.Localized" + } +)] diff --git a/src/Apps/StatCan.OrchardCore.Radar/Migrations/ContentTypeMigrations.cs b/src/Apps/StatCan.OrchardCore.Radar/Migrations/ContentTypeMigrations.cs new file mode 100644 index 000000000..a26e05901 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Migrations/ContentTypeMigrations.cs @@ -0,0 +1,1251 @@ +using System; +using System.Collections.Generic; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.ContentManagement.Metadata.Settings; +using OrchardCore.Data.Migration; +using OrchardCore.Title.Models; +using OrchardCore.ContentFields.Settings; +using OrchardCore.Taxonomies.Settings; +using OrchardCore.Flows.Models; +using OrchardCore.Contents.Models; +using OrchardCore.Autoroute.Models; +using OrchardCore.Media.Settings; +using Etch.OrchardCore.ContentPermissions.Models; + +namespace StatCan.OrchardCore.Radar.Migrations +{ + public class ContentTypeMigrations : DataMigration + { + + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly IContentManager _contentManager; + + private readonly Dictionary _taxonomyIds; + + public ContentTypeMigrations(IContentDefinitionManager contentDefinitionManager, IContentManager contentManager) + { + _contentDefinitionManager = contentDefinitionManager; + _contentManager = contentManager; + + _taxonomyIds = new Dictionary(); + } + + public int Create() + { + CreateTaxonomies(); + CreateArtifact(); + CreateRadarEntityPart(); + CreateRadarFormPart(); + CreateRadarPermissionPart(); + CreateProposal(); + CreateProject(); + CreateEvent(); + CreateCommunity(); + + CreateForm(); + + CreateLandingPage(); + CreateAppBar(); + CreateNavigationDrawer(); + CreateFooter(); + + CreateUserProfile(); + + return 1; + } + + private void CreateRadarPermissionPart() + { + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.RadarPermissionPart, part => part + .Attachable() + .WithDescription("Draft/publish status check") + ); + } + + private void CreateForm() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Form, type => type + .DisplayedAs("Form") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("Form", part => part + .WithPosition("1") + ) + .WithPart("RadarFormPart", part => part + .WithPosition("2") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + ) + .WithPart("FlowPart", part => part + .WithPosition("3") + ) + ); + } + + private void CreateTaxonomies() + { + // Add content permission to Taxonomy + _contentDefinitionManager.AlterTypeDefinition("Taxonomy", type => type + .WithPart("ContentPermissionsPart", part => part + .WithPosition("5") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found" + }) + ) + ); + + // Topic + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Topic, type => type + .DisplayedAs("Topic") + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("Topic", part => part + .WithPosition("2") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.Topic.Name.Text }}", + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + Pattern = "{{ ContentItem.ContentItemId }}", + ManageContainedItemRoutes = true, + }) + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("4") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Topic, part => part + .WithField("Name", field => field + .OfType("TextField") + .WithDisplayName("Name") + ) + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + ) + ); + + // Proposal Type + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.ProposalType, type => type + .DisplayedAs("Proposal Type") + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("ProposalType", part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.ProposalType.Name.Text }}", + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.ProposalType, part => part + .WithField("Name", field => field + .OfType("TextField") + .WithDisplayName("Name") + .WithPosition("0") + ) + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + .WithPosition("1") + ) + ); + + // Project Type + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.ProjectType, type => type + .DisplayedAs("Project Type") + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("ProjectType", part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.ProjectType.Name.Text }}", + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.ProjectType, part => part + .WithField("Name", field => field + .OfType("TextField") + .WithDisplayName("Name") + .WithPosition("0") + ) + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + .WithPosition("1") + ) + ); + + // Community Type + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.CommunityType, type => type + .DisplayedAs("Community Type") + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("CommunityType", part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.CommunityType.Name.Text }}", + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.CommunityType, part => part + .WithField("Name", field => field + .OfType("TextField") + .WithDisplayName("Name") + .WithPosition("0") + ) + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + .WithPosition("1") + ) + ); + } + + private void CreateArtifact() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Artifact, type => type + .DisplayedAs("Artifact") + .Draftable() + .Versionable() + .Securable() + .WithPart("Artifact", part => part + .WithPosition("2") + ) + .WithPart("TitlePart", part => part + .WithPosition("1") + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("3") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + Pattern = "{{ ContentItem.ContentItemId }}", + ManageContainedItemRoutes = true, + }) + ) + .WithPart(Constants.ContentTypes.RadarPermissionPart, part => part + .WithPosition("4") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Artifact, part => part + .Attachable() + .WithField("URL", field => field + .OfType("TextField") + .WithDisplayName("URL") + ) + .WithField("LocalizationSet", field => field + .OfType("TextField") + .WithDisplayName("LocalizationSet") + .WithPosition("1") + ) + ); + } + + private void CreateRadarEntityPart() + { + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.RadarEntityPart, part => part + .Attachable() + .WithDescription("Provides fields for an entity in Radar") + .WithField("Name", field => field + .OfType("TextField") + .WithDisplayName("Name") + ) + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + ) + .WithField("Topics", field => field + .OfType("TaxonomyField") + .WithDisplayName("Topics") + .WithEditor("Tags") + .WithDisplayMode("Tags") + .WithSettings(new TaxonomyFieldSettings + { + Unique = false, + TaxonomyContentItemId = _taxonomyIds.GetValueOrDefault("Topics") + }) + .WithSettings(new TaxonomyFieldTagsEditorSettings + { + Open = false, + }) + + ) + .WithField("RelatedEntity", field => field + .OfType("LocalizationSetContentPickerField") + .WithDisplayName("Related Entity") + .WithSettings(new LocalizationSetContentPickerFieldSettings + { + Multiple = true, + Required = false, + DisplayedContentTypes = new[] { Constants.ContentTypes.Project, Constants.ContentTypes.Proposal, Constants.ContentTypes.Community, Constants.ContentTypes.Event }, + }) + ) + .WithField("Publish", field => field + .OfType("BooleanField") + .WithDisplayName("Publish") + .WithPosition("4") + ) + ); + } + + private void CreateRadarFormPart() + { + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.RadarFormPart, part => part + .Attachable() + .WithDescription("Holds the initial value for a form") + ); + } + + private void CreateProposal() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Proposal, type => type + .DisplayedAs("Proposal") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart(Constants.ContentTypes.Proposal, part => part + .WithPosition("2") + ) + .WithPart(Constants.ContentTypes.RadarEntityPart, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.RadarEntityPart.Name.Text }}", + }) + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("3") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + .WithPart("Workspace", "BagPart", part => part + .WithDisplayName("Workspace") + .WithDescription("Add an Artifact to your workspace of this proposal") + .WithPosition("4") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.Artifact }, + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + AllowRouteContainedItems = true, + Pattern = "{{ \"proposals\" | t | append: \"/\" | append: ContentItem.ContentItemId }}", + }) + ) + .WithPart(Constants.ContentTypes.RadarPermissionPart, part => part + .WithPosition("8") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Proposal, part => part + .WithField("Type", field => field + .OfType("TaxonomyField") + .WithDisplayName("Type") + .WithEditor("Tags") + .WithDisplayMode("Tags") + .WithPosition("0") + .WithSettings(new TaxonomyFieldSettings + { + Required = true, + TaxonomyContentItemId = _taxonomyIds.GetValueOrDefault("Proposal Types"), + Unique = true, + }) + .WithSettings(new TaxonomyFieldTagsEditorSettings + { + Open = false, + }) + ) + ); + } + + private void CreateProject() + { + // Project Member + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.ProjectMember, type => type + .DisplayedAs("Project Member") + .Draftable() + .Versionable() + .Securable() + .WithPart(Constants.ContentTypes.ProjectMember, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = @"{% assign user = ContentItem.Content.ProjectMember.Member.UserIds | first | users_by_id %} + {{ user.Properties.UserProfile.UserProfile.FirstName.Text }} {{ user.Properties.UserProfile.UserProfile.LastName.Text }} ", + }) + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.ProjectMember, part => part + .WithField("Member", field => field + .OfType("UserPickerField") + .WithDisplayName("Member") + .WithPosition("0") + .WithSettings(new UserPickerFieldSettings + { + Required = true, + DisplayAllUsers = true, + DisplayedRoles = Array.Empty(), + }) + ) + .WithField("Role", field => field + .OfType("TextField") + .WithDisplayName("Role") + .WithPosition("1") + ) + ); + + // Project + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Project, type => type + .DisplayedAs("Project") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart(Constants.ContentTypes.Project, part => part + .WithPosition("2") + ) + .WithPart(Constants.ContentTypes.RadarEntityPart, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.RadarEntityPart.Name.Text }}", + }) + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("3") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + .WithPart("Workspace", "BagPart", part => part + .WithDisplayName("Workspace") + .WithDescription("Add an Artifact to your workspace of this project") + .WithPosition("5") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.Artifact }, + }) + ) + .WithPart(Constants.ContentTypes.ProjectMember, "BagPart", part => part + .WithDisplayName("Project Member") + .WithDescription("Add a member to this project") + .WithPosition("4") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.ProjectMember }, + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + AllowRouteContainedItems = true, + Pattern = "{{ \"projects\" | t | append: \"/\" | append: ContentItem.ContentItemId }}", + }) + ) + .WithPart(Constants.ContentTypes.RadarPermissionPart, part => part + .WithPosition("8") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Project, part => part + .WithField("Type", field => field + .OfType("TaxonomyField") + .WithEditor("Tags") + .WithDisplayMode("Tags") + .WithDisplayName("Type") + .WithPosition("0") + .WithSettings(new TaxonomyFieldSettings + { + Required = true, + TaxonomyContentItemId = _taxonomyIds.GetValueOrDefault("Project Types"), + Unique = true, + }) + .WithSettings(new TaxonomyFieldTagsEditorSettings + { + Open = false, + }) + ) + ); + } + + private void CreateEvent() + { + // Event Organizer + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.EventOrganizer, type => type + .DisplayedAs("Event Organizer") + .Draftable() + .Versionable() + .Securable() + .WithPart(Constants.ContentTypes.EventOrganizer, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = @"{% assign user = ContentItem.Content.EventOrganizer.Organizer.UserIds | first | users_by_id %} + {{ user.Properties.UserProfile.UserProfile.FirstName.Text }} {{ user.Properties.UserProfile.UserProfile.LastName.Text }}", + }) + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.EventOrganizer, part => part + .WithField("Organizer", field => field + .OfType("UserPickerField") + .WithDisplayName("Organizer") + .WithPosition("0") + .WithSettings(new UserPickerFieldSettings + { + Required = true, + DisplayAllUsers = true, + DisplayedRoles = Array.Empty(), + }) + ) + .WithField("Role", field => field + .OfType("TextField") + .WithDisplayName("Role") + .WithPosition("1") + ) + ); + + // Event + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Event, type => type + .DisplayedAs("Event") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart(Constants.ContentTypes.Event, part => part + .WithPosition("2") + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("3") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + .WithPart(Constants.ContentTypes.RadarEntityPart, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedDisabled, + Pattern = "{{ ContentItem.Content.RadarEntityPart.Name.Text }}", + }) + ) + .WithPart(Constants.ContentTypes.EventOrganizer, "BagPart", part => part + .WithDisplayName("Event Organizer") + .WithDescription("Event Organizer") + .WithPosition("4") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.EventOrganizer }, + }) + ) + .WithPart("Workspace", "BagPart", part => part + .WithDisplayName("Workspace") + .WithDescription("Add an Artifact to your workspace of this event") + .WithPosition("5") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.Artifact }, + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + AllowRouteContainedItems = true, + Pattern = "{{ \"events\" | t | append: \"/\" | append: ContentItem.ContentItemId }}", + }) + ) + .WithPart(Constants.ContentTypes.RadarPermissionPart, part => part + .WithPosition("8") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Event, part => part + .WithField("Attendees", field => field + .OfType("UserPickerField") + .WithDisplayName("Attendees") + .WithPosition("0") + .WithSettings(new UserPickerFieldSettings + { + Multiple = true, + DisplayAllUsers = true, + DisplayedRoles = Array.Empty(), + }) + ) + .WithField("StartDate", field => field + .OfType("DateTimeField") + .WithDisplayName("Start Date") + .WithPosition("1") + ) + .WithField("EndDate", field => field + .OfType("DateTimeField") + .WithDisplayName("End Date") + .WithPosition("2") + ) + ); + } + + private void CreateCommunity() + { + // Community Member + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.CommunityMember, type => type + .DisplayedAs("Community Member") + .Draftable() + .Versionable() + .Securable() + .WithPart(Constants.ContentTypes.CommunityMember, part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = @"{% assign user = ContentItem.Content.CommunityMember.Member.UserIds | first | users_by_id %} + {{ user.Properties.UserProfile.UserProfile.FirstName.Text }} {{ user.Properties.UserProfile.UserProfile.LastName.Text }}", + }) + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.CommunityMember, part => part + .WithField("Member", field => field + .OfType("UserPickerField") + .WithDisplayName("Member") + .WithPosition("0") + .WithSettings(new UserPickerFieldSettings + { + Required = true, + DisplayAllUsers = true, + DisplayedRoles = Array.Empty(), + }) + ) + .WithField("Role", field => field + .OfType("TextField") + .WithDisplayName("Role") + .WithPosition("1") + ) + ); + + // Community + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Community, type => type + .DisplayedAs("Community") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart(Constants.ContentTypes.Community, part => part + .WithPosition("2") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + .WithSettings(new TitlePartSettings + { + Options = TitlePartOptions.GeneratedHidden, + Pattern = "{{ ContentItem.Content.RadarEntityPart.Name.Text }}", + }) + ) + .WithPart(Constants.ContentTypes.RadarEntityPart, part => part + .WithPosition("1") + ) + .WithPart("ContentPermissionsPart", part => part + .WithPosition("3") + .WithSettings(new ContentPermissionsPartSettings + { + RedirectUrl = "not-found", + }) + ) + .WithPart("Workspace", "BagPart", part => part + .WithDisplayName("Workspace") + .WithDescription("Add an Artifact to your workspace of this community") + .WithPosition("5") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.Artifact }, + }) + ) + .WithPart(Constants.ContentTypes.CommunityMember, "BagPart", part => part + .WithDisplayName("Community Member") + .WithDescription("Add a member to this community") + .WithPosition("4") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.CommunityMember }, + }) + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + .WithSettings(new AutoroutePartSettings + { + AllowRouteContainedItems = true, + Pattern = "{{ \"communities\" | t | append: \"/\" | append: ContentItem.ContentItemId }}", + }) + ) + .WithPart(Constants.ContentTypes.RadarPermissionPart, part => part + .WithPosition("8") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Community, part => part + .WithField("Type", field => field + .OfType("TaxonomyField") + .WithDisplayName("Type") + .WithEditor("Tags") + .WithDisplayMode("Tags") + .WithPosition("0") + .WithSettings(new TaxonomyFieldSettings + { + Required = true, + TaxonomyContentItemId = _taxonomyIds.GetValueOrDefault("Community Types"), + Unique = true, + }) + .WithSettings(new TaxonomyFieldTagsEditorSettings + { + Open = false, + }) + ) + ); + } + + private void CreateLandingPage() + { + // Entity Card + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.EntityCard, type => type + .DisplayedAs("Entity Card") + .Versionable() + .Securable() + .WithPart("EntityCard", part => part + .WithPosition("2") + ) + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart("TitlePart", part => part + .WithPosition("1") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.EntityCard, part => part + .WithField("Type", field => field + .OfType("TextField") + .WithDisplayName("Type") + .WithPosition("2") + ) + .WithField("Limit", field => field + .OfType("NumericField") + .WithDisplayName("Limit") + .WithPosition("3") + ) + .WithField("Caption", field => field + .OfType("TextField") + .WithDisplayName("Caption") + .WithPosition("0") + ) + .WithField("Icon", field => field + .OfType("TextField") + .WithDisplayName("Icon") + .WithPosition("1") + ) + .WithField("ButtonText", field => field + .OfType("TextField") + .WithDisplayName("Button Text") + .WithPosition("5") + ) + .WithField("ButtonLink", field => field + .OfType("TextField") + .WithDisplayName("Button Link") + .WithPosition("4") + ) + ); + + // Trending Card + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.TrendingCard, type => type + .DisplayedAs("Trending Card") + .Draftable() + .Versionable() + .Securable() + .WithPart("TrendingCard", part => part + .WithPosition("2") + ) + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart("TitlePart", part => part + .WithPosition("1") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.TrendingCard, part => part + .WithField("Caption", field => field + .OfType("TextField") + .WithDisplayName("Caption") + .WithPosition("0") + ) + .WithField("Type", field => field + .OfType("TextField") + .WithDisplayName("Type") + .WithPosition("2") + ) + .WithField("Icon", field => field + .OfType("TextField") + .WithDisplayName("Icon") + .WithPosition("1") + ) + .WithField("ButtonText", field => field + .OfType("TextField") + .WithDisplayName("Button Text") + .WithPosition("3") + ) + ); + + // Header list + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.LandingPageHeaderList, type => type + .DisplayedAs("Landing Page Header List") + .Draftable() + .Versionable() + .WithPart("LandingPageHeaderList", part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition("LandingPageHeaderList", part => part + .WithField("Caption", field => field + .OfType("TextField") + .WithDisplayName("Caption") + .WithPosition("0") + ) + ); + + // Footer card + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.LandingPageFooterCard, type => type + .DisplayedAs("Landing Page Footer Card") + .WithSettings(new FullTextAspectSettings + { + IncludeBodyAspect = false, + IncludeDisplayText = false, + }) + .WithPart("LandingPageFooterCard", part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.LandingPageFooterCard, part => part + .WithField("Icon", field => field + .OfType("TextField") + .WithDisplayName("Icon") + .WithPosition("0") + .WithSettings(new TextFieldSettings + { + Hint = "MD Icon name", + Required = true, + }) + ) + .WithField("Link", field => field + .OfType("TextField") + .WithDisplayName("Link") + .WithPosition("2") + .WithSettings(new TextFieldSettings + { + Hint = "A link that the card will go to", + }) + ) + .WithField("Caption", field => field + .OfType("TextField") + .WithDisplayName("Caption") + .WithPosition("1") + ) + ); + + + // Landing Page + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.LandingPage, type => type + .DisplayedAs("Landing Page") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .WithPart(Constants.ContentTypes.LandingPage, part => part + .WithPosition("3") + ) + .WithPart("AutoroutePart", part => part + .WithPosition("2") + ) + .WithPart("LocalizationPart", part => part + .WithPosition("0") + ) + .WithPart("TitlePart", part => part + .WithPosition("1") + ) + .WithPart("Activities", "BagPart", part => part + .WithDisplayName("Activities") + .WithDescription("Add the activity cards here") + .WithPosition("5") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.EntityCard }, + }) + ) + .WithPart("Trends", "BagPart", part => part + .WithDisplayName("Trends") + .WithDescription("Add the trend card here") + .WithPosition("6") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.TrendingCard }, + }) + ) + .WithPart("Footer", "BagPart", part => part + .WithDisplayName("Footer") + .WithDescription("Add cards to the landing page footer") + .WithPosition("7") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.LandingPageFooterCard }, + }) + ) + .WithPart("HeaderList", part => part + .WithDisplayName("Header List") + .WithDescription("Provides a collection behavior for your content item where you can place other content items.") + .WithPosition("4") + .WithSettings(new BagPartSettings + { + ContainedContentTypes = new[] { Constants.ContentTypes.LandingPageHeaderList }, + }) + ) + ); + + _contentDefinitionManager.AlterPartDefinition("LandingPage", part => part + .WithField("Description", field => field + .OfType("TextField") + .WithDisplayName("Description") + .WithPosition("0") + ) + ); + } + + private void CreateUserProfile() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.UserProfile, type => type + .DisplayedAs("User Profile") + .Versionable() + .Stereotype("CustomUserSettings") + .WithPart(Constants.ContentTypes.UserProfile, part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.UserProfile, part => part + .WithField("FirstName", field => field + .OfType("TextField") + .WithDisplayName("First Name") + .WithPosition("0") + ) + .WithField("LastName", field => field + .OfType("TextField") + .WithDisplayName("Last Name") + .WithPosition("1") + ) + ); + + } + + private void CreateAppBar() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.AppBar, type => type + .DisplayedAs("App Bar") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .Stereotype("Widget") + .WithPart("AppBar", part => part + .WithPosition("1") + ) + .WithPart("TitlePart", part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.AppBar, part => part + .WithField("Height", field => field + .OfType("NumericField") + .WithDisplayName("Height") + .WithPosition("1") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 9999, + }) + ) + .WithField("Width", field => field + .OfType("NumericField") + .WithDisplayName("Width") + .WithPosition("2") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 9999, + }) + ) + .WithField("Elevation", field => field + .OfType("NumericField") + .WithDisplayName("Elevation") + .WithEditor("Slider") + .WithPosition("3") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 24, + }) + ) + .WithField("ExtensionHeight", field => field + .OfType("NumericField") + .WithDisplayName("Extension Height") + .WithPosition("4") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 9999, + DefaultValue = "48", + }) + ) + .WithField("ScrollThreshold", field => field + .OfType("NumericField") + .WithDisplayName("Scroll Threshold") + .WithPosition("5") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 9999, + }) + ) + .WithField("Logo", field => field + .OfType("MediaField") + .WithDisplayName("Logo") + .WithSettings(new MediaFieldSettings + { + Multiple = false, + }) + ) + ); + } + + private void CreateNavigationDrawer() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.NavigationDrawer, type => type + .DisplayedAs("Navigation Drawer") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .Stereotype("Widget") + .WithPart("NavigationDrawer", part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.NavigationDrawer, part => part + .WithField("Color", field => field + .OfType("TextField") + .WithDisplayName("Color") + .WithPosition("0") + ) + .WithField("MobileBreakpoint", field => field + .OfType("NumericField") + .WithDisplayName("Mobile Breakpoint") + .WithPosition("4") + .WithSettings(new NumericFieldSettings + { + Minimum = 0, + Maximum = 9999, + }) + ) + .WithField("OverlayColor", field => field + .OfType("TextField") + .WithDisplayName("Overlay Color") + .WithPosition("5") + ) + .WithField("OverlayOpacity", field => field + .OfType("NumericField") + .WithDisplayName("Overlay Opacity") + .WithEditor("Slider") + .WithPosition("6") + .WithSettings(new NumericFieldSettings + { + Scale = 4, + Minimum = 0, + Maximum = 1, + }) + ) + .WithField("Height", field => field + .OfType("NumericField") + .WithDisplayName("Height") + .WithPosition("3") + ) + .WithField("Width", field => field + .OfType("NumericField") + .WithDisplayName("Width") + .WithPosition("1") + ) + .WithField("MiniVariantWidth", field => field + .OfType("NumericField") + .WithDisplayName("Mini Variant Width") + .WithPosition("2") + .WithSettings(new NumericFieldSettings + { + DefaultValue = "56", + }) + ) + ); + } + + private void CreateFooter() + { + _contentDefinitionManager.AlterTypeDefinition(Constants.ContentTypes.Footer, type => type + .DisplayedAs("Footer") + .Creatable() + .Listable() + .Draftable() + .Versionable() + .Securable() + .Stereotype("Widget") + .WithPart("Footer", part => part + .WithPosition("0") + ) + ); + + _contentDefinitionManager.AlterPartDefinition(Constants.ContentTypes.Footer, part => part + .WithField("Version", field => field + .OfType("TextField") + .WithDisplayName("Version") + .WithPosition("0") + ) + .WithField("Caption", field => field + .OfType("TextField") + .WithDisplayName("Caption") + .WithPosition("1") + ) + .WithField("Logo", field => field + .OfType("MediaField") + .WithDisplayName("Logo") + .WithPosition("2") + .WithSettings(new MediaFieldSettings + { + Multiple = false, + }) + ) + ); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Migrations/IndexMigrations.cs b/src/Apps/StatCan.OrchardCore.Radar/Migrations/IndexMigrations.cs new file mode 100644 index 000000000..b0ad40e98 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Migrations/IndexMigrations.cs @@ -0,0 +1,23 @@ +using System; +using YesSql.Sql; +using OrchardCore.Data.Migration; +using StatCan.OrchardCore.Radar.Indexes; + +namespace StatCan.OrchardCore.Radar.Migrations +{ + public class IndexMigrations : DataMigration + { + public int Create() + { + SchemaBuilder.CreateMapIndexTable(table => table + .Column(nameof(RadarFormPartIndex.DisplayText)) + .Column(nameof(RadarFormPartIndex.ContentItemId), column => column.WithLength(26)) + .Column(nameof(RadarFormPartIndex.ContentType)) + .Column(nameof(RadarFormPartIndex.Published)) + .Column(nameof(RadarFormPartIndex.PublishedUtc)) + ); + + return 1; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Models/RadarFormPart.cs b/src/Apps/StatCan.OrchardCore.Radar/Models/RadarFormPart.cs new file mode 100644 index 000000000..a663f3427 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Models/RadarFormPart.cs @@ -0,0 +1,15 @@ +using OrchardCore.ContentManagement; + +namespace StatCan.OrchardCore.Radar.Models +{ + public class RadarFormPart : ContentPart + { + public string Id { get; set; } + + //This proprety holds the initial values for the form. This property is only filled when building the form. + public string InitialValues { get; set; } + + // This property holds the static options for the form. This property is only filled when building the form. + public string Options { get; set; } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Models/RadarPermissionPart.cs b/src/Apps/StatCan.OrchardCore.Radar/Models/RadarPermissionPart.cs new file mode 100644 index 000000000..4df16a6af --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Models/RadarPermissionPart.cs @@ -0,0 +1,15 @@ +using OrchardCore.ContentManagement; + +namespace StatCan.OrchardCore.Radar.Models +{ + public class RadarPermissionPart : ContentPart + { + public string ContentItemId { get; set; } + public string ContentType { get; set; } + public string Owner { get; set; } + public bool Published { get; set; } + + // Only applicable to bag items + public string ParentContentItemId { get; set; } = ""; + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Recipes/radar.setup.recipe.json b/src/Apps/StatCan.OrchardCore.Radar/Recipes/radar.setup.recipe.json new file mode 100644 index 000000000..be0a88e6f --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Recipes/radar.setup.recipe.json @@ -0,0 +1,3467 @@ +{ + "name": "radar-setup", + "displayName": "Radar Setup", + "description": "Recipe used to setup the Radar application.", + "author": "Digital Innovation Team", + "website": "https://digital.statcan.gc.ca", + "version": "1.0", + "issetuprecipe": true, + "categories": ["radar"], + "tags": ["innovation", "inno"], + "steps": [ + { + "name": "recipes", + "Values": [ + { + "executionid": "ThemeSetup", + "name": "vuetify-theme-setup" + } + ] + }, + { + "name": "feature", + "disable": [], + "enable": ["StatCan.OrchardCore.Radar"] + }, + { + "name": "media", + "Files": [ + { + "SourcePath": "../wwwroot/css/images/radar-icon-white.svg", + "TargetPath": "radar-icon-white.svg" + }, + { + "SourcePath": "../wwwroot/css/images/icon_communities.svg", + "TargetPath": "icon_communities.svg" + }, + { + "SourcePath": "../wwwroot/css/images/icon_events.svg", + "TargetPath": "icon_events.svg" + }, + { + "SourcePath": "../wwwroot/css/images/icon_experiments.svg", + "TargetPath": "icon_experiments.svg" + }, + { + "SourcePath": "../wwwroot/css/images/icon_opportunities.svg", + "TargetPath": "icon_opportunities.svg" + }, + { + "SourcePath": "../wwwroot/css/images/icon_topics.svg", + "TargetPath": "icon_topics.svg" + }, + { + "SourcePath": "../wwwroot/css/images/di-logo.svg", + "TargetPath": "di-logo.svg" + } + ] + }, + { + "name": "custom-settings", + "VuetifyThemeSettings": { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "VuetifyThemeSettings", + "DisplayText": "", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "VuetifyThemeSettings": { + "DisplayName": { + "Text": "Radar" + }, + "Logo": { + "Paths": ["radar-icon-white.svg"], + "MediaTexts": [""] + }, + "DisplayMode": { + "Text": "light" + }, + "ThemeOptions": { + "Text": null + } + } + } + }, + { + "name": "content", + "data": [ + { + "ContentItemId": "4mybr8kwj6cn541v2yktz44t5c", + "ContentItemVersionId": null, + "ContentType": "Taxonomy", + "DisplayText": "Topics", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": null + }, + "AliasPart": { + "Alias": null + }, + "AutoroutePart": { + "Path": "topics", + "SetHomepage": false, + "Disabled": false, + "RouteContainedItems": true, + "Absolute": false + }, + "TaxonomyPart": { + "TermContentType": "Topic", + "Terms": [] + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "43h0218am73wn0jrm1mms51hce", + "ContentItemVersionId": null, + "ContentType": "Taxonomy", + "DisplayText": "Proposal Types", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": null + }, + "AliasPart": { + "Alias": null + }, + "AutoroutePart": { + "Path": "proposal-types", + "SetHomepage": false, + "Disabled": false, + "RouteContainedItems": true, + "Absolute": false + }, + "TaxonomyPart": { + "TermContentType": "ProposalType", + "Terms": [] + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "4s1qtbtz9m7634cqp2pg9k6gmf", + "ContentItemVersionId": null, + "ContentType": "Taxonomy", + "DisplayText": "Project Types", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": null + }, + "AliasPart": { + "Alias": null + }, + "AutoroutePart": { + "Path": "project-types", + "SetHomepage": false, + "Disabled": false, + "RouteContainedItems": true, + "Absolute": false + }, + "TaxonomyPart": { + "TermContentType": "ProjectType", + "Terms": [] + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "44b27fsfqm5cdxm47c21r4ywn9", + "ContentItemVersionId": null, + "ContentType": "Taxonomy", + "DisplayText": "Community Types", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": null + }, + "AliasPart": { + "Alias": null + }, + "AutoroutePart": { + "Path": "community-types", + "SetHomepage": false, + "Disabled": false, + "RouteContainedItems": true, + "Absolute": false + }, + "TaxonomyPart": { + "TermContentType": "CommunityType", + "Terms": [] + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPage", + "DisplayText": "Digital Radar", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPage": { + "Description": { + "Text": "Explore, manage and discover insights across your projects" + } + }, + "AutoroutePart": { + "Path": "digital-radar", + "SetHomepage": true, + "Disabled": false, + "RouteContainedItems": false, + "Absolute": false + }, + "LocalizationPart": { + "LocalizationSet": "4xwnrm19d4119rg6szffxkfpwt", + "Culture": "en" + }, + "TitlePart": { + "Title": "Digital Radar" + }, + "HeaderList": { + "ContentItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPageHeaderList", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPageHeaderList": { + "Caption": { + "Text": "Discover pathfinding projects" + } + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPageHeaderList", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPageHeaderList": { + "Caption": { + "Text": "Explore technologies and topics" + } + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPageHeaderList", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPageHeaderList": { + "Caption": { + "Text": "Share your team's work" + } + } + } + ] + }, + "Activities": { + "ContentItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "EntityCard", + "DisplayText": "Projects", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "EntityCard": { + "Type": { + "Text": "Project" + }, + "Limit": { + "Value": 5.0 + }, + "Caption": { + "Text": "In-flight projects" + }, + "Icon": { + "Text": "mdi-flask-outline" + }, + "ButtonText": { + "Text": "All Projects" + }, + "ButtonLink": { + "Text": "projects" + } + }, + "LocalizationPart": { + "LocalizationSet": "4egqzgdmmhkcqvxb5qzbah8hdp", + "Culture": "en" + }, + "TitlePart": { + "Title": "Projects" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "EntityCard", + "DisplayText": "Events", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "EntityCard": { + "Type": { + "Text": "Event" + }, + "Limit": { + "Value": 5.0 + }, + "Caption": { + "Text": "Upcoming events" + }, + "Icon": { + "Text": "mdi-calendar" + }, + "ButtonText": { + "Text": "All Events" + }, + "ButtonLink": { + "Text": "events" + } + }, + "LocalizationPart": { + "LocalizationSet": "4g33gt5aan2pdxyad05r5ergcn", + "Culture": "en" + }, + "TitlePart": { + "Title": "Events" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "EntityCard", + "DisplayText": "Committees", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "EntityCard": { + "Type": { + "Text": "Community" + }, + "Limit": { + "Value": 5.0 + }, + "Caption": { + "Text": "Latest committees" + }, + "Icon": { + "Text": "mdi-account-multiple" + }, + "ButtonText": { + "Text": "All Communities" + }, + "ButtonLink": { + "Text": "communities" + } + }, + "LocalizationPart": { + "LocalizationSet": "4hj956jn7axdywb3vr7fp9pcdc", + "Culture": "en" + }, + "TitlePart": { + "Title": "Committees" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "EntityCard", + "DisplayText": "Proposals", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "EntityCard": { + "Type": { + "Text": "Proposal" + }, + "Limit": { + "Value": 5.0 + }, + "Caption": { + "Text": "Current proposals" + }, + "Icon": { + "Text": "mdi-alert-circle-outline" + }, + "ButtonText": { + "Text": "All Proposals" + }, + "ButtonLink": { + "Text": "proposals" + } + }, + "LocalizationPart": { + "LocalizationSet": "444sm3q8x1m6mv4cnk8b23vacc", + "Culture": "en" + }, + "TitlePart": { + "Title": "Proposals" + } + } + ] + }, + "Trends": { + "ContentItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "TrendingCard", + "DisplayText": "Topics", + "Latest": false, + "Published": false, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TrendingCard": { + "Caption": { + "Text": "Most used topics across all activities" + }, + "Type": { + "Text": "Topics" + }, + "Icon": { + "Text": "mdi-pound" + }, + "ButtonText": { + "Text": "All Topics" + } + }, + "LocalizationPart": { + "LocalizationSet": "4nmfph1ps5dd80b7n9et6dyvxn", + "Culture": "en" + }, + "TitlePart": { + "Title": "Topics" + } + } + ] + }, + "Footer": { + "ContentItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPageFooterCard", + "DisplayText": "User guide", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-09-24T18:52:05.0220266Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPageFooterCard": { + "Icon": { + "Text": "mdi-book-open-variant" + }, + "Link": { + "Text": "#" + }, + "Caption": { + "Text": "Learn more about the Radar" + } + }, + "TitlePart": { + "Title": "User guide" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LandingPageFooterCard", + "DisplayText": "Contact us", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-09-24T18:52:05.0220266Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LandingPageFooterCard": { + "Icon": { + "Text": "mdi-email" + }, + "Link": { + "Text": "#" + }, + "Caption": { + "Text": "Ask questions and send feedback" + } + }, + "TitlePart": { + "Title": "Contact us" + } + } + ] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "Menu", + "DisplayText": "Main Menu", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Main Menu" + }, + "AliasPart": { + "Alias": "main-menu-en" + }, + "MenuPart": {}, + "MenuItemsListPart": { + "MenuItems": [ + { + "ContentItemId": "49gq0283mmxpf61ttqjzahsync", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Register", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:05:42.5116274Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Register", + "Url": null + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": null + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "MenuItemsListPart": { + "MenuItems": [ + { + "ContentItemId": "4vnwcz06x74wsxfa0tgs4rzrm8", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Projects", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:06:36.1670259Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Projects", + "Url": "~/projects/create" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-flask-outline" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "4z3hw920rrgc415c1bz3t79smh", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Proposals", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:08:24.5654821Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Proposals", + "Url": "~/proposals/create" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-alert-circle-outline" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "4hmjn2fy1r107tftspm4vpeqw0", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Events", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:08:43.8403153Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Events", + "Url": "~/events/create" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-calendar" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "4h0yqqe4jbn010xdjakffmcdnm", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Communities", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:09:11.8078551Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Communities", + "Url": "~/communities/create" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-account-multiple" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "4adxycardc78229ahq6s118rwe", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Topics", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T16:09:39.6793362Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "LinkMenuItemPart": { + "Name": "Topics", + "Url": "~/topics/create" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-pound" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + } + ] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Activities", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Activities", + "Url": null + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": null + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "MenuItemsListPart": { + "MenuItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Projects", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Projects", + "Url": "~/projects" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-flask-outline" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Proposals", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + + "LinkMenuItemPart": { + "Name": "Proposals", + "Url": "~/proposals" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-alert-circle-outline" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Events", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Events", + "Url": "~/events" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-calendar" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Communities", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Communities", + "Url": "~/communities" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-account-multiple" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + } + ] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Data", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Data", + "Url": null + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": null + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "MenuItemsListPart": { + "MenuItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Topics", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Topics", + "Url": "~/topics" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-pound" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + } + ] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Support", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Support", + "Url": null + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": null + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "MenuItemsListPart": { + "MenuItems": [ + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Documentation", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Documentation", + "Url": "#" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-help-circle-outline" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Contact us", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Contact us", + "Url": "#" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-email" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "LinkMenuItem", + "DisplayText": "Community", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "LinkMenuItemPart": { + "Name": "Community", + "Url": "#" + }, + "LinkMenuItem": {}, + "CommonMenuItemPart": { + "IconName": { + "Text": "mdi-forum" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + } + } + ] + } + } + ] + }, + "@WeldedPartSettings": { + "MenuPart": { + "Position": "3" + }, + "MenuItemsListPart": { + "Position": "4" + } + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "AppBar", + "DisplayText": "Radar", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "AppBar": { + "Image": { + "Paths": ["radar-icon-white.svg"], + "MediaTexts": [""] + }, + "Height": { + "Value": null + }, + "Width": { + "Value": null + }, + "Elevation": { + "Value": 10.0 + }, + "ExtensionHeight": { + "Value": 48.0 + }, + "ScrollThreshold": { + "Value": null + }, + "Logo": { + "Paths": ["radar-icon-white.svg"], + "MediaTexts": [""] + } + }, + "TitlePart": { + "Title": "Radar" + }, + "LayerMetadata": { + "RenderTitle": false, + "Position": 2.0, + "Zone": "Header", + "Layer": "All" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "NavigationDrawer", + "DisplayText": null, + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "NavigationDrawer": { + "Color": { + "Text": "#FFFFFF" + }, + "MobileBreakpoint": { + "Value": null + }, + "OverlayColor": { + "Text": null + }, + "OverlayOpacity": { + "Value": null + }, + "Width": { + "Text": null, + "Value": null + }, + "Height": { + "Value": null + }, + "MiniVariantWidth": { + "Value": null + } + }, + "LayerMetadata": { + "RenderTitle": false, + "Position": 1.0, + "Zone": "NavigationDrawer", + "Layer": "All" + } + }, + { + "ContentItemId": "[js: uuid()]", + "ContentItemVersionId": null, + "ContentType": "Footer", + "DisplayText": null, + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Footer": { + "Version": { + "Text": "1.0.0" + }, + "Caption": { + "Text": "digital@statcan" + }, + "Logo": { + "Paths": ["di-logo.svg"], + "MediaTexts": [""] + } + }, + "LayerMetadata": { + "RenderTitle": false, + "Position": 1.0, + "Zone": "Footer", + "Layer": "LandingPage" + } + }, + { + "ContentItemId": "4w1g1ehv6fbvtsfhtdtqmqj7wv", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Topic Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Topic Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "4e4vrxqn8zphz70091ckbd45x0", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-15T15:14:07.4591744Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["4g0wbrpyppkqd3z1kjbv5jbdnj"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "4k1ex2mpprg6csqsmz2f0dm3cr", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Event Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "Id": null, + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Event Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "443w0p6kp3zvmw0cymq315xa4e", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-19T13:17:37.1031947Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["41ewd04a6rabq7f16d69zsfsnv"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "4m3x6bk94sjgerz76ve9fzc3dm", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Artifact Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "Id": null, + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Artifact Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "4agsw4eq25ccavq98qbym6sx5w", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": null, + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-19T16:42:17.8329591Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": null, + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["4kv0zcjsw40a61nbhasd4tkkc8"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "48g1mk0np1jes448xwzrcwb54t", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Community Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "Id": null, + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Community Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "41rs3p7cs7ge7x1bs5xre1w04r", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": "", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-23T17:12:50.3687249Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "", + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["4db4f2rx87mdbtts4414ybcn4m"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "4m040838spwxq376q0sb7crm49", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Proposal Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "Id": null, + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Proposal Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "453284axrmvmwsvgd6vdapcxxy", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": "", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-23T18:23:31.4670746Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "", + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["4qmtfht0t4v7fsf0pkbsc0bzda"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "4ebx0pggyy1h2x5f19gfcnx1nc", + "ContentItemVersionId": null, + "ContentType": "Form", + "DisplayText": "Project Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "Form": {}, + "RadarFormPart": { + "Id": null, + "InitialValues": null, + "Options": null + }, + "TitlePart": { + "Title": "Project Form" + }, + "FlowPart": { + "Widgets": [ + { + "ContentItemId": "4ww0ry5c18z505w6tsh662x1nz", + "ContentItemVersionId": null, + "ContentType": "VueFormReference", + "DisplayText": "", + "Latest": false, + "Published": false, + "ModifiedUtc": "2021-11-24T13:03:32.9385778Z", + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "", + "Author": "admin", + "VueFormReference": { + "FormReference": { + "ContentItemIds": ["4wrjjj5400ecftxxv82b2vqcxe"] + } + }, + "FlowMetadata": { + "Alignment": 3, + "Size": 100 + } + } + ] + } + }, + { + "ContentItemId": "4qmtfht0t4v7fsf0pkbsc0bzda", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Proposal Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Proposal Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"proposalTypeLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"topicsLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"communitiesLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"visibilityLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n {{\r\n \"submitLabel\" | localize }}\r\n \r\n\r\n\r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "proposal-form" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions,\r\n valueNames: [\"user\", \"role\"]\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\nvar contentFormModel = convertToFormModel(\"Proposal\", data);\r\n\r\nif(!validateRadarEntityName(\"Proposal\", contentFormModel.Name, contentItem))\r\n{\r\n addError('serverValidationMessage', localizedText.nameNonUniqueError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Name, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.nameLengthError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Description, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionLengthError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name) || !validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.emptyTextError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name))\r\n{\r\n addError('serverValidationMessage', localizedText.nameEmptyError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionEmptyError);\r\n}\r\n\r\nif(!validateTopics(contentFormModel.Topics))\r\n{\r\n addError('serverValidationMessage', localizedText.topicError);\r\n}\r\n\r\nif(!validateRelatedEntities(contentFormModel.RelatedEntities, \"Community\"))\r\n{\r\n addError('serverValidationMessage', localizedText.communityError);\r\n}\r\n\r\nif(!validateEntityType(\"Proposal Types\", contentFormModel.Type))\r\n{\r\n addError('serverValidateMessage', localizedText.typeError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar contentFormModel = convertToFormModel(\"Proposal\", data);\r\nvar contentDocument = convertToContentDocument(\"Proposal\", contentFormModel);\r\n\r\n// If existing content can't be found then default to create\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\n\r\nif (contentItem == null) {\r\n contentItem = createLocalizedContentItem(\"Proposal\", contentDocument);\r\n} else {\r\n updateLocalizedContentItem(contentItem, contentDocument);\r\n}\r\n\r\nhttpRedirect(`~/proposals/${contentItem.ContentItemId}`);" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "descriptionLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Description" + }, + { + "Culture": "en", + "Value": "Description" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "topicsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sujets" + }, + { + "Culture": "en", + "Value": "Topics" + } + ] + }, + { + "Name": "proposalTypeLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Type de projet" + }, + { + "Culture": "en", + "Value": "Proposal Type" + } + ] + }, + { + "Name": "visibilityLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Visibilité" + }, + { + "Culture": "en", + "Value": "Visibility" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "nameNonUniqueError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit être unique" + }, + { + "Culture": "en", + "Value": "Name must be unique" + } + ] + }, + { + "Name": "nameLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit comporter moins de 100 caractères" + }, + { + "Culture": "en", + "Value": "Name must be less than 100 characters" + } + ] + }, + { + "Name": "descriptionLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description doit comporter moins de 512 caractères" + }, + { + "Culture": "en", + "Value": "Description must be less than 512 characters" + } + ] + }, + { + "Name": "nameEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Name cannot be empty" + } + ] + }, + { + "Name": "descriptionEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Description cannot be empty" + } + ] + }, + { + "Name": "topicError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des sujets en double ou certains sujets n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated topics or some topics does not exist" + } + ] + }, + { + "Name": "typeError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le type sélectionné n'existe pas" + }, + { + "Culture": "en", + "Value": "The selected type does not exist" + } + ] + }, + { + "Name": "emptyTextError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom et la description ne peuvent pas être vides" + }, + { + "Culture": "en", + "Value": "Name and Description cannot be empty" + } + ] + }, + { + "Name": "communitiesLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Communautés" + }, + { + "Culture": "en", + "Value": "Communities" + } + ] + }, + { + "Name": "communityError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des communautés dupliquées ou certaines communautés n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated communities or some communities does not exist" + } + ] + }, + { + "Name": "addButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Ajouter" + }, + { + "Culture": "en", + "Value": "Add" + } + ] + }, + { + "Name": "placeholderLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sélectionnez l'option" + }, + { + "Culture": "en", + "Value": "Select option" + } + ] + } + ] + } + }, + { + "ContentItemId": "4db4f2rx87mdbtts4414ybcn4m", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Community Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Community Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"communityTypeLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"topicsLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"eventsLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"visibilityLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n\r\n \r\n \r\n \r\n\r\n \r\n\r\n \r\n \r\n \r\n
\r\n\r\n\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n {{\r\n \"submitLabel\" | localize }}\r\n \r\n\r\n\r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "community-form" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions,\r\n valueNames: [\"user\", \"role\"]\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\nvar contentFormModel = convertToFormModel(\"Community\", data);\r\n\r\nif(!validateRadarEntityName(\"Community\", contentFormModel.Name, contentItem))\r\n{\r\n addError('serverValidationMessage', localizedText.nameNonUniqueError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Name, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.nameLengthError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Description, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionLengthError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name) || !validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.emptyTextError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name))\r\n{\r\n addError('serverValidationMessage', localizedText.nameEmptyError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionEmptyError);\r\n}\r\n\r\nif(!validateTopics(contentFormModel.Topics))\r\n{\r\n addError('serverValidationMessage', localizedText.topicError);\r\n}\r\n\r\nif(!validateEntityType(\"Community Types\", contentFormModel.Type))\r\n{\r\n addError('serverValidationMessage', localizedText.typeError);\r\n}\r\n\r\nif(!validateMemberWithRole(contentFormModel.CommunityMembers))\r\n{\r\n addError('serverValidationMessage', localizedText.memberError);\r\n}\r\n\r\nif(!validateRelatedEntities(contentFormModel.RelatedEntities, \"Event\"))\r\n{\r\n addError('serverValidationMessage', localizedText.eventError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar contentFormModel = convertToFormModel(\"Community\", data);\r\nvar contentDocument = convertToContentDocument(\"Community\", contentFormModel);\r\n\r\n// If existing content can't be found then default to create\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\n\r\nif (contentItem == null) {\r\n contentItem = createLocalizedContentItem(\"Community\", contentDocument);\r\n} else {\r\n updateLocalizedContentItem(contentItem, contentDocument);\r\n}\r\n\r\nhttpRedirect(`~/communities/${contentItem.ContentItemId}`);" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "descriptionLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Description" + }, + { + "Culture": "en", + "Value": "Description" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "topicsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sujets" + }, + { + "Culture": "en", + "Value": "Topics" + } + ] + }, + { + "Name": "communityTypeLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Type de projet" + }, + { + "Culture": "en", + "Value": "Community Type" + } + ] + }, + { + "Name": "visibilityLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Visibilité" + }, + { + "Culture": "en", + "Value": "Visibility" + } + ] + }, + { + "Name": "communityMembersLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Membres du projet" + }, + { + "Culture": "en", + "Value": "Community Members" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "nameNonUniqueError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit être unique" + }, + { + "Culture": "en", + "Value": "Name must be unique" + } + ] + }, + { + "Name": "nameLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit comporter moins de 100 caractères" + }, + { + "Culture": "en", + "Value": "Name must be less than 100 characters" + } + ] + }, + { + "Name": "descriptionLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description doit comporter moins de 512 caractères" + }, + { + "Culture": "en", + "Value": "Description must be less than 512 characters" + } + ] + }, + { + "Name": "nameEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Name cannot be empty" + } + ] + }, + { + "Name": "descriptionEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Description cannot be empty" + } + ] + }, + { + "Name": "topicError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des sujets en double ou certains sujets n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated topics or some topics does not exist" + } + ] + }, + { + "Name": "typeError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le type sélectionné n'existe pas" + }, + { + "Culture": "en", + "Value": "The selected type does not exist" + } + ] + }, + { + "Name": "emptyTextError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom et la description ne peuvent pas être vides" + }, + { + "Culture": "en", + "Value": "Name and Description cannot be empty" + } + ] + }, + { + "Name": "memberError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des membres en double ou certains membres n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated members or some member does not exist" + } + ] + }, + { + "Name": "eventsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Événements" + }, + { + "Culture": "en", + "Value": "Events" + } + ] + }, + { + "Name": "eventError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des événements dupliqués et certains événements n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated events are some events does not exist" + } + ] + }, + { + "Name": "addButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Ajouter" + }, + { + "Culture": "en", + "Value": "Add" + } + ] + }, + { + "Name": "placeholderLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sélectionnez l'option" + }, + { + "Culture": "en", + "Value": "Select option" + } + ] + }, + { + "Name": "memberLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Membre" + }, + { + "Culture": "en", + "Value": "Member" + } + ] + }, + { + "Name": "roleLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Rôle" + }, + { + "Culture": "en", + "Value": "Role" + } + ] + } + ] + } + }, + { + "ContentItemId": "4wrjjj5400ecftxxv82b2vqcxe", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Project Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Project Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"projectTypeLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"topicsLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"visibilityLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n\r\n \r\n \r\n \r\n\r\n \r\n\r\n \r\n \r\n \r\n
\r\n\r\n\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n {{\r\n \"submitLabel\" | localize }}\r\n \r\n\r\n\r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "project-0" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions,\r\n valueNames: [\"user\", \"role\"]\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\nvar contentFormModel = convertToFormModel(\"Project\", data);\r\n\r\nif(!validateRadarEntityName(\"Project\", contentFormModel.Name, contentItem))\r\n{\r\n addError('serverValidationMessage', localizedText.nameNonUniqueError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Name, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.nameLengthError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Description, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionLengthError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name) || !validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.emptyTextError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name))\r\n{\r\n addError('serverValidationMessage', localizedText.nameEmptyError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionEmptyError);\r\n}\r\n\r\nif(!validateTopics(contentFormModel.Topics))\r\n{\r\n addError('serverValidationMessage', localizedText.topicError);\r\n}\r\n\r\nif(!validateEntityType(\"Project Types\", contentFormModel.Type))\r\n{\r\n addError('serverValidationMessage', localizedText.typeError);\r\n}\r\n\r\nif(!validateMemberWithRole(contentFormModel.ProjectMembers))\r\n{\r\n addError('serverValidationMessage', localizedText.memberError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar contentFormModel = convertToFormModel(\"Project\", data);\r\nvar contentDocument = convertToContentDocument(\"Project\", contentFormModel);\r\n\r\n// If existing content can't be found then default to create\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\n\r\nif (contentItem == null) {\r\n contentItem = createLocalizedContentItem(\"Project\", contentDocument);\r\n} else {\r\n updateLocalizedContentItem(contentItem, contentDocument);\r\n}\r\n\r\nhttpRedirect(`~/projects/${contentItem.ContentItemId}`);" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "descriptionLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Description" + }, + { + "Culture": "en", + "Value": "Description" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "topicsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sujets" + }, + { + "Culture": "en", + "Value": "Topics" + } + ] + }, + { + "Name": "projectTypeLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Type de projet" + }, + { + "Culture": "en", + "Value": "Project Type" + } + ] + }, + { + "Name": "visibilityLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Visibilité" + }, + { + "Culture": "en", + "Value": "Visibility" + } + ] + }, + { + "Name": "projectMembersLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Membres du projet" + }, + { + "Culture": "en", + "Value": "Project Members" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "nameNonUniqueError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit être unique" + }, + { + "Culture": "en", + "Value": "Name must be unique" + } + ] + }, + { + "Name": "nameLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit comporter moins de 100 caractères" + }, + { + "Culture": "en", + "Value": "Name must be less than 100 characters" + } + ] + }, + { + "Name": "descriptionLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description doit comporter moins de 512 caractères" + }, + { + "Culture": "en", + "Value": "Description must be less than 512 characters" + } + ] + }, + { + "Name": "nameEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Name cannot be empty" + } + ] + }, + { + "Name": "descriptionEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Description cannot be empty" + } + ] + }, + { + "Name": "topicError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des sujets en double ou certains sujets n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated topics or some topics does not exist" + } + ] + }, + { + "Name": "typeError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le type sélectionné n'existe pas" + }, + { + "Culture": "en", + "Value": "The selected type does not exist" + } + ] + }, + { + "Name": "emptyTextError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom et la description ne peuvent pas être vides" + }, + { + "Culture": "en", + "Value": "Name and Description cannot be empty" + } + ] + }, + { + "Name": "memberError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des membres en double ou certains membres n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated members or some member does not exist" + } + ] + }, + { + "Name": "addButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Ajouter" + }, + { + "Culture": "en", + "Value": "Add" + } + ] + }, + { + "Name": "memberLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Membre" + }, + { + "Culture": "en", + "Value": "Member" + } + ] + }, + { + "Name": "roleLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Rôle" + }, + { + "Culture": "en", + "Value": "Role" + } + ] + } + ] + } + }, + { + "ContentItemId": "41ewd04a6rabq7f16d69zsfsnv", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Event Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Event Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"topicsLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n\r\n \r\n

{{ \"organizersLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n\r\n \r\n

{{ \"attendeesLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ \"cancelButtonLabel\" | localize }}\r\n \r\n \r\n {{ \"okButtonLabel\" | localize }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ \"cancelButtonLabel\" | localize }}\r\n \r\n \r\n {{ \"okButtonLabel\" | localize }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ \"cancelButtonLabel\" | localize }}\r\n \r\n \r\n {{ \"okButtonLabel\" | localize }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ \"cancelButtonLabel\" | localize }}\r\n \r\n \r\n {{ \"okButtonLabel\" | localize }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n\r\n \r\n

{{ \"visibilityLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n \t{{ \"submitLabel\" | localize }}\r\n \r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "event-form" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions,\r\n menu1: null,\r\n menu2: null,\r\n menu3: null,\r\n menu4: null,\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\nvar contentFormModel = convertToFormModel(\"Event\", data);\r\n\r\nif(!validateRadarEntityName(\"Event\", contentFormModel.Name, contentItem))\r\n{\r\n addError('serverValidationMessage', localizedText.nameNonUniqueError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Name, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.nameLengthError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Description, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionLengthError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name) || !validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.emptyTextError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name))\r\n{\r\n addError('serverValidationMessage', localizedText.nameEmptyError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionEmptyError);\r\n}\r\n\r\nif(!validateTopics(contentFormModel.Topics))\r\n{\r\n addError('serverValidationMessage', localizedText.topicError);\r\n}\r\n\r\nif(!validateEventDateTime(contentFormModel.StartDate, contentFormModel.startTime, contentFormModel.EndDate, contentFormModel.EndTime))\r\n{\r\n addError('serverValidationMessage', localizedText.dateTimeError);\r\n}\r\n\r\nif(!validateMemberWithoutRole(contentFormModel.Attendees))\r\n{\r\n addError('serverValidationMessage', localizedText.attendeesError);\r\n}\r\n\r\nif(!validateMemberWithoutRole(contentFormModel.EventOrganizers))\r\n{\r\n addError('serverValidationMessage', localizedText.organizerError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar contentFormModel = convertToFormModel(\"Event\", data);\r\nvar contentDocument = convertToContentDocument(\"Event\", contentFormModel);\r\n\r\n// If existing content can't be found then default to create\r\nvar contentItem = getLocalizedContentItemById(data.id);\r\n\r\nif (contentItem == null) {\r\n contentItem = createLocalizedContentItem(\"Event\", contentDocument);\r\n} else {\r\n updateLocalizedContentItem(contentItem, contentDocument);\r\n}\r\n\r\nhttpRedirect(`~/events/${contentItem.ContentItemId}`);" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "descriptionLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Description" + }, + { + "Culture": "en", + "Value": "Description" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "topicsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sujets" + }, + { + "Culture": "en", + "Value": "Topics" + } + ] + }, + { + "Name": "visibilityLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Visibilité" + }, + { + "Culture": "en", + "Value": "Visibility" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "dateTimeError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La date de début ne peut pas être postérieure à la date de fin" + }, + { + "Culture": "en", + "Value": "Start date cannot be after end date" + } + ] + }, + { + "Name": "nameLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit comporter moins de 100 caractères" + }, + { + "Culture": "en", + "Value": "Name must be less than 100 characters" + } + ] + }, + { + "Name": "descriptionLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description doit comporter moins de 512 caractères" + }, + { + "Culture": "en", + "Value": "Description must be less than 512 characters" + } + ] + }, + { + "Name": "nameEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Name cannot be empty" + } + ] + }, + { + "Name": "descriptionEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "La description ne peut pas être vide" + }, + { + "Culture": "en", + "Value": "Description cannot be empty" + } + ] + }, + { + "Name": "topicError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des sujets en double ou certains sujets n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated topics or some topics does not exist" + } + ] + }, + { + "Name": "emptyTextError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom et la description ne peuvent pas être vides" + }, + { + "Culture": "en", + "Value": "Name and Description cannot be empty" + } + ] + }, + { + "Name": "addButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Ajouter" + }, + { + "Culture": "en", + "Value": "Add" + } + ] + }, + { + "Name": "placeholderLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sélectionnez l'option" + }, + { + "Culture": "en", + "Value": "Select option" + } + ] + }, + { + "Name": "organizersLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Organisateurs d'événements" + }, + { + "Culture": "en", + "Value": "Event Organizers" + } + ] + }, + { + "Name": "attendeesLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Participants/es" + }, + { + "Culture": "en", + "Value": "Attendees" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "startDateLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Date de début" + }, + { + "Culture": "en", + "Value": "Start Date" + } + ] + }, + { + "Name": "startTimeLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Heure de début" + }, + { + "Culture": "en", + "Value": "Start Time" + } + ] + }, + { + "Name": "endDateLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Date de fin" + }, + { + "Culture": "en", + "Value": "End Date" + } + ] + }, + { + "Name": "endTimeLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Heure de fin" + }, + { + "Culture": "en", + "Value": "End Time" + } + ] + }, + { + "Name": "attendeesError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des participants en double ou certains participants n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated attendees or some attendees does not exist" + } + ] + }, + { + "Name": "organizerError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Il y a des organisateurs en double ou certains organisateurs n'existent pas" + }, + { + "Culture": "en", + "Value": "There are duplicated organizers or some organizers does not exist" + } + ] + }, + { + "Name": "nameNonUniqueError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Le nom doit être unique" + }, + { + "Culture": "en", + "Value": "Name must be unique" + } + ] + }, + { + "Name": "okButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "OK" + }, + { + "Culture": "en", + "Value": "OK" + } + ] + }, + { + "Name": "cancelButtonLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Annuler" + }, + { + "Culture": "en", + "Value": "Cancel" + } + ] + } + ] + } + }, + { + "ContentItemId": "4g0wbrpyppkqd3z1kjbv5jbdnj", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Topic Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Topic Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n {{\r\n \"submitLabel\" | localize }}\r\n \r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "topic-form" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar topic = getTaxonomyByTypeAndId(\"Topics\", data.id);\r\nvar contentFormModel = convertToFormModel(\"Topic\", data);\r\n\r\nif(!validateTaxonomyName(\"Topics\", contentFormModel.Name, topic))\r\n{\r\n addError('serverValidationMessage', localizedText.nameNonUniqueError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Name, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.nameLengthError);\r\n}\r\n\r\nif(!validateMaxStringLength(contentFormModel.Description, 100))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionLengthError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name) || !validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.emptyTextError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Name))\r\n{\r\n addError('serverValidationMessage', localizedText.nameEmptyError);\r\n}\r\n\r\nif(!validateString(contentFormModel.Description))\r\n{\r\n addError('serverValidationMessage', localizedText.descriptionEmptyError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar id = createOrUpdateTopic(data.id, data);\r\n\r\nif(id !== null)\r\n{\r\n httpRedirect(`~/topics/${id}`);\r\n}" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "descriptionLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Description" + }, + { + "Culture": "en", + "Value": "Description" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "nameNonUniqueError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Name must be unique" + } + ] + }, + { + "Name": "nameLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "nameLengthError" + } + ] + }, + { + "Name": "descriptionLengthError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Description must be less than 512 characters" + } + ] + }, + { + "Name": "nameEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Name cannot be empty" + } + ] + }, + { + "Name": "descriptionEmptyError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Description cannot be empty" + } + ] + }, + { + "Name": "placeholderLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sélectionnez l'option" + }, + { + "Culture": "en", + "Value": "Select option" + } + ] + } + ] + } + }, + { + "ContentItemId": "4kv0zcjsw40a61nbhasd4tkkc8", + "ContentItemVersionId": null, + "ContentType": "VueForm", + "DisplayText": "Artifact Form", + "Latest": true, + "Published": true, + "ModifiedUtc": null, + "PublishedUtc": null, + "CreatedUtc": null, + "Owner": "[js: parameters('AdminUserId')]", + "Author": "[js: parameters('AdminUsername')]", + "TitlePart": { + "Title": "Artifact Form" + }, + "VueForm": { + "Template": { + "Text": "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

{{ \"visibilityLabel\" | localize }}

\r\n \r\n \r\n \r\n
\r\n \r\n

{{ \"permissionsLabel\" | localize }}

\r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n {% raw %}{{ message }}{% endraw %}\r\n \r\n \r\n \r\n {{\r\n \"submitLabel\" | localize }}\r\n \r\n
\r\n
\r\n
" + }, + "RenderAs": { + "Text": null + }, + "Disabled": { + "Value": false + }, + "Debug": { + "Value": false + }, + "DisabledHtml": { + "Html": "" + }, + "SuccessMessage": { + "Text": null + } + }, + "AliasPart": { + "Alias": "artifact-form" + }, + "VueFormScripts": { + "ClientInit": { + "Text": null + }, + "ComponentOptions": { + "Text": "{\r\n data: () => ({\r\n ...window.formValues,\r\n ...window.formOptions\r\n })\r\n}" + }, + "OnValidation": { + "Text": "var data = requestFormAsJsonObject();\r\nvar localizedText = getLocalizedTextValues(getFormContentItem());\r\n\r\nvar contentFormModel = convertToFormModel(\"Artifact\", data);\r\n\r\nvar parentContentItem = getLocalizedContentItemById(data.parentId);\r\n\r\nif(parentContentItem == null)\r\n{\r\n addError('serverValidationMessage', localizedText.noParentError);\r\n}\r\n\r\nif(!validateUrl(contentFormModel.Url, \"www.github.com\"))\r\n{\r\n addError('serverValidationMessage', localizedText.urlError);\r\n}" + }, + "OnSubmitted": { + "Text": "var data = requestFormAsJsonObject();\r\n\r\nvar contentFormModel = convertToFormModel(\"Artifact\", data);\r\n\r\nvar id = createOrUpdateArtifact(data.parentId, data.id, contentFormModel);\r\n\r\nif(id !== null)\r\n{\r\n var parentContentItem = getLocalizedContentItemById(data.parentId);\r\n\r\n if(parentContentItem != null)\r\n {\r\n if(parentContentItem.ContentType == \"Project\")\r\n {\r\n httpRedirect(`~/projects/${parentContentItem.ContentItemId}/artifacts/${id}`);\r\n } else if(parentContentItem.ContentType == \"Event\")\r\n {\r\n httpRedirect(`~/events/${parentContentItem.ContentItemId}/artifacts/${id}`);\r\n } else if(parentContentItem.ContentType == \"Proposal\")\r\n {\r\n httpRedirect(`~/prosals/${parentContentItem.ContentItemId}/artifacts/${id}`);\r\n }\r\n }\r\n}" + } + }, + "ContentPermissionsPart": { + "Enabled": false, + "Roles": [] + }, + "LocalizedTextPart": { + "Data": [ + { + "Name": "nameLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Nom" + }, + { + "Culture": "en", + "Value": "Name" + } + ] + }, + { + "Name": "urlLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Url" + }, + { + "Culture": "en", + "Value": "Url" + } + ] + }, + { + "Name": "permissionsLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Autorisations" + }, + { + "Culture": "en", + "Value": "Permissions" + } + ] + }, + { + "Name": "submitLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Soumettre" + }, + { + "Culture": "en", + "Value": "Submit" + } + ] + }, + { + "Name": "serverValidationError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Une erreure est survenue." + }, + { + "Culture": "en", + "Value": "An error occured." + } + ] + }, + { + "Name": "noParentError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Cannot create artifact on unknown entity" + } + ] + }, + { + "Name": "urlError", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "" + }, + { + "Culture": "en", + "Value": "Malformed url" + } + ] + }, + { + "Name": "placeholderLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Sélectionnez l'option" + }, + { + "Culture": "en", + "Value": "Select option" + } + ] + }, + { + "Name": "visibilityLabel", + "LocalizedItems": [ + { + "Culture": "fr", + "Value": "Visibilité" + }, + { + "Culture": "en", + "Value": "Visibility" + } + ] + } + ] + } + } + ] + }, + { + "name": "lucene-index", + "Indices": [ + { + "search": { + "AnalyzerName": "standardanalyzer", + "IndexLatest": true, + "IndexedContentTypes": [ + "Community", + "Event", + "Project", + "Proposal" + ], + "Culture": "any" + } + } + ] + }, + { + "name": "Queries", + "Queries": [ + { + "Template": "{% assign roleText = \"%\" | append: role | append: \"%\" %}\r\n\r\nselect DocumentId\r\n from ContentPermissionsPartIndex \r\n where ContentType = @contentType and Published = true and (not Enabled or Roles like '{{ roleText }}')\r\n\torder by PublishedUtc desc", + "ReturnDocuments": true, + "Name": "RecentActivitiesSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select ContentType\r\n\tfrom TaxonomyIndex \r\n\twhere ContentField = @type and TermContentItemId = @id", + "ReturnDocuments": false, + "Name": "TaxonomyEntityCountSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select DocumentId from ContentItemIndex where DisplayText = @type and Latest = true;", + "ReturnDocuments": true, + "Name": "AllTaxonomiesSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select TermContentItemId, count(TermContentItemId) as popularity \r\n \tfrom TaxonomyIndex \r\n \twhere ContentField = @type \r\n \tgroup by TermContentItemId \r\n \torder by popularity desc", + "ReturnDocuments": false, + "Name": "TaxonomyTrendingUpSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select TermContentItemId, count(TermContentItemId) as popularity \r\n \tfrom TaxonomyIndex \r\n \twhere ContentField = @type \r\n \tgroup by TermContentItemId \r\n \torder by popularity asc", + "ReturnDocuments": false, + "Name": "TaxonomyTrendingDownSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select DocumentId\r\n\tfrom TaxonomyIndex \r\n\twhere ContentField = @type and TermContentItemId = @id", + "ReturnDocuments": true, + "Name": "TaxonomyEntitiesSQL", + "Source": "Sql", + "Schema": null + }, + { + "Index": "search", + "Template": "{\r\n \"query\": {\r\n \"bool\": {\r\n \"must\": [\r\n {\r\n \"wildcard\": {\r\n \"Content.ContentItem.FullText\": \"{{ Term }}\"\r\n }\r\n },\r\n {\r\n \"wildcard\": {\r\n \"Content.ContentItem.ContentType\": \"{{ Type }}\"\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n}", + "ReturnContentItems": true, + "Name": "EntityListLucene", + "Source": "Lucene", + "Schema": null + }, + { + "Template": "select ContentItemId from RadarFormPartIndex where DisplayText = @type;", + "ReturnDocuments": false, + "Name": "FormQuerySQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select * from UserIndex;", + "ReturnDocuments": false, + "Name": "AllUsersSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select DocumentId from ContentItemIndex where ContentType = @type and DisplayText = @name and Published = true;", + "ReturnDocuments": true, + "Name": "EntityByTypeAndNameSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select UserId from UserIndex where UserId = @userId;", + "ReturnDocuments": false, + "Name": "UserByIdSQL", + "Source": "Sql", + "Schema": null + }, + { + "Template": "select ContentItemId from LocalizedContentItemIndex where LocalizationSet = @localizationSet;", + "ReturnDocuments": false, + "Name": "EntityByLocalizationSetSQL", + "Source": "Sql", + "Schema": null + } + ] + }, + { + "name": "Layers", + "Layers": [ + { + "Name": "All", + "Rule": null, + "Description": null, + "LayerRule": { + "Conditions": [ + { + "$type": "OrchardCore.Rules.Models.BooleanCondition, OrchardCore.Rules", + "Value": true, + "Name": "BooleanCondition", + "ConditionId": "4wj6wnc9v6gjesch0s05qkngmb" + } + ], + "Name": null, + "ConditionId": "4vx5exkhq8nmfz701dt2qh5vn6" + } + }, + { + "Name": "LandingPage", + "Rule": null, + "Description": null, + "LayerRule": { + "Conditions": [ + { + "$type": "OrchardCore.Rules.Models.HomepageCondition, OrchardCore.Rules", + "Value": true, + "Name": "HomepageCondition", + "ConditionId": "47k3j9d6mw9y37v4d1pbx6hrws" + } + ], + "Name": null, + "ConditionId": "4tw7nhdcfck9drfdber9f3p54s" + } + } + ] + } + ] +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/ResourceManagementOptionsConfiguration.cs b/src/Apps/StatCan.OrchardCore.Radar/ResourceManagementOptionsConfiguration.cs new file mode 100644 index 000000000..d6d5ef520 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/ResourceManagementOptionsConfiguration.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Options; +using OrchardCore.ResourceManagement; + +namespace StatCan.OrchardCore.Radar +{ + public class ResourceManagementOptionsConfiguration : IConfigureOptions + { + private static ResourceManifest _manifest; + + static ResourceManagementOptionsConfiguration() + { + _manifest = new ResourceManifest(); + + _manifest + .DefineStyle("Radar-styles") + .SetUrl("~/StatCan.OrchardCore.Radar/css/radar.min.css", "~/StatCan.OrchardCore.Radar/css/radar.css") + .SetVersion("1.0.0"); + + _manifest + .DefineScript("Radar-vue-components") + .SetDependencies("vuetify-theme") + .SetUrl("~/StatCan.OrchardCore.Radar/js/vue-components/radar-vue-components.umd.min.js", "~/StatCan.OrchardCore.Radar/js/vue-components/radar-vue-components.umd.js") + .SetVersion("1.0.0"); + } + + public void Configure(ResourceManagementOptions options) + { + options.ResourceManifests.Add(_manifest); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Scripting/LocalizedContentMethodsProvider.cs b/src/Apps/StatCan.OrchardCore.Radar/Scripting/LocalizedContentMethodsProvider.cs new file mode 100644 index 000000000..408af0054 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Scripting/LocalizedContentMethodsProvider.cs @@ -0,0 +1,123 @@ +using System; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Scripting; +using OrchardCore.ContentManagement; +using OrchardCore.ContentLocalization; +using OrchardCore.Localization; +using OrchardCore.Autoroute.Models; + +namespace StatCan.OrchardCore.Radar.Scripting +{ + public class LocalizedContentMethodsProvider : IGlobalMethodProvider + { + private readonly GlobalMethod _createLocalizedContentItem; + private readonly GlobalMethod _updateLocalizedContentItem; + private readonly GlobalMethod _getLocalizedContentItemById; + + public LocalizedContentMethodsProvider() + { + _createLocalizedContentItem = new GlobalMethod() + { + Name = "createLocalizedContentItem", + Method = serviceProvider => (Func)((contentType, properties) => + { + // The behaviour is that a content will be created in the default culture. Then the content will be localized across all supported localizations + + // Creating content in the default culture + var contentManager = serviceProvider.GetRequiredService(); + var contentItem = contentManager.NewAsync(contentType).GetAwaiter().GetResult(); + contentItem.Merge(properties); + + // Create localized version of the content + var contentLocalizationManager = serviceProvider.GetRequiredService(); + var localizationService = serviceProvider.GetRequiredService(); + + var supportedCultures = localizationService.GetSupportedCulturesAsync().GetAwaiter().GetResult(); + ContentItem resultItem = null; + + foreach (var culture in supportedCultures) + { + var localizedContent = contentLocalizationManager.LocalizeAsync(contentItem, culture).GetAwaiter().GetResult(); + contentManager.CreateAsync(localizedContent).GetAwaiter().GetResult(); + contentManager.UpdateAsync(localizedContent).GetAwaiter().GetResult(); // This is needed to to generate the routes + + localizedContent.Alter(part => part.RouteContainedItems = true); + contentManager.UpdateAsync(localizedContent).GetAwaiter().GetResult(); // This is needed to enable the rounte contained option + contentManager.PublishAsync(localizedContent).GetAwaiter().GetResult(); + + if (CultureInfo.CurrentCulture.Name == culture) + { + resultItem = localizedContent; + } + } + + return resultItem; + }) + }; + + _updateLocalizedContentItem = new GlobalMethod() + { + Name = "updateLocalizedContentItem", + Method = serviceProvider => (Action)((contentItem, properties) => + { + var contentLocalizationManager = serviceProvider.GetRequiredService(); + var localizationService = serviceProvider.GetRequiredService(); + var contentManager = serviceProvider.GetRequiredService(); + + contentItem.Merge(properties, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); + + contentManager.UpdateAsync(contentItem).GetAwaiter().GetResult(); + + var supportedCultures = localizationService.GetSupportedCulturesAsync().GetAwaiter().GetResult(); + var localizationSet = contentItem.Content.LocalizationPart.LocalizationSet.ToString(); + + properties["RadarEntityPart"]["Name"]["Text"].Parent.Remove(); + properties["RadarEntityPart"]["Description"]["Text"].Parent.Remove(); + + // Update non-text related field + foreach (var culture in supportedCultures) + { + if (CultureInfo.CurrentCulture.Name != culture) + { + var localizedVersion = contentLocalizationManager.GetContentItemAsync(localizationSet, culture).GetAwaiter().GetResult() as ContentItem; + localizedVersion.Merge(properties, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); + + contentManager.UpdateAsync(localizedVersion).GetAwaiter().GetResult(); + } + } + }) + }; + + _getLocalizedContentItemById = new GlobalMethod() + { + Name = "getLocalizedContentItemById", + Method = serviceProvider => (Func)((id) => + { + var contentManager = serviceProvider.GetRequiredService(); + var contentLocalizationManager = serviceProvider.GetRequiredService(); + + var contentItem = contentManager.GetAsync(id, VersionOptions.Latest).GetAwaiter().GetResult(); + + if (contentItem == null) + { + return null; + } + + var localizationSet = contentItem.Content.LocalizationPart.LocalizationSet.ToString(); + var localizedContent = contentLocalizationManager.GetContentItemAsync(localizationSet, CultureInfo.CurrentCulture.Name).GetAwaiter().GetResult(); + + return localizedContent; + }) + }; + } + + public IEnumerable GetMethods() + { + return new[] { _createLocalizedContentItem, _updateLocalizedContentItem, _getLocalizedContentItemById }; + } + + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormMethodsProvider.cs b/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormMethodsProvider.cs new file mode 100644 index 000000000..9cb903083 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormMethodsProvider.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using OrchardCore.ContentManagement; +using OrchardCore.Queries; +using OrchardCore.Shortcodes.Services; +using OrchardCore.Flows.Models; +using OrchardCore.Scripting; +using OrchardCore.Taxonomies.Models; +using OrchardCore.Autoroute.Models; +using OrchardCore.ContentLocalization; +using OrchardCore.Localization; +using StatCan.OrchardCore.Radar.FormModels; +using StatCan.OrchardCore.Radar.Services.ValueConverters; +using StatCan.OrchardCore.Radar.Services.ContentConverters; +using StatCan.OrchardCore.Radar.Services; + +namespace StatCan.OrchardCore.Radar.Scripting +{ + public class RadarFormMethodsProvider : IGlobalMethodProvider + { + private readonly GlobalMethod _createOrUpdateTopic; + private readonly GlobalMethod _convertToFormModel; + private readonly GlobalMethod _convertToContentDocument; + private readonly GlobalMethod _createOrUpdateArtifact; + private readonly GlobalMethod _getTaxonomyByTypeAndId; + + public RadarFormMethodsProvider() + { + { + _createOrUpdateTopic = new GlobalMethod + { + Name = "createOrUpdateTopic", + Method = serviceProvider => (Func)((id, values) => + { + var rawValueConverter = serviceProvider.GetRequiredService(); + var topicFormModel = (TopicFormModel)rawValueConverter.ConvertAsync(values).GetAwaiter().GetResult(); + + var contentConverter = serviceProvider.GetRequiredService(); + + var queryManager = serviceProvider.GetRequiredService(); + var shortcodeService = serviceProvider.GetRequiredService(); + var contentManager = serviceProvider.GetRequiredService(); + + // Each topic needs to be retrived from the taxonomy term + var topicQuery = queryManager.GetQueryAsync("AllTaxonomiesSQL").GetAwaiter().GetResult(); + var topicResult = queryManager.ExecuteQueryAsync(topicQuery, new Dictionary { { "type", "Topics" } }).GetAwaiter().GetResult(); + + // Updating existing topic + if (topicResult != null) + { + var topicTaxonomy = topicResult.Items.First() as ContentItem; + var topicPart = topicTaxonomy.As().Content; + + foreach (JObject existingTopic in (JArray)topicPart.Terms) + { + if (id.Equals(existingTopic["ContentItemId"].Value())) + { + var topic = contentManager.NewAsync("Topic").GetAwaiter().GetResult(); + var existing = existingTopic.ToObject(); + + // Converts form model into content item document + var topicUpdateObject = contentConverter.ConvertAsync(topicFormModel, new { Existing = existing }).GetAwaiter().GetResult(); + + topic.ContentItemId = existing.ContentItemId; + topic.Merge(existing); + topic.Merge(topicUpdateObject, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }); + topic.Weld(); + topic.Alter(t => t.TaxonomyContentItemId = topicTaxonomy.ContentItemId); + + existingTopic.Merge(topic.Content, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }); + + existingTopic["DisplayText"] = topicUpdateObject["Topic"]["Name"]["Text"].Value(); + + contentManager.UpdateAsync(topicTaxonomy).GetAwaiter().GetResult(); + + return existing.ContentItemId; + } + } + + // Creating new topic + var newTopic = contentManager.NewAsync("Topic").GetAwaiter().GetResult(); + newTopic.Weld(); + newTopic.Alter(t => t.TaxonomyContentItemId = topicTaxonomy.ContentItemId); + newTopic.Alter(part => + { + part.Path = newTopic.ContentItemId; + }); + + // Converts form model into content item document + var topicCreateObject = contentConverter.ConvertAsync(topicFormModel, null).GetAwaiter().GetResult(); + + newTopic.Merge(topicCreateObject); + newTopic.DisplayText = topicCreateObject["Topic"]["Name"]["Text"].Value(); + + topicTaxonomy.Alter(part => part.Terms.Add(newTopic)); + + contentManager.UpdateAsync(topicTaxonomy).GetAwaiter().GetResult(); + contentManager.UnpublishAsync(topicTaxonomy).GetAwaiter().GetResult(); // Needs to unpublish it first otherwise publish has no effect + contentManager.PublishAsync(topicTaxonomy).GetAwaiter().GetResult(); + + return newTopic.ContentItemId; + } + + return null; + }) + }; + + _createOrUpdateArtifact = new GlobalMethod() + { + Name = "createOrUpdateArtifact", + Method = serviceProvider => (Func)((parentId, id, formModel) => + { + var shortcodeService = serviceProvider.GetRequiredService(); + var contentManager = serviceProvider.GetRequiredService(); + var contentConverter = serviceProvider.GetRequiredService(); + + var contentLocalizationManager = serviceProvider.GetRequiredService(); + var localizationService = serviceProvider.GetRequiredService(); + + var parentContentItem = contentManager.GetAsync(parentId).GetAwaiter().GetResult(); + + var supportedCultures = localizationService.GetSupportedCulturesAsync().GetAwaiter().GetResult(); + + if (parentContentItem != null) + { + var localizationSet = parentContentItem.Content.LocalizationPart.LocalizationSet.ToString(); + var isUpdate = false; + var artifactId = ""; + + var workspace = parentContentItem.Get("Workspace"); + var artifactLocalizationSet = ""; + + foreach (var artifact in workspace.ContentItems) + { + if (id.Equals(artifact.ContentItemId)) + { + isUpdate = true; + artifactId = artifact.ContentItemId; + artifactLocalizationSet = artifact.Content.Artifact.LocalizationSet.Text.ToObject(); + } + } + + if (isUpdate) + { + // Update the artifact in other localized version of parent content items + foreach (var supportedCulture in supportedCultures) + { + var localizedVersion = contentLocalizationManager.GetContentItemAsync(localizationSet, supportedCulture).GetAwaiter().GetResult() as ContentItem; + + var localizedWorkspace = localizedVersion.Get("Workspace"); + + foreach (var artifact in localizedWorkspace.ContentItems) + { + if (artifact.Content.Artifact.LocalizationSet.Text.ToObject() == artifactLocalizationSet) + { + var tempArtifact = contentManager.NewAsync("Artifact").GetAwaiter().GetResult(); + tempArtifact.Merge(artifact); + + // Converts form model into content item document + var artifactUpdateObject = contentConverter.ConvertAsync(formModel, new { Existing = artifact }).GetAwaiter().GetResult(); + tempArtifact.Merge(artifactUpdateObject, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }); + + artifact.Merge(tempArtifact, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }); + + artifact.DisplayText = artifactUpdateObject["TitlePart"]["Title"].Value(); + + if (artifactUpdateObject["Published"].Value()) + { + contentManager.PublishAsync(artifact).GetAwaiter().GetResult(); + } + else + { + contentManager.UnpublishAsync(artifact).GetAwaiter().GetResult(); + } + + contentManager.UpdateAsync(artifact).GetAwaiter().GetResult(); + + localizedVersion.Apply("Workspace", localizedWorkspace); + + contentManager.UpdateAsync(localizedVersion).GetAwaiter().GetResult(); + contentManager.UnpublishAsync(localizedVersion).GetAwaiter().GetResult(); + contentManager.PublishAsync(localizedVersion).GetAwaiter().GetResult(); + } + } + } + + return artifactId; + } + + var artifactCreateObject = contentConverter.ConvertAsync(formModel, null).GetAwaiter().GetResult(); + + // Create the artifact in all localized version also + foreach (var supportedCulture in supportedCultures) + { + var localizedVersion = contentLocalizationManager.GetContentItemAsync(localizationSet, supportedCulture).GetAwaiter().GetResult() as ContentItem; + + var newArtifact = contentManager.NewAsync("Artifact").GetAwaiter().GetResult(); + newArtifact.Merge(artifactCreateObject); + newArtifact.DisplayText = artifactCreateObject["TitlePart"]["Title"].Value(); + newArtifact.Alter(part => part.Path = "artifacts/" + newArtifact.ContentItemId); + + if(artifactCreateObject["Published"].Value()) + { + contentManager.PublishAsync(newArtifact).GetAwaiter().GetResult(); + } + + contentManager.UpdateAsync(newArtifact).GetAwaiter().GetResult(); + + newArtifact.Latest = true; + + var localizedWorkspace = localizedVersion.Get("Workspace"); + localizedWorkspace.ContentItems.Add(JObject.FromObject(newArtifact).ToObject()); + localizedVersion.Apply("Workspace", localizedWorkspace); + + contentManager.UpdateAsync(localizedVersion).GetAwaiter().GetResult(); + contentManager.UnpublishAsync(localizedVersion).GetAwaiter().GetResult(); + contentManager.PublishAsync(localizedVersion).GetAwaiter().GetResult(); + + if (CultureInfo.CurrentCulture.Name == supportedCulture) + { + artifactId = newArtifact.ContentItemId; + } + } + + return artifactId; + } + + return null; + }) + }; + + _convertToFormModel = new GlobalMethod() + { + Name = "convertToFormModel", + Method = serviceProvider => (Func)((type, rawValues) => + { + IRawValueConverter converter = null; + + if (type == "Project") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Proposal") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Community") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Event") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Artifact") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Topic") + { + converter = serviceProvider.GetRequiredService(); + } + + return converter.ConvertAsync(rawValues).GetAwaiter().GetResult(); + }) + }; + + _convertToContentDocument = new GlobalMethod() + { + Name = "convertToContentDocument", + Method = serviceProvider => (Func)((type, formModel) => + { + IContentConverter converter = null; + + if (type == "Project") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Proposal") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Community") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Event") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Artifact") + { + converter = serviceProvider.GetRequiredService(); + } + else if (type == "Topic") + { + converter = serviceProvider.GetRequiredService(); + } + + return converter.ConvertAsync(formModel, null).GetAwaiter().GetResult(); + }) + }; + + _getTaxonomyByTypeAndId = new GlobalMethod() + { + Name = "getTaxonomyByTypeAndId", + Method = serviceProvider => (Func)((type, id) => + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + var taxonomyManager = serviceProvider.GetRequiredService(); + + return taxonomyManager.GetTaxonomyTermByIdAsync(type, id).GetAwaiter().GetResult(); + }) + }; + } + } + + public IEnumerable GetMethods() + { + return new[] { _createOrUpdateTopic, _convertToFormModel, _convertToContentDocument, _createOrUpdateArtifact, _getTaxonomyByTypeAndId }; + } + } +} + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormValidationMethodsProvider.cs b/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormValidationMethodsProvider.cs new file mode 100644 index 000000000..9322e2431 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Scripting/RadarFormValidationMethodsProvider.cs @@ -0,0 +1,395 @@ +using System.Linq; +using System; +using System.Globalization; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Scripting; +using OrchardCore.ContentManagement; +using OrchardCore.Queries; +using OrchardCore.ContentLocalization.Models; +using OrchardCore.Shortcodes.Services; +using OrchardCore.Security.Services; +using StatCan.OrchardCore.Radar.Services; + +namespace StatCan.OrchardCore.Radar.Scripting +{ + public class RadarFormValidationMethodsProvider : IGlobalMethodProvider + { + private readonly GlobalMethod _validateRadarEntityName; + private readonly GlobalMethod _validateMaxStringLength; + private readonly GlobalMethod _validateString; + private readonly GlobalMethod _validateMemberWithoutRole; + private readonly GlobalMethod _validateMemberWithRole; + private readonly GlobalMethod _validateRelatedEntities; + private readonly GlobalMethod _validateEventDateTime; + private readonly GlobalMethod _validateTaxonomyName; + private readonly GlobalMethod _validateTopics; + private readonly GlobalMethod _validateEntityType; // This is not Content Type + private readonly GlobalMethod _validateUrl; + private readonly GlobalMethod _validateRoles; + + public RadarFormValidationMethodsProvider() + { + _validateRadarEntityName = new GlobalMethod() + { + Name = "validateRadarEntityName", + Method = serviceProvider => (Func)((contentType, name, existingContent) => + { + var queryManager = serviceProvider.GetRequiredService(); + + var nameQuery = queryManager.GetQueryAsync("EntityByTypeAndNameSQL").GetAwaiter().GetResult(); + var nameResult = queryManager.ExecuteQueryAsync(nameQuery, new Dictionary { { "type", contentType }, { "name", name } }).GetAwaiter().GetResult(); + + var contentItems = nameResult.Items.Cast(); + + if (!nameResult.Items.Any()) + { + return true; + } + + // Vadaliation is slightly different if updating a content item + if (existingContent != null) + { + var existingLocalizationPart = existingContent.As(); + + foreach (var contentItem in contentItems) + { + var localizationPart = contentItem.As(); + + if (contentItem.ContentItemId != existingContent.ContentItemId && existingLocalizationPart.LocalizationSet != localizationPart.LocalizationSet && existingLocalizationPart.Culture == localizationPart.Culture) + { + return false; + } + } + + return true; // Here means the name is unique + } + + foreach (var contentItem in contentItems) + { + var localizationPart = contentItem.As(); + + // It's not ok to have non-unique name across different localization + if (CultureInfo.CurrentCulture.Name == localizationPart.Culture) + { + return false; + } + } + + return true; + }) + }; + + _validateMaxStringLength = new GlobalMethod() + { + Name = "validateMaxStringLength", + Method = serviceProvider => (Func)((str, targetLength) => + { + return str.Length <= targetLength; + }) + }; + + _validateString = new GlobalMethod() + { + Name = "validateString", + Method = serviceProvider => (Func)((str) => + { + return !string.IsNullOrWhiteSpace(str); + }) + }; + + _validateMemberWithoutRole = new GlobalMethod() + { + Name = "validateMemberWithoutRole", + Method = serviceProvider => (Func>, bool>)((members) => + { + var queryManager = serviceProvider.GetRequiredService(); + + var userIds = new HashSet(); + + foreach (var member in members) + { + userIds.Add(member["value"]); + } + + // Check if they are duplicated user + if (userIds.Count() != members.Count()) + { + return false; + } + + // Check if there are any non-existent user id + var userQuery = queryManager.GetQueryAsync("UserByIdSQL").GetAwaiter().GetResult(); + + foreach (var userId in userIds) + { + var result = queryManager.ExecuteQueryAsync(userQuery, new Dictionary { { "userId", userId } }).GetAwaiter().GetResult(); + + if (!result.Items.Any()) + { + return false; + } + } + + return true; + }) + }; + + _validateMemberWithRole = new GlobalMethod() + { + Name = "validateMemberWithRole", + Method = serviceProvider => (Func>, bool>)((members) => + { + var queryManager = serviceProvider.GetRequiredService(); + + var userIds = new HashSet(); + var roles = new LinkedList(); + + foreach (var member in members) + { + var memberObj = (JObject) member["user"]; + + userIds.Add(memberObj["value"].Value()); + roles.AddLast((string)member["role"]); + } + + // Check if they are duplicated user + if (userIds.Count() != members.Count()) + { + return false; + } + + + // Check if there are empty roles + if (roles.Where(role => string.IsNullOrWhiteSpace(role)).Any()) + { + return false; + } + + // Check if there are any non-existent user id + var userQuery = queryManager.GetQueryAsync("UserByIdSQL").GetAwaiter().GetResult(); + + foreach (var userId in userIds) + { + var result = queryManager.ExecuteQueryAsync(userQuery, new Dictionary { { "userId", userId } }).GetAwaiter().GetResult(); + + if (!result.Items.Any()) + { + return false; + } + } + + return true; + }) + }; + + _validateRelatedEntities = new GlobalMethod() + { + Name = "validateRelatedEntities", + Method = serviceProvider => (Func>, string, bool>)((relatedEntities, requiredContentType) => + { + var queryManager = serviceProvider.GetRequiredService(); + var contentManager = serviceProvider.GetRequiredService(); + + var entityIds = new HashSet(); + + foreach (var entity in relatedEntities) + { + entityIds.Add(entity["value"]); + } + + // Check if they are duplicated entity + if (entityIds.Count() != relatedEntities.Count()) + { + return false; + } + + // Check if there are any non-existent user id or not correct content type + var entityQuery = queryManager.GetQueryAsync("EntityByLocalizationSetSQL").GetAwaiter().GetResult(); + + foreach (var entityId in entityIds) + { + var result = queryManager.ExecuteQueryAsync(entityQuery, new Dictionary { { "localizationSet", entityId } }).GetAwaiter().GetResult(); + + if (!result.Items.Any()) + { + return false; + } + + var contentItemId = (result.Items.First() as JObject)["ContentItemId"].Value(); + + var contentItem = contentManager.GetAsync(contentItemId).GetAwaiter().GetResult(); + + if (contentItem.ContentType != requiredContentType) + { + return false; + } + } + + return true; + }) + }; + + _validateEventDateTime = new GlobalMethod() + { + Name = "validateEventDateTime", + Method = serviceProvider => (Func)((startDate, startTime, endDate, endTime) => + { + try + { + DateTime startDateTime = DateTime.Parse($"{startDate} {startTime}"); + DateTime endDateTime = DateTime.Parse($"{endDate} {endTime}"); + + return startDateTime <= endDateTime; + } + catch (FormatException) + { + return false; + } + }) + }; + + _validateTaxonomyName = new GlobalMethod() + { + Name = "validateTaxonomyName", + Method = serviceProvider => (Func)((type, name, existingTerm) => + { + var shortcodeService = serviceProvider.GetRequiredService(); + var taxonomyManager = serviceProvider.GetRequiredService(); + + var terms = taxonomyManager.GetTaxonomyTermsAsync(type).GetAwaiter().GetResult(); + + if (terms != null) + { + foreach (var term in terms) + { + var termName = shortcodeService.ProcessAsync(term.DisplayText).GetAwaiter().GetResult(); + + if (existingTerm != null && term.ContentItemId != existingTerm.ContentItemId && term.DisplayText == name) + { + return false; + } + else if (term.DisplayText == name) + { + return false; + } + } + + return true; + } + + return false; + }) + }; + + _validateTopics = new GlobalMethod() + { + Name = "validateTopics", + Method = serviceProvider => (Func>, bool>)((topics) => + { + var taxonomyManager = serviceProvider.GetRequiredService(); + + var topicIds = new HashSet(); + + foreach (var topic in topics) + { + topicIds.Add(topic["value"]); + } + + // Check if they are duplicated entity + if (topicIds.Count() != topics.Count()) + { + return false; + } + + foreach (var topicId in topicIds) + { + var topic = taxonomyManager.GetTaxonomyTermByIdAsync("Topics", topicId); + + if (topic == null) + { + return false; + } + } + + return true; + }) + }; + + _validateEntityType = new GlobalMethod() + { + Name = "validateEntityType", + Method = serviceProvider => (Func, bool>)((typeTaxonomy, type) => + { + if (type.Count() > 2) // 2 because each key-value pair is counted as 2 elements + { + return false; + } + + var taxonomyManager = serviceProvider.GetRequiredService(); + + var typeTerm = taxonomyManager.GetTaxonomyTermByIdAsync(typeTaxonomy, type["value"]).GetAwaiter().GetResult(); + + if (typeTerm == null) + { + return false; + } + + return true; + }) + }; + + _validateUrl = new GlobalMethod() + { + Name = "validateUrl", + Method = serviceProvider => (Func)((url, allowedHosts) => + { + if(!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + return false; + } + + if(allowedHosts.Any()) + { + string host = new Uri(url).Host; + + if(!allowedHosts.Contains(host)) + { + return false; + } + } + + return true; + }) + }; + + _validateRoles = new GlobalMethod() + { + Name = "validateRoles", + Method = serviceProvider => (Func)((roles) => + { + var roleService = serviceProvider.GetRequiredService(); + + var allowedRoles = roleService.GetRoleNamesAsync().GetAwaiter().GetResult(); + + foreach(var role in roles) + { + if(!allowedRoles.Contains(role)) + { + return false; + } + } + + return true; + }) + }; + } + + public IEnumerable GetMethods() + { + return new[] { _validateRadarEntityName, _validateMaxStringLength, _validateString, _validateMemberWithoutRole, _validateMemberWithRole, + _validateRelatedEntities, _validateEventDateTime, _validateTaxonomyName, _validateTopics, _validateEntityType, _validateUrl, _validateRoles }; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/BagItemManager.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/BagItemManager.cs new file mode 100644 index 000000000..3445de9aa --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/BagItemManager.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.ContentManagement; +using OrchardCore.Flows.Models; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class BagItemManager + { + private readonly IContentManager _contentManager; + + public BagItemManager(IContentManager contentManager) + { + _contentManager = contentManager; + } + + public async Task DeleteBagItemAsync(string bagName, string parentId, string id) + { + var parentContentItem = await _contentManager.GetAsync(parentId); + + if(parentContentItem == null) + { + throw new Exception("Parent Content Item does not exist"); + } + + var bag = parentContentItem.Get(bagName); + + bag.ContentItems = bag.ContentItems.Where(contentItem => contentItem.ContentItemId != id).ToList(); + + parentContentItem.Apply(bagName, bag); + + await _contentManager.UpdateAsync(parentContentItem); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ArtifactContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ArtifactContentConverter.cs new file mode 100644 index 000000000..de1a73df1 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ArtifactContentConverter.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class ArtifactContentConverter : BaseContentConverter + { + public ArtifactContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + public override JObject ConvertFromFormModel(FormModel formModel, dynamic context) + { + var artifactFormModel = (ArtifactFormModel)formModel; + + if (context != null) + { + var artifactUpdateObject = new + { + Published = GetPublishStatus(artifactFormModel.PublishStatus), + Artifact = new + { + URL = new { Text = artifactFormModel.Url }, + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = artifactFormModel.Roles, + }, + TitlePart = new + { + Title = UpdateLocalizedString(context.Existing.DisplayText.ToString(), artifactFormModel.Name, CultureInfo.CurrentCulture.Name) + } + }; + + return JObject.FromObject(artifactUpdateObject); + } + else + { + var artifactCreateObject = new + { + Published = GetPublishStatus(artifactFormModel.PublishStatus), + Artifact = new + { + URL = new { Text = artifactFormModel.Url }, + LocalizationSet = new { Text = Guid.NewGuid().ToString() } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = artifactFormModel.Roles, + }, + TitlePart = new + { + Title = CreateLocalizedString(artifactFormModel.Name, CultureInfo.CurrentCulture.Name) + } + }; + + return JObject.FromObject(artifactCreateObject); + } + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverter.cs new file mode 100644 index 000000000..27c9a07f1 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverter.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OrchardCore.ContentManagement; +using OrchardCore.Queries; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public abstract class BaseContentConverter : IContentConverter + { + private readonly IQueryManager _queryManager; + private readonly IContentManager _contentManager; + + public BaseContentConverter(BaseContentConverterDependency baseContentConverterDependency) + { + _queryManager = baseContentConverterDependency.GetQueryManager(); + _contentManager = baseContentConverterDependency.GetContentManager(); + } + + public Task ConvertAsync(FormModel formModel, dynamic context) + { + return ConvertFromFormModelAsync(formModel, context); + } + + public virtual JObject ConvertFromFormModel(FormModel formModel, dynamic context) + { + return null; + } + + public virtual Task ConvertFromFormModelAsync(FormModel formModel, dynamic context) + { + return Task.FromResult(ConvertFromFormModel(formModel, context)); + } + + protected async Task> GetMembersContentAsync(ICollection> members, string type, Func, object> func) + { + // Contents in bag parts has to be ContentItem + + ICollection membersContent = new LinkedList(); + + foreach (var member in members) + { + var memberObject = func(member); + + var contentItem = await _contentManager.NewAsync(type); + contentItem.Merge(memberObject); + await _contentManager.UpdateAsync(contentItem); + + membersContent.Add(contentItem); + } + + return membersContent; + } + + protected async Task GetTaxonomyIdAsync(string type) + { + var topicQuery = await _queryManager.GetQueryAsync("AllTaxonomiesSQL"); + var topicResult = await _queryManager.ExecuteQueryAsync(topicQuery, new Dictionary { { "type", type } }); + + if (topicResult == null) + { + return null; + } + + var topicTaxonomy = topicResult.Items.First() as ContentItem; + + return topicTaxonomy.ContentItemId; + } + + protected bool GetPublishStatus(string statusString) + { + if(statusString == "Publish" || statusString == "Publier") + { + return true; + } else + { + return false; + } + } + + // Maps object with all string properties to string list + protected ICollection MapDictListToStringList(ICollection> list, Func, string> func) + { + ICollection stringList = new LinkedList(); + + foreach (var item in list) + { + string value = func(item); + stringList.Add(value); + } + + return stringList; + } + + protected string CreateLocalizedString(string content, string currentCulture) + { + var supportedCultures = new string[] { "en", "fr" }; // Assumming only en and fr + StringBuilder sb = new StringBuilder(); + + foreach (var supportedCulture in supportedCultures) + { + sb.Append($"[locale {supportedCulture}]"); + sb.Append(content); + sb.Append("[/locale]"); + } + + return sb.ToString(); + } + + protected IDictionary ExtractLocalizedString(string localizedString) + { + var supportedCultures = new string[] { "en", "fr" }; // Assumming only en and fr + var localizedStrings = new Dictionary(); + + foreach (var supportedCulture in supportedCultures) + { + var leftTag = $"[locale {supportedCulture}]"; + var rightTag = "[/locale]"; + + var startingIndex = localizedString.IndexOf(leftTag) + leftTag.Length; + var endingIndex = localizedString.IndexOf(rightTag, startingIndex); + + var content = localizedString.Substring(startingIndex, endingIndex - startingIndex); + + localizedStrings.Add(supportedCulture, content); + } + + return localizedStrings; + } + + protected string UpdateLocalizedString(string orginal, string insert, string currentCulture) + { + IDictionary localizedStrings = ExtractLocalizedString(orginal); + var supportedCultures = new string[] { "en", "fr" }; // Assumming only en and fr + StringBuilder sb = new StringBuilder(); + + foreach (var supportedCulture in supportedCultures) + { + sb.Append($"[locale {supportedCulture}]"); + if (currentCulture == supportedCulture) + { + sb.Append(insert); + } + else + { + sb.Append(localizedStrings[supportedCulture]); + } + sb.Append("[/locale]"); + } + + return sb.ToString(); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverterDependency.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverterDependency.cs new file mode 100644 index 000000000..9a07f076c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/BaseContentConverterDependency.cs @@ -0,0 +1,27 @@ +using OrchardCore.ContentManagement; +using OrchardCore.Queries; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class BaseContentConverterDependency + { + private readonly IQueryManager _queryManager; + private readonly IContentManager _contentManager; + + public BaseContentConverterDependency(IQueryManager queryManager, IContentManager contentManager) + { + _queryManager = queryManager; + _contentManager = contentManager; + } + + public IQueryManager GetQueryManager() + { + return _queryManager; + } + + public IContentManager GetContentManager() + { + return _contentManager; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/CommunityContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/CommunityContentConverter.cs new file mode 100644 index 000000000..1968f0b45 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/CommunityContentConverter.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class CommunityContentConverter : BaseContentConverter + { + public CommunityContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + public override async Task ConvertFromFormModelAsync(FormModel formModel, object context) + { + CommunityFormModel communityFormModel = (CommunityFormModel)formModel; + + var communityContentObject = new + { + Published = GetPublishStatus(communityFormModel.PublishStatus), + Community = new + { + Type = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Community Types"), + TermContentItemIds = new string[] { communityFormModel.Type["value"] }, + TagNames = new string[] { communityFormModel.Type["label"] } + } + }, + RadarEntityPart = new + { + Name = new + { + Text = communityFormModel.Name + }, + Description = new + { + Text = communityFormModel.Description + }, + Topics = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Topics"), + TermContentItemIds = MapDictListToStringList(communityFormModel.Topics, topic => topic["value"]), + TagNames = MapDictListToStringList(communityFormModel.Topics, topic => topic["label"]) + }, + Publish = new + { + Value = GetPublishStatus(communityFormModel.PublishStatus), + } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = communityFormModel.Roles + }, + CommunityMember = new + { + ContentItems = await GetMembersContentAsync(communityFormModel.CommunityMembers, "CommunityMember", member => + { + var userObject = (JObject)member["user"]; + var memberObject = new + { + CommunityMember = new + { + Member = new + { + UserIds = new string[] { userObject["value"].Value() }, + UserNames = new string[] { userObject["label"].Value() } + }, + Role = new + { + Text = member["role"] + } + } + }; + + return memberObject; + }) + } + }; + + return JObject.FromObject(communityContentObject); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/EventContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/EventContentConverter.cs new file mode 100644 index 000000000..487bd7355 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/EventContentConverter.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading.Tasks; +using System.Globalization; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class EventContentConverter : BaseContentConverter + { + public EventContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + + public override async Task ConvertFromFormModelAsync(FormModel formModel, dynamic context) + { + EventFormModel eventFormModel = (EventFormModel)formModel; + + var eventContentObject = new + { + Published = GetPublishStatus(eventFormModel.PublishStatus), + Event = new + { + Attendees = new + { + UserIds = MapDictListToStringList(eventFormModel.Attendees, attendee => attendee["value"].ToString()), + UserNames = MapDictListToStringList(eventFormModel.Attendees, attendee => attendee["label"].ToString()), + }, + StartDate = new + { + Value = DateTime.Parse($"{eventFormModel.StartDate} {eventFormModel.StartTime}", CultureInfo.CurrentCulture) + }, + EndDate = new + { + Value = DateTime.Parse($"{eventFormModel.EndDate} {eventFormModel.EndTime}", CultureInfo.CurrentCulture) + } + }, + RadarEntityPart = new + { + Name = new + { + Text = eventFormModel.Name + }, + Description = new + { + Text = eventFormModel.Description + }, + Topics = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Topics"), + TermContentItemIds = MapDictListToStringList(eventFormModel.Topics, topic => topic["value"]), + TagNames = MapDictListToStringList(eventFormModel.Topics, topic => topic["label"]) + }, + Publish = new + { + Value = GetPublishStatus(eventFormModel.PublishStatus), + } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = eventFormModel.Roles + }, + EventOrganizer = new + { + ContentItems = await GetMembersContentAsync(eventFormModel.EventOrganizers, "EventOrganizer", organizer => + { + var organizerObject = new + { + EventOrganizer = new + { + Organizer = new + { + UserIds = new string[] { organizer["value"] }, + UserNames = new string[] { organizer["label"] } + }, + Role = new + { + Text = "" + } + } + }; + + return organizerObject; + }) + }, + AutoroutePart = new + { + RouteContainedItems = true, + } + }; + + return JObject.FromObject(eventContentObject); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/IContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/IContentConverter.cs new file mode 100644 index 000000000..490ef4b11 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/IContentConverter.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public interface IContentConverter + { + Task ConvertAsync(FormModel formModel, dynamic context); + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProjectContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProjectContentConverter.cs new file mode 100644 index 000000000..bc820d160 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProjectContentConverter.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class ProjectContentConverter : BaseContentConverter + { + public ProjectContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + + public override async Task ConvertFromFormModelAsync(FormModel formModel, object context) + { + ProjectFormModel projectFormModel = (ProjectFormModel)formModel; + + var projectContentObject = new + { + Published = GetPublishStatus(projectFormModel.PublishStatus), + Project = new + { + Type = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Project Types"), + TermContentItemIds = new string[] { projectFormModel.Type["value"] }, + TagNames = new string[] { projectFormModel.Type["label"] } + } + }, + RadarEntityPart = new + { + Name = new + { + Text = projectFormModel.Name + }, + Description = new + { + Text = projectFormModel.Description + }, + Topics = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Topics"), + TermContentItemIds = MapDictListToStringList(projectFormModel.Topics, topic => topic["value"]), + TagNames = MapDictListToStringList(projectFormModel.Topics, topic => topic["label"]) + }, + Publish = new + { + Value = GetPublishStatus(projectFormModel.PublishStatus), + } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = projectFormModel.Roles + }, + ProjectMember = new + { + ContentItems = await GetMembersContentAsync(projectFormModel.ProjectMembers, "ProjectMember", member => + { + var userObject = (JObject)member["user"]; + var memberObject = new + { + ProjectMember = new + { + Member = new + { + UserIds = new string[] { userObject["value"].Value() }, + UserNames = new string[] { userObject["label"].Value() } + }, + Role = new + { + Text = member["role"] + } + } + }; + + return memberObject; + }) + }, + AutoroutePart = new + { + RouteContainedItems = true, + } + }; + + return JObject.FromObject(projectContentObject); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProposalContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProposalContentConverter.cs new file mode 100644 index 000000000..53d662711 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/ProposalContentConverter.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class ProposalContentConverter : BaseContentConverter + { + public ProposalContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + + public override async Task ConvertFromFormModelAsync(FormModel formModel, dynamic context) + { + ProposalFormModel proposalFormModel = (ProposalFormModel)formModel; + + var proposalContentObject = new + { + Proposal = new + { + Type = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Proposal Types"), + TermContentItemIds = new string[] { proposalFormModel.Type["value"] }, + TagNames = new string[] { proposalFormModel.Type["label"] } + } + }, + RadarEntityPart = new + { + Name = new + { + Text = proposalFormModel.Name + }, + Description = new + { + Text = proposalFormModel.Description + }, + Topics = new + { + TaxonomyContentItemId = await GetTaxonomyIdAsync("Topics"), + TermContentItemIds = MapDictListToStringList(proposalFormModel.Topics, topic => topic["value"]), + TagNames = MapDictListToStringList(proposalFormModel.Topics, topic => topic["label"]) + }, + RelatedEntity = new + { + LocalizationSets = MapDictListToStringList(proposalFormModel.RelatedEntities, entity => entity["value"]) + }, + Publish = new + { + Value = GetPublishStatus(proposalFormModel.PublishStatus), + } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = proposalFormModel.Roles + }, + AutoroutePart = new + { + RouteContainedItems = true, + } + }; + + return JObject.FromObject(proposalContentObject); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/TopicContentConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/TopicContentConverter.cs new file mode 100644 index 000000000..c5c2eafb1 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ContentConverters/TopicContentConverter.cs @@ -0,0 +1,55 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ContentConverters +{ + public class TopicContentConverter : BaseContentConverter + { + public TopicContentConverter(BaseContentConverterDependency baseContentConverterDependency) : base(baseContentConverterDependency) + { + + } + public override JObject ConvertFromFormModel(FormModel formModel, dynamic context) + { + var topicFormModel = (TopicFormModel)formModel; + + if (context != null) + { + var topicUpdateObject = new + { + Topic = new + { + Name = new { Text = UpdateLocalizedString(context.Existing.Content.Topic.Name.Text.ToString(), topicFormModel.Name, CultureInfo.CurrentCulture.Name) }, + Description = new { Text = UpdateLocalizedString(context.Existing.Content.Topic.Description.Text.ToString(), topicFormModel.Description, CultureInfo.CurrentCulture.Name) } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = topicFormModel.Roles, + } + }; + + return JObject.FromObject(topicUpdateObject); + } + else + { + var topicCreateObject = new + { + Topic = new + { + Name = new { Text = CreateLocalizedString(topicFormModel.Name, CultureInfo.CurrentCulture.Name) }, + Description = new { Text = CreateLocalizedString(topicFormModel.Description, CultureInfo.CurrentCulture.Name) } + }, + ContentPermissionsPart = new + { + Enabled = true, + Roles = topicFormModel.Roles, + } + }; + + return JObject.FromObject(topicCreateObject); + } + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/EntitySearcher.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/EntitySearcher.cs new file mode 100644 index 000000000..5ecced39b --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/EntitySearcher.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Globalization; +using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement; +using OrchardCore.Queries; +using Etch.OrchardCore.ContentPermissions.Services; +using OrchardCore.ContentLocalization.Models; +using OrchardCore.Contents; +using OrchardCore.Shortcodes.Services; +using StatCan.OrchardCore.Radar.Models; +using StatCan.OrchardCore.Radar.Helpers; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class EntitySearcher + { + private readonly IQueryManager _queryManager; + private readonly IContentPermissionsService _contentPermissionsService; + private readonly IAuthorizationService _authorizationService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IShortcodeService _shortcodeService; + private readonly TaxonomyManager _taxonomyManager; + + private const string LIST_QUERY = "EntityListLucene"; + + public EntitySearcher( + IQueryManager queryManager, + IContentPermissionsService contentPermissionsService, + IAuthorizationService authorizationService, + IHttpContextAccessor httpContextAccessor, + TaxonomyManager taxonomyManager, + IShortcodeService shortcodeService) + { + _queryManager = queryManager; + _contentPermissionsService = contentPermissionsService; + _authorizationService = authorizationService; + _httpContextAccessor = httpContextAccessor; + _shortcodeService = shortcodeService; + _taxonomyManager = taxonomyManager; + } + + public async Task> SearchTaxonomyAsync(string type, string searchText) + { + var terms = await _taxonomyManager.GetTaxonomyTermsAsync(type); + ICollection contentItems = new LinkedList(); + + foreach (var term in terms) + { + if (!_contentPermissionsService.CanAccess(term)) + { + continue; + } + + // Delocalize the term name + var termName = await _shortcodeService.ProcessAsync(term.DisplayText); + + if (termName.Contains(searchText, StringComparison.OrdinalIgnoreCase)) + { + contentItems.Add(term); + } + } + + return contentItems; + } + + public async Task> SearchContentItemsAsync(string type, string searchText) + { + // Get the lucene query + var query = await _queryManager.GetQueryAsync(LIST_QUERY); + + // Prepare the parameters + IDictionary parameters = new Dictionary(); + parameters.Add("Type", type); + parameters.Add("Term", searchText != null ? searchText.ToLower() : ""); + + var results = await _queryManager.ExecuteQueryAsync(query, parameters); + + // Convert result to content items + var contentItems = new List(); + + if (results != null) + { + foreach (var result in results.Items) + { + if (!(result is ContentItem contentItem)) + { + contentItem = null; + + if (result is JObject jObject) + { + contentItem = jObject.ToObject(); + } + } + + var localizationPart = contentItem.As(); + var radarPermissionPart = contentItem.As(); + var user = _httpContextAccessor.HttpContext.User; + + if (contentItem?.ContentItemId == null || user == null) + { + continue; + } + // Orchard content permission check + else if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ViewContent, contentItem)) + { + continue; + } + // Content Permission check + else if (!_contentPermissionsService.CanAccess(contentItem)) + { + continue; + } + // Culture check + else if (localizationPart != null && localizationPart.Culture != CultureInfo.CurrentCulture.Name) + { + continue; + } + // Publish/Draft check + else if (!radarPermissionPart.Published && !Ownership.IsOwner(contentItem, user)) + { + continue; + } + + contentItems.Add(contentItem); + } + } + + return contentItems; + } + + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/FormOptionsProvider.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/FormOptionsProvider.cs new file mode 100644 index 000000000..b4da24b73 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/FormOptionsProvider.cs @@ -0,0 +1,183 @@ +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using OrchardCore.ContentManagement; +using OrchardCore.Taxonomies.Models; +using OrchardCore.Security.Services; +using OrchardCore.Queries; +using OrchardCore.Shortcodes.Services; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class FormOptionsProvider + { + private readonly IRoleService _roleService; + private readonly IQueryManager _queryManager; + private readonly IShortcodeService _shortcodeService; + + public FormOptionsProvider(IRoleService roleService, IQueryManager queryManager, IShortcodeService shortcodeService) + { + _roleService = roleService; + _queryManager = queryManager; + _shortcodeService = shortcodeService; + } + + public async Task GetOptionsAsync(string entityType) + { + if (entityType == "topics") + { + return await GetTopicFormOptionsAsync(); + } + else if (entityType == "projects") + { + return await GetProjectFormOptionsAsync(); + } + else if (entityType == "proposals") + { + return await GetProposalFormOptionsAsync(); + } + else if (entityType == "communities") + { + return await GetCommunityFormOptionsAsync(); + } + else if (entityType == "events") + { + return await GetEventFormOptionsAsync(); + } + else if (entityType == "artifacts") + { + return await GetArtifactFormOptionsAsync(); + } + + return null; + } + + private async Task GetTopicFormOptionsAsync() + { + var roles = await _roleService.GetRoleNamesAsync(); + + var formOptionModel = new FormOptionModel() + { + RoleOptions = new LinkedList(), + }; + + await FillRoleOptionsAsync(formOptionModel); + + return formOptionModel; + } + + private async Task GetProjectFormOptionsAsync() + { + var projectFormModel = new EntityFormOptionModel() + { + RoleOptions = new LinkedList(), + TypeOptions = new LinkedList>(), + }; + + await FillRoleOptionsAsync(projectFormModel); + await FillTypeOptionsAsync(projectFormModel, "Project Types"); + await FillVisibilityOptionsAsync(projectFormModel); + + return projectFormModel; + } + + private async Task GetProposalFormOptionsAsync() + { + var proposalFormModel = new EntityFormOptionModel() + { + RoleOptions = new LinkedList(), + TypeOptions = new LinkedList>(), + }; + + await FillRoleOptionsAsync(proposalFormModel); + await FillTypeOptionsAsync(proposalFormModel, "Proposal Types"); + await FillVisibilityOptionsAsync(proposalFormModel); + + return proposalFormModel; + } + + private async Task GetCommunityFormOptionsAsync() + { + var communityFormModel = new EntityFormOptionModel() + { + RoleOptions = new LinkedList(), + TypeOptions = new LinkedList>(), + }; + + await FillRoleOptionsAsync(communityFormModel); + await FillTypeOptionsAsync(communityFormModel, "Community Types"); + await FillVisibilityOptionsAsync(communityFormModel); + + return communityFormModel; + } + + private async Task GetEventFormOptionsAsync() + { + var eventFormModel = new EntityFormOptionModel() + { + RoleOptions = new LinkedList(), + TypeOptions = new LinkedList>(), + }; + + await FillRoleOptionsAsync(eventFormModel); + await FillVisibilityOptionsAsync(eventFormModel); + + return eventFormModel; + } + + private async Task GetArtifactFormOptionsAsync() + { + var roles = await _roleService.GetRoleNamesAsync(); + + var formOptionModel = new EntityFormOptionModel() + { + RoleOptions = new LinkedList(), + }; + + await FillRoleOptionsAsync(formOptionModel); + await FillVisibilityOptionsAsync(formOptionModel); + + return formOptionModel; + } + + private async Task FillVisibilityOptionsAsync(EntityFormOptionModel formOptionModel) + { + var localizedPublishOption = await _shortcodeService.ProcessAsync("[locale en]Publish[/locale][locale fr]Publier[/locale]"); + var localizedDraftOption = await _shortcodeService.ProcessAsync("[locale en]Draft[/locale][locale fr]Brouillon[/locale]"); + + formOptionModel.PublishOptions = new string[] { localizedPublishOption, localizedDraftOption }; + } + private async Task FillRoleOptionsAsync(FormOptionModel formOptionModel) + { + var roles = await _roleService.GetRoleNamesAsync(); + + foreach (var role in roles) + { + formOptionModel.RoleOptions.Add(role); + } + } + + private async Task FillTypeOptionsAsync(EntityFormOptionModel formOptionModel, string type) + { + var taxonomyQuery = await _queryManager.GetQueryAsync("AllTaxonomiesSQL"); + var taxonomyResult = await _queryManager.ExecuteQueryAsync(taxonomyQuery, new Dictionary { { "type", type } }); + + if (taxonomyResult != null) + { + var taxonomyPart = (taxonomyResult.Items.First() as ContentItem).As(); + + foreach (var taxonomy in taxonomyPart.Terms) + { + var optionPair = new Dictionary() + { + {"value", taxonomy.ContentItemId}, + {"label", await _shortcodeService.ProcessAsync(taxonomy.DisplayText)} + }; + + formOptionModel.TypeOptions.Add(optionPair); + } + } + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/FormValueProvider.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/FormValueProvider.cs new file mode 100644 index 000000000..843738fc4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/FormValueProvider.cs @@ -0,0 +1,438 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using OrchardCore.ContentManagement; +using OrchardCore.ContentLocalization; +using OrchardCore.Queries; +using OrchardCore.Shortcodes.Services; +using OrchardCore.Taxonomies.Models; +using OrchardCore.ContentLocalization.Models; +using OrchardCore.Flows.Models; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class FormValueProvider + { + private readonly IContentManager _contentManager; + + private readonly IQueryManager _queryManager; + + private readonly IContentLocalizationManager _contentLocalizationManager; + + private readonly IShortcodeService _shortcodeService; + + public FormValueProvider(IContentManager contentManager, IQueryManager queryManager, IContentLocalizationManager contentLocalizationManager, IShortcodeService shortcodeService) + { + _contentManager = contentManager; + _queryManager = queryManager; + _contentLocalizationManager = contentLocalizationManager; + _shortcodeService = shortcodeService; + } + + public async Task GetInitialValuesAsync(string entityType, string id) + { + if (entityType == "topics") + { + return await GetTopicInitialValuesAsync(id); + } + else if (entityType == "projects") + { + return await GetProjectInitialValuesAsync(id); + } + else if (entityType == "communities") + { + return await GetCommunityInitialValuesAsync(id); + } + else if (entityType == "events") + { + return await GetEventInitialValuesAsync(id); + } + else if (entityType == "proposals") + { + return await GetProposalInitialValuesAsync(id); + } + + return null; + } + + public async Task GetInitialValuesAsync(string entityType, string parentId, string childId) + { + if (entityType == "artifacts") + { + return await GetArtifactInitialValuesAsync(parentId, childId); + } + + return null; + } + + public async Task GetArtifactInitialValuesAsync(string parentId, string childId) + { + var artifactModel = new ArtifactFormModel(); + + artifactModel = new ArtifactFormModel() + { + Id = "", + ParentId = "", + Name = "", + Url = "", + Roles = Array.Empty(), + PublishStatus = "", + }; + + if (!string.IsNullOrEmpty(parentId)) + { + var parentContentItem = await GetLocalizedContentAsync(parentId); + + artifactModel.ParentId = parentContentItem.ContentItemId; + + if(!string.IsNullOrEmpty(childId)) + { + var artifacts = parentContentItem.Get("Workspace").ContentItems; + + foreach (var artifact in artifacts) + { + if (childId == artifact.ContentItemId) + { + artifactModel.Id = artifact.ContentItemId; + artifactModel.Name = await _shortcodeService.ProcessAsync(artifact.DisplayText); + artifactModel.Url = artifact.Content.Artifact.URL.Text.ToObject(); + artifactModel.Roles = artifact.Content.ContentPermissionsPart.Roles.ToObject(); + artifactModel.PublishStatus = await GetPublishStatusAsync(artifact.Published); + + return artifactModel; + } + } + } + + return artifactModel; + } + + return null; + } + + private async Task GetTopicInitialValuesAsync(string id) + { + // Initialize a new form model + var topicFormModel = new TopicFormModel(); + + topicFormModel = new TopicFormModel() + { + Id = "", + Name = "", + Description = "", + Roles = Array.Empty() + }; + + if (!string.IsNullOrEmpty(id)) + { + // Each topic needs to be retrived from the taxonomy term + var topicQuery = await _queryManager.GetQueryAsync("AllTaxonomiesSQL"); + var topicResult = await _queryManager.ExecuteQueryAsync(topicQuery, new Dictionary { { "type", "Topics" } }); + + if (topicResult != null) + { + var topicPart = (topicResult.Items.First() as ContentItem).As(); + + foreach (var topic in topicPart.Terms) + { + if (id.Equals(topic.ContentItemId)) + { + topicFormModel.Id = topic.ContentItemId; + topicFormModel.Name = await _shortcodeService.ProcessAsync(topic.DisplayText); + topicFormModel.Description = await _shortcodeService.ProcessAsync(topic.Content.Topic.Description.Text.ToString()); + topicFormModel.Roles = topic.Content.ContentPermissionsPart.Roles.ToObject(); + + return topicFormModel; + } + } + } + + return null; // Here means that a topic with the given id does not exist + } + + return topicFormModel; // Lack of id means the form is for creation + } + + private async Task GetProjectInitialValuesAsync(string id) + { + var projectFormModel = new ProjectFormModel + { + Id = "", + Name = "", + Description = "", + Roles = Array.Empty(), + Topics = new LinkedList>(), + Type = new Dictionary(), + ProjectMembers = new LinkedList>(), + RelatedEntities = new LinkedList>(), + PublishStatus = "", + }; + + if (!string.IsNullOrEmpty(id)) + { + var contentItem = await GetLocalizedContentAsync(id); + + if (contentItem == null) + { + return null; + } + + await GetValuesFromRadarEntityPartAsync(projectFormModel, contentItem); + projectFormModel.Type = new Dictionary() + { + {"value", contentItem.Content.Project.Type.TermContentItemIds.ToObject()[0]}, + {"label", contentItem.Content.Project.Type.TagNames.ToObject()[0]} + }; + + var projectMembers = contentItem.Content.ProjectMember.ContentItems; + + foreach (var member in projectMembers) + { + var user = new Dictionary() + { + {"user", new Dictionary() + { + {"value", member.ProjectMember.Member.UserIds.ToObject()[0]}, + {"label", member.ProjectMember.Member.UserNames.ToObject()[0]} + }}, + {"role", await _shortcodeService.ProcessAsync(member.ProjectMember.Role.Text.ToString())} + }; + + projectFormModel.ProjectMembers.Add(user); + } + } + + return projectFormModel; + } + + private async Task GetCommunityInitialValuesAsync(string id) + { + var communityFormModel = new CommunityFormModel + { + Id = "", + Name = "", + Description = "", + Roles = Array.Empty(), + Topics = new LinkedList>(), + Type = new Dictionary(), + RelatedEntities = new LinkedList>(), + CommunityMembers = new LinkedList>(), + PublishStatus = "", + }; + + if (!string.IsNullOrEmpty(id)) + { + var contentItem = await GetLocalizedContentAsync(id); + + if (contentItem == null) + { + return null; + } + + await GetValuesFromRadarEntityPartAsync(communityFormModel, contentItem); + communityFormModel.Type = new Dictionary() + { + {"value", contentItem.Content.Community.Type.TermContentItemIds.ToObject()[0]}, + {"label", contentItem.Content.Community.Type.TagNames.ToObject()[0]} + }; + + var communityMembers = contentItem.Content.CommunityMember.ContentItems; + + foreach (var member in communityMembers) + { + var user = new Dictionary() + { + {"user", new Dictionary() + { + {"value", member.CommunityMember.Member.UserIds.ToObject()[0]}, + {"label", member.CommunityMember.Member.UserNames.ToObject()[0]} + }}, + {"role", await _shortcodeService.ProcessAsync(member.CommunityMember.Role.Text.ToString())} + }; + + communityFormModel.CommunityMembers.Add(user); + } + } + + return communityFormModel; + } + + private async Task GetEventInitialValuesAsync(string id) + { + var eventFormModel = new EventFormModel + { + Id = "", + Name = "", + Description = "", + Roles = Array.Empty(), + Topics = new LinkedList>(), + StartDate = GetDateFromDateTime(DateTime.Now), + EndDate = GetDateFromDateTime(DateTime.Now), + StartTime = GetTimeFromDateTime(DateTime.Now), + EndTime = GetTimeFromDateTime(DateTime.Now), + Attendees = new LinkedList>(), + EventOrganizers = new LinkedList>(), + RelatedEntities = new LinkedList>(), + PublishStatus = "", + }; + + if (!string.IsNullOrEmpty(id)) + { + var contentItem = await GetLocalizedContentAsync(id); + + if (contentItem == null) + { + return null; + } + + await GetValuesFromRadarEntityPartAsync(eventFormModel, contentItem); + eventFormModel.StartDate = GetDateFromDateTime(DateTime.Parse(contentItem.Content.Event.StartDate.Value.ToString())); + eventFormModel.StartTime = GetTimeFromDateTime(DateTime.Parse(contentItem.Content.Event.StartDate.Value.ToString())); + eventFormModel.EndDate = GetDateFromDateTime(DateTime.Parse(contentItem.Content.Event.EndDate.Value.ToString())); + eventFormModel.EndTime = GetTimeFromDateTime(DateTime.Parse(contentItem.Content.Event.EndDate.Value.ToString())); + + string[] attendeeIds = contentItem.Content.Event.Attendees.UserIds.ToObject(); + string[] attendeeNames = contentItem.Content.Event.Attendees.UserNames.ToObject(); + + for (var i = 0; i < attendeeIds.Length; i++) + { + var user = new Dictionary() + { + {"value", attendeeIds[i]}, + {"label", attendeeNames[i]} + }; + + eventFormModel.Attendees.Add(user); + } + + var eventOrganizers = contentItem.Content.EventOrganizer.ContentItems; + + foreach (var organizer in eventOrganizers) + { + var user = new Dictionary() + { + {"value", organizer.EventOrganizer.Organizer.UserIds.ToObject()[0]}, + {"label", organizer.EventOrganizer.Organizer.UserNames.ToObject()[0]} + }; + + eventFormModel.EventOrganizers.Add(user); + } + } + + return eventFormModel; + } + + private async Task GetProposalInitialValuesAsync(string id) + { + var proposalFormModel = new ProposalFormModel + { + Id = "", + Name = "", + Description = "", + Roles = Array.Empty(), + Topics = new LinkedList>(), + Type = new Dictionary(), + RelatedEntities = new LinkedList>(), + PublishStatus = "", + }; + + if (!string.IsNullOrEmpty(id)) + { + var contentItem = await GetLocalizedContentAsync(id); + + if (contentItem == null) + { + return null; + } + + await GetValuesFromRadarEntityPartAsync(proposalFormModel, contentItem); + proposalFormModel.Type = new Dictionary() + { + {"value", contentItem.Content.Proposal.Type.TermContentItemIds.ToObject()[0]}, + {"label", contentItem.Content.Proposal.Type.TagNames.ToObject()[0]} + }; + } + + return proposalFormModel; + } + + private string GetDateFromDateTime(DateTime dt) + { + return dt.ToString("yyyy-MM-dd"); + } + + private string GetTimeFromDateTime(DateTime dt) + { + return dt.ToString("hh:mm"); + } + + private async Task GetLocalizedContentAsync(string id) + { + var contentItem = await _contentManager.GetAsync(id, VersionOptions.Latest); + + if (contentItem == null) + { + return null; + } + + var localizationSet = contentItem.Content.LocalizationPart.LocalizationSet.ToString(); + var localizedContent = await _contentLocalizationManager.GetContentItemAsync(localizationSet, CultureInfo.CurrentCulture.Name); + + return localizedContent; + } + + // Hepler method to get the values from RadarEntityPart + private async Task GetValuesFromRadarEntityPartAsync(EntityFormModel entityFormModel, ContentItem contentItem) + { + entityFormModel.Id = contentItem.ContentItemId; + entityFormModel.Name = contentItem.DisplayText; + entityFormModel.Description = contentItem.Content.RadarEntityPart.Description.Text.ToString(); + entityFormModel.Roles = contentItem.Content.ContentPermissionsPart.Roles.ToObject(); + entityFormModel.PublishStatus = await GetPublishStatusAsync(contentItem.Content.RadarEntityPart.Publish.Value.ToObject()); + + var topicIds = contentItem.Content.RadarEntityPart.Topics.TermContentItemIds.ToObject(); + var topicNames = contentItem.Content.RadarEntityPart.Topics.TagNames.ToObject(); + + for (var i = 0; i < topicIds.Length; i++) + { + var optionPair = new Dictionary() + { + {"value", topicIds[i]}, + {"label", await _shortcodeService.ProcessAsync(topicNames[i])} + }; + + entityFormModel.Topics.Add(optionPair); + } + + var localizationSets = contentItem.Content.RadarEntityPart.RelatedEntity.LocalizationSets.ToObject(); + + foreach (var localizationSet in localizationSets) + { + contentItem = await _contentLocalizationManager.GetContentItemAsync(localizationSet, CultureInfo.CurrentCulture.Name); + var part = contentItem.As(); + + var optionPair = new Dictionary(){ + {"value", part.LocalizationSet}, + {"label", contentItem.DisplayText} + }; + + entityFormModel.RelatedEntities.Add(optionPair); + } + } + + private async Task GetPublishStatusAsync(bool isPublished) + { + if (isPublished) + { + return await _shortcodeService.ProcessAsync("[locale en]Publish[/locale][locale fr]Publier[/locale]"); ; + } + else + { + return await _shortcodeService.ProcessAsync("[locale en]Draft[/locale][locale fr]Brouillon[/locale]"); + } + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/TaxonomyManager.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/TaxonomyManager.cs new file mode 100644 index 000000000..5cdfb720a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/TaxonomyManager.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using OrchardCore.Queries; +using OrchardCore.ContentManagement; +using OrchardCore.Taxonomies.Models; + +namespace StatCan.OrchardCore.Radar.Services +{ + public class TaxonomyManager + { + private readonly IQueryManager _queryManager; + private readonly IContentManager _contentManager; + private const string TAXONOMY_QUERY = "AllTaxonomiesSQL"; + + public TaxonomyManager(IQueryManager queryManager, IContentManager contentManager) + { + _queryManager = queryManager; + _contentManager = contentManager; + } + + public async Task GetTaxonomyTermByIdAsync(string type, string id) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + // Each topic needs to be retrived from the taxonomy term + var terms = await GetTaxonomyTermsAsync(type); + + if (terms != null) + { + foreach (var term in terms) + { + if (id.Equals(term.ContentItemId)) + { + return term; + } + } + } + + return null; + } + + public async Task> GetTaxonomyTermsAsync(string type) + { + // Each topic needs to be retrived from the taxonomy term + var taxonomyQuery = await _queryManager.GetQueryAsync(TAXONOMY_QUERY); + var taxonomyResult = await _queryManager.ExecuteQueryAsync(taxonomyQuery, new Dictionary { { "type", type } }); + + if (taxonomyResult != null) + { + var taxonomy = taxonomyResult.Items.First() as ContentItem; + var part = taxonomy.As(); + + return part.Terms; + } + + return null; + } + + public async Task GetTaxonomyAsync(string type) + { + // Each topic needs to be retrived from the taxonomy term + var taxonomyQuery = await _queryManager.GetQueryAsync(TAXONOMY_QUERY); + var taxonomyResult = await _queryManager.ExecuteQueryAsync(taxonomyQuery, new Dictionary { { "type", type } }); + + if (taxonomyResult != null) + { + return taxonomyResult.Items.First() as ContentItem; + } + + return null; + } + + public async Task DeleteTaxonomyAsync(string type, string id) + { + var taxonomy = await GetTaxonomyAsync(type); + + if (taxonomy == null) + { + throw new Exception("The taxonomy with the given type does not exist"); + } + + var part = taxonomy.As(); + + part.Terms = part.Terms.Where(t => t.ContentItemId != id).ToList(); + + taxonomy.Apply(part); + + await _contentManager.UpdateAsync(taxonomy); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ArtifactRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ArtifactRawValueConverter.cs new file mode 100644 index 000000000..598ca7e76 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ArtifactRawValueConverter.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class ArtifactRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("publishOptions"); + rawValues.Remove("__RequestVerificationToken"); + + FixSingleArrayValue(rawValues, "roles"); + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/BaseRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/BaseRawValueConverter.cs new file mode 100644 index 000000000..8abe55c66 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/BaseRawValueConverter.cs @@ -0,0 +1,98 @@ +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public abstract class BaseRawValueConverter : IRawValueConverter + { + public Task ConvertAsync(JObject rawValues) + { + return ConvertFromRawValuesAsync(rawValues); + } + + public virtual FormModel ConvertFromRawValues(JObject rawValues) + { + return null; + } + + public virtual Task ConvertFromRawValuesAsync(JObject rawValues) + { + return Task.FromResult(ConvertFromRawValues(rawValues)); + } + + protected void FixSingleArrayValue(JObject rawValues, string key) + { + // Array having a single value gets converted to JValue instead of JArray so we need to convert it back + if (rawValues[key] is JValue) + { + var roleArray = new JArray + { + rawValues[key] + }; + rawValues[key] = roleArray; + } + } + + protected void FillTopics(JObject rawValues) + { + var topics = new JArray(); + if(rawValues["topics[label]"] != null && rawValues["topics[label]"] != null) + { + for (var i = 0; i < rawValues["topics[label]"].Count(); i++) + { + var topicObject = JObject.FromObject( + new + { + value = rawValues["topics[value]"][i], + label = rawValues["topics[label]"][i] + } + ); + + topics.Add(topicObject); + } + rawValues.Remove("topics[label]"); + rawValues.Remove("topics[value]"); + } + rawValues["topics"] = topics; + } + + protected void FillType(JObject rawValues) + { + var type = JObject.FromObject( + new + { + label = rawValues["type[label]"], + value = rawValues["type[value]"] + } + ); + rawValues.Remove("type[label]"); + rawValues.Remove("type[value]"); + rawValues["type"] = type; + } + + protected void FillRelatedEntities(JObject rawValues) + { + var relatedEntities = new JArray(); + if(rawValues["relatedEntities[label]"] != null && rawValues["relatedEntities[label]"] != null) + { + for (var i = 0; i < rawValues["relatedEntities[label]"].Count(); i++) + { + var topicObject = JObject.FromObject( + new + { + value = rawValues["relatedEntities[value]"][i], + label = rawValues["relatedEntities[label]"][i] + } + ); + + relatedEntities.Add(topicObject); + } + } + rawValues.Remove("relatedEntities[label]"); + rawValues.Remove("relatedEntities[value]"); + rawValues["relatedEntities"] = relatedEntities; + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/CommunityRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/CommunityRawValueConverter.cs new file mode 100644 index 000000000..0178b3fb8 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/CommunityRawValueConverter.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class CommunityRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("__RequestVerificationToken"); + rawValues.Remove("publishOptions"); + rawValues.Remove("typeOptions[label]"); + rawValues.Remove("typeOptions[value]"); + rawValues.Remove("valueNames"); + + FixSingleArrayValue(rawValues, "roles"); + FixSingleArrayValue(rawValues, "topics[label]"); + FixSingleArrayValue(rawValues, "topics[value]"); + FixSingleArrayValue(rawValues, "relatedEntities[label]"); + FixSingleArrayValue(rawValues, "relatedEntities[value]"); + FixSingleArrayValue(rawValues, "communityMembers[role]"); + FixSingleArrayValue(rawValues, "communityMembers[user][label]"); + FixSingleArrayValue(rawValues, "communityMembers[user][value]"); + + FillTopics(rawValues); + FillRelatedEntities(rawValues); + FillType(rawValues); + + // Convert to project form model + // Normalize project member + var communityMembers = new JArray(); + if (rawValues["communityMembers[role]"] != null && rawValues["communityMembers[user][label]"] != null && rawValues["communityMembers[user][value]"] != null) + { + for (var i = 0; i < rawValues["communityMembers[role]"].Count(); i++) + { + var memberObject = JObject.FromObject( + new + { + role = rawValues["communityMembers[role]"][i], + user = new + { + label = rawValues["communityMembers[user][label]"][i], + value = rawValues["communityMembers[user][value]"][i] + } + } + ); + + communityMembers.Add(memberObject); + } + } + rawValues.Remove("communityMembers[role]"); + rawValues.Remove("communityMembers[user][label]"); + rawValues.Remove("communityMembers[user][value]"); + rawValues["communityMembers"] = communityMembers; + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/EventRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/EventRawValueConverter.cs new file mode 100644 index 000000000..1c0ae077c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/EventRawValueConverter.cs @@ -0,0 +1,75 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class EventRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("__RequestVerificationToken"); + rawValues.Remove("publishOptions"); + rawValues.Remove("typeOptions[label]"); + rawValues.Remove("typeOptions[value]"); + rawValues.Remove("menu1"); + rawValues.Remove("menu2"); + rawValues.Remove("menu3"); + rawValues.Remove("menu4"); + + FixSingleArrayValue(rawValues, "roles"); + FixSingleArrayValue(rawValues, "topics[label]"); + FixSingleArrayValue(rawValues, "topics[value]"); + FixSingleArrayValue(rawValues, "attendees[label]"); + FixSingleArrayValue(rawValues, "attendees[value]"); + FixSingleArrayValue(rawValues, "eventOrganizers[label]"); + FixSingleArrayValue(rawValues, "eventOrganizers[value]"); + + FillTopics(rawValues); + + var attendees = new JArray(); + if (rawValues["attendees[label]"] != null && rawValues["attendees[value]"] != null) + { + for (var i = 0; i < rawValues["attendees[label]"].Count(); i++) + { + var organizerObject = JObject.FromObject( + new + { + label = rawValues["attendees[label]"][i], + value = rawValues["attendees[value]"][i] + } + ); + + attendees.Add(organizerObject); + } + } + rawValues.Remove("attendees[label]"); + rawValues.Remove("attendees[value]"); + rawValues["attendees"] = attendees; + + var eventOrganizers = new JArray(); + if (rawValues["eventOrganizers[label]"] != null && rawValues["eventOrganizers[value]"] != null) + { + for (var i = 0; i < rawValues["eventOrganizers[label]"].Count(); i++) + { + var organizerObject = JObject.FromObject( + new + { + label = rawValues["eventOrganizers[label]"][i], + value = rawValues["eventOrganizers[value]"][i] + } + ); + + eventOrganizers.Add(organizerObject); + } + } + rawValues.Remove("eventOrganizers[label]"); + rawValues.Remove("eventOrganizers[value]"); + rawValues["eventOrganizers"] = eventOrganizers; + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/IRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/IRawValueConverter.cs new file mode 100644 index 000000000..86be4e112 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/IRawValueConverter.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + /* + The purpose of the converter is to convert the raw json from the forms into strongly typed form model + */ + public interface IRawValueConverter + { + Task ConvertAsync(JObject rawValues); + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProjectRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProjectRawValueConverter.cs new file mode 100644 index 000000000..fa23fbd42 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProjectRawValueConverter.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class ProjectRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("__RequestVerificationToken"); + rawValues.Remove("publishOptions"); + rawValues.Remove("typeOptions[label]"); + rawValues.Remove("typeOptions[value]"); + rawValues.Remove("valueNames"); + + FixSingleArrayValue(rawValues, "roles"); + FixSingleArrayValue(rawValues, "topics[label]"); + FixSingleArrayValue(rawValues, "topics[value]"); + FixSingleArrayValue(rawValues, "projectMembers[role]"); + FixSingleArrayValue(rawValues, "projectMembers[user][label]"); + FixSingleArrayValue(rawValues, "projectMembers[user][value]"); + + FillTopics(rawValues); + FillType(rawValues); + + // Convert to project form model + // Normalize project member + var projectMembers = new JArray(); + if (rawValues["projectMembers[role]"] != null && rawValues["projectMembers[user][label]"] != null && rawValues["projectMembers[user][value]"] != null) + { + for (var i = 0; i < rawValues["projectMembers[role]"].Count(); i++) + { + var memberObject = JObject.FromObject( + new + { + role = rawValues["projectMembers[role]"][i], + user = new + { + label = rawValues["projectMembers[user][label]"][i], + value = rawValues["projectMembers[user][value]"][i] + } + } + ); + + projectMembers.Add(memberObject); + } + } + rawValues.Remove("projectMembers[role]"); + rawValues.Remove("projectMembers[user][label]"); + rawValues.Remove("projectMembers[user][value]"); + rawValues["projectMembers"] = projectMembers; + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProposalRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProposalRawValueConverter.cs new file mode 100644 index 000000000..449ca58a0 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/ProposalRawValueConverter.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class ProposalRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("__RequestVerificationToken"); + rawValues.Remove("publishOptions"); + rawValues.Remove("typeOptions[label]"); + rawValues.Remove("typeOptions[value]"); + + FixSingleArrayValue(rawValues, "roles"); + FixSingleArrayValue(rawValues, "topics[label]"); + FixSingleArrayValue(rawValues, "topics[value]"); + FixSingleArrayValue(rawValues, "relatedEntities[label]"); + FixSingleArrayValue(rawValues, "relatedEntities[value]"); + + FillTopics(rawValues); + FillRelatedEntities(rawValues); + FillType(rawValues); + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/TopicRawValueConverter.cs b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/TopicRawValueConverter.cs new file mode 100644 index 000000000..8a8849a1a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Services/ValueConverters/TopicRawValueConverter.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatCan.OrchardCore.Radar.FormModels; + +namespace StatCan.OrchardCore.Radar.Services.ValueConverters +{ + public class TopicRawValueConverter : BaseRawValueConverter + { + public override FormModel ConvertFromRawValues(JObject rawValues) + { + rawValues.Remove("roleOptions"); + rawValues.Remove("__RequestVerificationToken"); + + FixSingleArrayValue(rawValues, "roles"); + + return JsonConvert.DeserializeObject(rawValues.ToString()); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Startup.cs b/src/Apps/StatCan.OrchardCore.Radar/Startup.cs new file mode 100644 index 000000000..8182b41d2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Startup.cs @@ -0,0 +1,202 @@ +using System; +using Fluid; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc; +using YesSql.Indexes; +using OrchardCore.Modules; +using OrchardCore.Data.Migration; +using OrchardCore.ResourceManagement; +using OrchardCore.Scripting; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.Liquid; +using StatCan.OrchardCore.Radar.Filters; +using StatCan.OrchardCore.Radar.Migrations; +using StatCan.OrchardCore.Radar.Models; +using StatCan.OrchardCore.Radar.Indexes; +using StatCan.OrchardCore.Radar.Drivers; +using StatCan.OrchardCore.Radar.Services; +using StatCan.OrchardCore.Radar.Services.ValueConverters; +using StatCan.OrchardCore.Radar.Services.ContentConverters; +using StatCan.OrchardCore.Radar.Scripting; +using StatCan.OrchardCore.Radar.Liquid; +using StatCan.OrchardCore.Radar.Handlers; + +namespace StatCan.OrchardCore.Radar +{ + public class Startup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddTransient, ResourceManagementOptionsConfiguration>(); + + services.Configure(options => + { + options.Filters.Add(typeof(ResourceInjectionFilter)); + }); + + services.Configure(options => + { + options.MemberAccessStrategy.Register(); + }); + + services.AddContentPart() + .UseDisplayDriver(); + services.AddSingleton(); + services.AddScoped(); + + services.AddContentPart() + .UseDisplayDriver() + .AddHandler(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddLiquidFilter("is_owner"); + services.AddLiquidFilter("content_update_url"); + services.AddLiquidFilter("parent_contentitem_id"); + services.AddLiquidFilter("remove_unviewable_items"); + services.AddLiquidFilter("list_only"); + + // Value converters + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // Content converters + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + // List view routes + routes.MapAreaControllerRoute( + name: "ProjectListView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "projects", + defaults: new { controller = "List", action = "List" }, + dataTokens: new { type = "Project" } + ); + + routes.MapAreaControllerRoute( + name: "ProposalListView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "proposals", + defaults: new { controller = "List", action = "List" }, + dataTokens: new { type = "Proposal" } + ); + + routes.MapAreaControllerRoute( + name: "EventListView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "events", + defaults: new { controller = "List", action = "List" }, + dataTokens: new { type = "Event" } + ); + + routes.MapAreaControllerRoute( + name: "CommunityListView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "communities", + defaults: new { controller = "List", action = "List" }, + dataTokens: new { type = "Community" } + ); + + // Form view routes + routes.MapAreaControllerRoute( + name: "FormCreateView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "{entityType:regex(^(topics|projects|events|communities|proposals)$)}/create", + defaults: new { controller = "Form", action = "Form" } + ); + + routes.MapAreaControllerRoute( + name: "FormUpdateView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "{entityType:regex(^(topics|projects|events|communities|proposals)$)}/update/{id}", + defaults: new { controller = "Form", action = "Form" } + ); + + // Speical case for delete. Logically it's part the form views. + routes.MapAreaControllerRoute( + name: "ContentDelete", + areaName: "StatCan.OrchardCore.Radar", + pattern: "contents/delete", + defaults: new { controller = "Form", action = "ContentDelete" } + ); + + // Special Cases + routes.MapAreaControllerRoute( + name: "FormContainedCreateView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "{parentType:regex(^(projects|events|communities|proposals)$)}/{parentId}/{childType:regex(^(artifacts)$)}/create", + defaults: new { controller = "Form", action = "FormContained" } + ); + + routes.MapAreaControllerRoute( + name: "FormContainedUpdateView", + areaName: "StatCan.OrchardCore.Radar", + pattern: "{parentType:regex(^(projects|events|communities|proposals)$)}/{parentId}/{childType:regex(^(artifacts)$)}/update/{id}", + defaults: new { controller = "Form", action = "FormContained" } + ); + + // search api endpoints + routes.MapAreaControllerRoute( + name: "ListSearchAPI", + areaName: "StatCan.OrchardCore.Radar", + pattern: "api/radar/list-search", + defaults: new { controller = "List", action = "Search" } + ); + + routes.MapAreaControllerRoute( + name: "GlobalSearchAPI", + areaName: "StatCan.OrchardCore.Radar", + pattern: "api/radar/global-search", + defaults: new { controller = "List", action = "GlobalSearch" } + ); + + routes.MapAreaControllerRoute( + name: "FormTopicSearchAPI", + areaName: "StatCan.OrchardCore.Radar", + pattern: "api/radar/topic-search", + defaults: new { controller = "Form", action = "TopicSearch" } + ); + + routes.MapAreaControllerRoute( + name: "FormUserSearchAPI", + areaName: "StatCan.OrchardCore.Radar", + pattern: "api/radar/user-search", + defaults: new { controller = "Form", action = "UserSearch" } + ); + + routes.MapAreaControllerRoute( + name: "FormEntitySearchAPI", + areaName: "StatCan.OrchardCore.Radar", + pattern: "api/radar/entity-search", + defaults: new { controller = "Form", action = "EntitySearch" } + ); + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/StatCan.OrchardCore.Radar.csproj b/src/Apps/StatCan.OrchardCore.Radar/StatCan.OrchardCore.Radar.csproj new file mode 100644 index 000000000..a30da7cf5 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/StatCan.OrchardCore.Radar.csproj @@ -0,0 +1,70 @@ + + + + $(AspNetCoreTargetFramework) + true + ..\..\..\roslynator.ruleset + + + + + + + + + + + + Never + + + Never + + + Never + + + Never + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.Summary.liquid new file mode 100644 index 000000000..2d9888f44 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.Summary.liquid @@ -0,0 +1,21 @@ +{% assign parentId = Request.Path | parent_contentitem_id %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + + {{ Model.ContentItem | dispay_text | shortcode }} + + + + {% if Model.ContentItem.Published %} + Published + {% else %} + Draft + {% endif %} + + + {{ Model.ContentItem.ModifiedUtc | local | date: "%b %d, %Y" }} + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: true, DisplayMode: "Summary", SuccessUrl: failUrl, FailUrl: failUrl, ParentId: parentId | shape_render}} + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.liquid new file mode 100644 index 000000000..d3314eb24 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Artifact.liquid @@ -0,0 +1,50 @@ +{% assign parentId = Request.Path | parent_contentitem_id %} +{% capture artifactPath %}/{{ Model.ContentItem.Content.AutoroutePart.Path }}{% endcapture %} +{% capture successUrl %}~{{ Request.Path | remove: artifactPath }}{% endcapture %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | display_text | shortcode }}
+
+ {{ Model.ContentItem.Content.Artifact.URL.Text }} +
+
+
+ +
+ + mdi-link + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: true, DisplayMode: "Detail", SuccessUrl: successUrl, FailUrl: failUrl, ParentId: parentId | shape_render}} +
+
+
+ + + + +
+ + mdi-update + +
+ {{ Model.ContentItem.ModifiedUtc | local | date: "%b %d, %Y" }} +
+
+
+
+
+
+
+
+
+
+
+
+{% endzone %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.Summary.liquid new file mode 100644 index 000000000..ba1d028ea --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.Summary.liquid @@ -0,0 +1,17 @@ +{% assign owner = Model.ContentItem.Owner | users_by_id %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + {{ Model.ContentItem | display_text }} + + +
+ mdi-account +
{{ Model.ContentItem.Content.CommunityMember.ContentItems | size | plus: 1 }}
+
+ + {{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }} + {{ Model.ContentItem.Content.Community.Type.TagNames.first | shortcode }} + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Summary", SuccessUrl: "~/communities", FailUrl: failUrl | shape_render}} + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.liquid new file mode 100644 index 000000000..d6d0a4f38 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Community.liquid @@ -0,0 +1,147 @@ +{% assign owner = Model.ContentItem.Owner | users_by_id %} +{% assign type = Model.ContentItem.Content.Community.Type.TagNames | first | shortcode %} +{% assign events = Model.ContentItem.Content.RadarEntityPart.RelatedEntity.LocalizationSets | localization_set | remove_unauthorized_items %} + +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | display_text }}
+
+ {{ Model.ContentItem.Content.RadarEntityPart.Description.Text }} +
+
+
+ +
+ + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Detail", SuccessUrl: "~/communities", FailUrl: failUrl | shape_render}} +
+
+
+ + + + +
+ + mdi-account-plus + +
+ {{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }} +
+
+
+ +
+ + mdi-bookmark + +
+ {{ type }} +
+
+
+ +
+ + mdi-eye + +
+ {{ "StatusLabel" | shape_new: Published: Model.ContentItem.Content.RadarEntityPart.Publish.Value | shape_render }} +
+
+
+
+
+
+ + +
+ {% assign topics = Model.ContentItem.Content.RadarEntityPart.Topics | taxonomy_terms %} + {% for topic in topics %} + {{ topic | shape_build_display: "Summary" | shape_render }} + {% endfor %} +
+
+
+
+
+
+
+
+
+{% endzone %} + +
+
+
+
+ {{ "Members" | t }} +
+
+
{{ Model.ContentItem.Content.CommunityMember.ContentItems | size | plus: 1 }}
+
+
+
+ + + + + + + + + + + + {% assign members = Model.ContentItem.Content.CommunityMember.ContentItems %} + {% for member in members %} + + + {% endfor %} + +
{{ "Name" | t }}{{ "Role" | t }}
{{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }}{{ "Owner" | t }} +
{{ member.DisplayText }} + {{ member.CommunityMember.Role.Text | shortcode }} +
+
+
+ +
+
+
+ {{ "Events" | t }} +
+
+
+ {{ events | size }} +
+
+
+ + + + + + + + + + + {% for item in events %} + {{ item | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Date" | t }}{{ "Organizer" | t }}{{ "Visibility" | t }}
+
+
diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-EntityCard.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-EntityCard.liquid new file mode 100644 index 000000000..6cdee1f38 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-EntityCard.liquid @@ -0,0 +1,46 @@ +{% assign user = User | user_id | users_by_id %} +{% assign userRole = user.RoleNames | first %} +{% assign entities = Queries.RecentActivitiesSQL | query: contentType: Model.ContentItem.Content.EntityCard.Type.Text, role: userRole %} + +{% assign entitiesSize = entities | size %} +{% if entitiesSize > 0 %} + {% assign entities = entities | filter_current_culture | remove_unviewable_items | list_only: limit: 5 %} +{% endif %} + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.Summary.liquid new file mode 100644 index 000000000..0faca75f0 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.Summary.liquid @@ -0,0 +1,21 @@ +{% assign organizer = Model.ContentItem.Content.EventOrganizer.ContentItems | first %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + {{ Model.ContentItem | display_text }} + + +
{{ Model.ContentItem.Content.Event.StartDate.Value | local | date: "%b %d, %Y" }} - {{ Model.ContentItem.Content.Event.EndDate.Value | local | date: "%b %d, %Y" }}
+
{{ Model.ContentItem.Content.Event.StartDate.Value | local | date: "%H:%M %p" }} - {{ Model.ContentItem.Content.Event.EndDate.Value | local | date: "%H:%M %p" }}
+ + {{ organizer.DisplayText }} + + {% if Model.ContentItem.Published %} + {{ "Published" | t }} + {% else %} + {{ "Draft" | t }} + {% endif %} + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Summary", SuccessUrl: "~/events", FailUrl: failUrl | shape_render}} + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.liquid new file mode 100644 index 000000000..7ba21fe9c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Event.liquid @@ -0,0 +1,129 @@ +{% assign organizer = Model.ContentItem.Content.EventOrganizer.ContentItems | first %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | display_text }}
+
+ {{ Model.ContentItem.Content.RadarEntityPart.Description.Text }} +
+
+
+ +
+ + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Detail", SuccessUrl: "~/events", FailUrl: failUrl | shape_render}} +
+
+
+ + + + +
+ + mdi-account-plus + +
+ {{ organizer.DisplayText }} +
+
+
+ +
+ + mdi-update + +
+ {{ Model.ContentItem.ModifiedUtc | local | date: "%b %d, %Y" }} +
+
+
+ +
+ + mdi-calendar + +
+ {{ Model.ContentItem.Content.Event.StartDate.Value | date: "%b %d %H:%M %p, %Y" }} - {{ Model.ContentItem.Content.Event.EndDate.Value | date: "%b %d %H:%M %p, %Y" }} +
+
+
+ +
+ + mdi-eye + +
+ {{ "StatusLabel" | shape_new: Published: Model.ContentItem.Content.RadarEntityPart.Publish.Value | shape_render }} +
+
+
+
+
+
+ + +
+ {% assign topics = Model.ContentItem.Content.RadarEntityPart.Topics | taxonomy_terms %} + {% for topic in topics %} + {{ topic | shape_build_display: "Summary" | shape_render }} + {% endfor %} +
+
+
+
+
+
+
+
+
+{% endzone %} + +
+
+
+
+ {{ "Attendees" | t }} +
+
+
{{Model.ContentItem.Content.Event.Attendees.UserIds | size | plus: 1 }}
+
+
+
+ + + + + + + + + + + + {% assign members = Model.ContentItem.Content.Event.Attendees.UserIds %} + {% for member in members %} + {% assign user = member | users_by_id %} + + + {% endfor %} + +
{{ "Name" | t }}
{{ organizer.DisplayText }}
{{ user.Properties.UserProfile.UserProfile.FirstName.Text }} {{ user.Properties.UserProfile.UserProfile.LastName.Text }} +
+
+
+ +
+ {{ "Workspace" | shape_new: Model: Model.ContentItem.Content.Workspace, ParentItem: Model.ContentItem | shape_render }} +
+
diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPage.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPage.liquid new file mode 100644 index 000000000..b63b5be7f --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPage.liquid @@ -0,0 +1,108 @@ +{% zone "Header" %} +{% assign imagePath = Model.ContentItem.Content.LandingPage.HeaderImage.Paths.first %} +
+
+
+ + + +
+ {{ Model.ContentItem | display_text }} +
+
+
+ + +
+ {{ Model.ContentItem.Content.LandingPage.Description.Text }} +
+
+
+ + +
+ {% for headerItem in Model.ContentItem.Content.HeaderList.ContentItems %} +
+ mdi-menu-right +
{{ headerItem.LandingPageHeaderList.Caption.Text }}
+
+ {% endfor %} +
+
+
+ + + + + +
+
+
+
+
+
+
+{% endzone %} + +{% if User.Identity.IsAuthenticated == true %} +
+ + + +
+ {{ "Recent activities" | t }} +
+
+
+ + {% for activity in Model.ContentItem.Content.Activities.ContentItems %} + + {{ activity | shape_build_display: "Detail" | shape_render }} + + {% endfor %} + +
+
+ +
+ + + +
+ {{ "Recent trends" | t }} +
+
+
+ + {% for trend in Model.ContentItem.Content.Trends.ContentItems %} + + {{ trend| shape_build_display: "Detail" | shape_render }} + + {% endfor %} + +
+
+{% endif %} + +
+ + + {% for footerCard in Model.ContentItem.Content.Footer.ContentItems %} + + {{ footerCard | shape_build_display: "Detail" | shape_render }} + + {% endfor %} + + +
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPageFooterCard.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPageFooterCard.liquid new file mode 100644 index 000000000..95805c361 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-LandingPageFooterCard.liquid @@ -0,0 +1,13 @@ + + +
+ + {{ Model.ContentItem.Content.LandingPageFooterCard.Icon.Text }} + +
+ +
{{ Model.ContentItem.Content.LandingPageFooterCard.Caption.Text }}
+
+
+
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.Summary.liquid new file mode 100644 index 000000000..85bba3fae --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.Summary.liquid @@ -0,0 +1,19 @@ +{% assign owner = Model.ContentItem.Owner | users_by_id %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + {{ Model.ContentItem | display_text }} + + + {{ Model.ContentItem.ModifiedUtc | local | date: "%b %d, %Y" }} + + {{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }} + + {{ "StatusLabel" | shape_new: Published: Model.ContentItem.Content.RadarEntityPart.Publish.Value | shape_render }} + + + {{ Model.ContentItem.CreatedUtc | local | date: "%b %d, %Y" }} + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Summary", SuccessUrl: "~/projects", FailUrl: failUrl | shape_render}} + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.liquid new file mode 100644 index 000000000..42930fd79 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Project.liquid @@ -0,0 +1,120 @@ +{% assign owner = Model.ContentItem.Owner | users_by_id %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | display_text }}
+
+ {{ Model.ContentItem.Content.RadarEntityPart.Description.Text }} +
+
+
+ +
+ + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Detail", SuccessUrl: "~/projects", FailUrl: failUrl | shape_render}} +
+
+
+ + + + +
+ + mdi-account-plus + +
+ {{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }} +
+
+
+ +
+ + mdi-update + +
+ {{ Model.ContentItem.ModifiedUtc | local | date: "%b %d, %Y" }} +
+
+
+ +
+ + mdi-eye + +
+ {{ "StatusLabel" | shape_new: Published: Model.ContentItem.Content.RadarEntityPart.Publish.Value | shape_render }} +
+
+
+
+
+
+ + +
+ {% assign topics = Model.ContentItem.Content.RadarEntityPart.Topics | taxonomy_terms %} + {% for topic in topics %} + {{ topic | shape_build_display: "Summary" | shape_render }} + {% endfor %} +
+
+
+
+
+
+
+
+
+{% endzone %} + +
+
+
+
+ {{ "Contributors" | t }} +
+
+
{{ Model.ContentItem.Content.ProjectMember.ContentItems | size | plus: 1 }}
+
+
+
+ + + + + + + + + + + + {% assign members = Model.ContentItem.Content.ProjectMember.ContentItems %} + {% for member in members %} + + + {% endfor %} + +
{{ "Name" | t }}{{ "Role" | t }}
{{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }}{{ "Owner" | t }} +
{{ member.DisplayText }} + {{ member.ProjectMember.Role.Text | shortcode }} +
+
+
+ +
+ {{ "Workspace" | shape_new: Model: Model.ContentItem.Content.Workspace, ParentItem: Model.ContentItem | shape_render }} +
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.Summary.liquid new file mode 100644 index 000000000..ee4d0897a --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.Summary.liquid @@ -0,0 +1,18 @@ +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + {{ Model.ContentItem | display_text }} + + + {{ Model.ContentItem.CreatedUtc | local | date: "%b %d, %Y" }} + + + {% if Model.ContentItem.Published %} + {{ "Published" | t }} + {% else %} + {{ "Draft" | t }} + {% endif %} + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Summary", SuccessUrl: "~/proposals", FailUrl: failUrl | shape_render}} + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.liquid new file mode 100644 index 000000000..bdf79fbdb --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Proposal.liquid @@ -0,0 +1,105 @@ +{% assign owner = Model.ContentItem.Owner | users_by_id %} +{% assign communities = Model.ContentItem.Content.RadarEntityPart.RelatedEntity.LocalizationSets | localization_set | remove_unauthorized_items %} +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | display_text }}
+
+ {{ Model.ContentItem.Content.RadarEntityPart.Description.Text }} +
+
+
+ +
+ + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Detail", SuccessUrl: "~/proposals", FailUrl: failUrl | shape_render}} +
+
+
+ + + + +
+ + mdi-account-plus + +
+ {{ owner.Properties.UserProfile.UserProfile.FirstName.Text }} {{ owner.Properties.UserProfile.UserProfile.LastName.Text }} +
+
+
+ +
+ + mdi-eye + +
+ {{ "StatusLabel" | shape_new: Published: Model.ContentItem.Content.RadarEntityPart.Publish.Value | shape_render }} +
+
+
+
+
+
+ + +
+ {% assign topics = Model.ContentItem.Content.RadarEntityPart.Topics | taxonomy_terms %} + {% for topic in topics %} + {{ topic | shape_build_display: "Summary" | shape_render }} + {% endfor %} +
+
+
+
+
+
+
+
+
+{% endzone %} + +
+
+
+
+ {{ "Affected Communities" | t }} +
+
+
{{ communities | size }}
+
+
+
+ + + + + + + + + + + {% for item in communities %} + {{ item | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Members" | t }}{{ "Owner" | t }}{{ "Type" | t }}
+
+
+ +
+ {{ "Workspace" | shape_new: Model: Model.ContentItem.Content.Workspace, ParentItem: Model.ContentItem | shape_render }} +
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.Summary.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.Summary.liquid new file mode 100644 index 000000000..b4a99d477 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.Summary.liquid @@ -0,0 +1,13 @@ +{% assign canView = Model.ContentItem | user_can_view %} +{% if canView %} + +
+ + mdi-pound + +
+ {{ Model.ContentItem | display_text | shortcode }} +
+
+
+{% endif %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.liquid new file mode 100644 index 000000000..771c120c2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-Topic.liquid @@ -0,0 +1,118 @@ +{% script src:"/StatCan.OrchardCore.Radar/js/table.js", at:"Head" %} +{% script name:"Radar-vue-components", at:"Foot" %} + +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.ContentItem | DisplayText | shortcode }}
+
+ {{ Model.ContentItem.Content.Topic.Description.Text | shortcode }} +
+
+
+ +
+ + + {{ "OptionsButton" | shape_new: ContentItem: Model.ContentItem, Relative: false, DisplayMode: "Detail", SuccessUrl: "~/topics", FailUrl: failUrl | shape_render}} +
+
+
+
+
+
+
+
+
+{% endzone %} + +{% assign entities = Queries.TaxonomyEntitiesSQL | query: type: "Topics", id: Model.ContentItem.ContentItemId | remove_unauthorized_items | filter_current_culture %} + +{% assign projects = entities | where: "ContentType", "Project" %} +{% assign communities = entities | where: "ContentType", "Community" %} +{% assign proposals = entities | where: "ContentType", "Proposal" %} +{% assign events = entities | where: "ContentType", "Event" %} + +
+
+ + + + + + + + + + + + + + {% for project in projects %} + {{ project | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Updated" | t }}{{ "Registrar" | t }}{{ "Visibility" | t }}{{ "Created" | t }}
+
+ + + + + + + + + + + + {% for event in events %} + {{ event | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Date" | t }}{{ "Organizer" | t }}{{ "Visibility" | t }}
+
+ + + + + + + + + + + + {% for community in communities %} + {{ community | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Members" | t }}{{ "Owner" | t }}{{ "Type" | t }}
+
+ + + + + + + + + + + {% for proposal in proposals %} + {{ proposal | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Created" | t }}{{ "Visibility" | t }}
+
+
+
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Content-TrendingCard.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-TrendingCard.liquid new file mode 100644 index 000000000..f653a6111 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Content-TrendingCard.liquid @@ -0,0 +1,87 @@ +{% assign entities = Queries.AllTaxonomiesSQL | query: type: Model.ContentItem.Content.TrendingCard.Type.Text | first %} +{% assign trending_up = Queries.TaxonomyTrendingUpSQL | query: type: Model.ContentItem.Content.TrendingCard.Type.Text %} +{% assign trending_down = Queries.TaxonomyTrendingDownSQL | query: type: Model.ContentItem.Content.TrendingCard.Type.Text %} + +{% assign terms = entities.ContentItem.Content.TaxonomyPart.Terms | remove_unauthorized_items %} + +
+ +
+
+
+ + mdi-trending-up + +
+ {% capture _ %}{% increment countTrendingUp %}{% endcapture %} + {% for tup in trending_up %} + {% assign e = terms | where: "ContentItemId", tup.TermContentItemId | first %} + {% if e %} + {% capture count %}{% increment countTrendingUp %}{% endcapture %} +
+ {% assign trendUpTopics = trendUpTopics | append: e.ContentItemId %} + {{ e | shape_build_display: "Summary" | shape_render }} +
+ {% endif %} + {% if count == "3" %} + {% break %} + {% endif %} + + {% endfor %} +
+
+
+ + mdi-trending-down + +
+ {% capture _ %}{% increment countTrendingDown %}{% endcapture %} + {% for tdown in trending_down %} + {% assign e = terms | where: "ContentItemId", tdown.TermContentItemId | first %} + + {% assign exist = false %} + {% if trendUpTopics contains e.ContentItemId %} + {% assign exist = true %} + {% endif %} + + {% if exist == false %} + {% capture count %}{% increment countTrendingDown %}{% endcapture %} +
+ {{ e | shape_build_display: "Summary" | shape_render }} +
+ {% endif %} + + {% if count == "3" %} + {% break %} + {% endif %} + + {% endfor %} +
+
+
+
+ + + {{ Model.ContentItem.Content.TrendingCard.ButtonText.Text }} + + +
diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/DeleteButton.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/DeleteButton.cshtml new file mode 100644 index 000000000..0840f0eb4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/DeleteButton.cshtml @@ -0,0 +1,8 @@ +
+ + + + + + +
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Form/Form.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/Form/Form.cshtml new file mode 100644 index 000000000..acb5f2292 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Form/Form.cshtml @@ -0,0 +1,9 @@ +@model dynamic + +@await DisplayAsync(Model.FormValue) + +@await DisplayAsync(await New.InitialValueLoader()) // This to make sure that the values are ready before the form gets rendered + +@await DisplayAsync(await New.OptionsLoader()) // Same idea this is to make sure the options are ready + +@await DisplayAsync(Model.Content) // values and options can then be loaded correctly into the form \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/GlobalSearch.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/GlobalSearch.cshtml new file mode 100644 index 000000000..31747b39f --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/GlobalSearch.cshtml @@ -0,0 +1,47 @@ +@using Newtonsoft.Json; + + + +@{ + var searchURL = Url.Action("GlobalSearch", "List", new { area = "StatCan.OrchardCore.Radar" }); +} + +@* + Use a json object to pass in the localized text and icon +*@ +@{ + var titles = new Dictionary(); + + // Project + titles.Add("Project", new Dictionary + { + { "title", T["Projects"] }, + { "icon", "mdi-flask-outline" } + }); + + // Community + titles.Add("Community", new Dictionary + { + { "title", T["Communities"] }, + { "icon", "mdi-account-multiple" } + }); + + // Event + titles.Add("Event", new Dictionary + { + { "title", T["Events"] }, + { "icon", "mdi-calendar" } + }); + + // Proposal + titles.Add("Proposal", new Dictionary + { + { "title", T["Proposals"] }, + { "icon", "mdi-alert-circle-outline" } + }); + + // Convert to json + var titlesJson = JsonConvert.SerializeObject(titles); +} + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/InitialValueLoader.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/InitialValueLoader.liquid new file mode 100644 index 000000000..114e31cdb --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/InitialValueLoader.liquid @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/List/Community.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Community.cshtml new file mode 100644 index 000000000..60900cfa8 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Community.cshtml @@ -0,0 +1,71 @@ +@model IEnumerable + + + + +
+
+ + + +
+
@T["Communities"]
+
+
+ @Model.Count() +
+
+
+
+ + mdi-magnify + +
+ + + +
+ + + mdi-close + + +
+
+ + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + @foreach (var shape in Model) + { + @await DisplayAsync(shape) + + + + } + +
@T["Name"]@T["Members"]@T["Owner"]@T["Type"]
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/List/Event.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Event.cshtml new file mode 100644 index 000000000..fc51bbe0d --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Event.cshtml @@ -0,0 +1,69 @@ +@model IEnumerable + + + + +
+
+ + + +
+
@T["Events"]
+
+
+ @Model.Count() +
+
+
+
+ + mdi-magnify + +
+ + + +
+ + + mdi-close + + +
+
+ + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + @foreach (var shape in Model) + { + @await DisplayAsync(shape) + + } + +
@T["Name"]@T["Date"]@T["Organizer"]@T["Visibility"]
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/List/Project.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Project.cshtml new file mode 100644 index 000000000..a0d9fd292 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Project.cshtml @@ -0,0 +1,69 @@ +@model IEnumerable + + + + +
+
+ + + +
+
@T["Projects"]
+
+
+ @Model.Count() +
+
+
+
+ + mdi-magnify + +
+ + + +
+ + + mdi-close + + +
+
+ + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + @foreach (var shape in Model) + { + @await DisplayAsync(shape) + } + +
@T["Name"]@T["Updated"]@T["Registrar"]@T["Visibility"]@T["Created"]
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/List/Proposal.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Proposal.cshtml new file mode 100644 index 000000000..2e839bb66 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/List/Proposal.cshtml @@ -0,0 +1,68 @@ +@model IEnumerable + + + + +
+
+ + + +
+
@T["Proposals"]
+
+
+ @Model.Count() +
+
+
+
+ + mdi-magnify + +
+ + + +
+ + + mdi-close + + +
+
+ + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + @foreach (var shape in Model) + { + @await DisplayAsync(shape) + + } + +
@T["Name"]@T["Created"]@T["Visibility"]
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/LocalizationButton.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/LocalizationButton.liquid new file mode 100644 index 000000000..43284b7fa --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/LocalizationButton.liquid @@ -0,0 +1,13 @@ +{% comment %} Assumption that we only support two cultures: EN and FR {% endcomment %} + +{% capture targetCultureShortName %}{% if Culture.Name contains 'en' %}FR{% else %}EN{% endif %}{% endcapture %} +{% capture targetCultureLongName %}{% if Culture.Name contains 'en' %}Français{% else %}English{% endif %}{% endcapture %} +{% capture targetCultureUrl %}{{ targetCultureShortName | downcase | switch_culture_url }}{% endcapture %} + + + {{"Switch to" | t}} {{targetCultureLongName}} + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Menu-MainMenu.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Menu-MainMenu.liquid new file mode 100644 index 000000000..a662af16f --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Menu-MainMenu.liquid @@ -0,0 +1,3 @@ +{% for item in Model.Items %} + {{ "RadarMenu" | shape_new: Model: item.Properties.ContentItem | shape_render }} +{% endfor %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/NotFound.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/NotFound.liquid new file mode 100644 index 000000000..3f4d23063 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/NotFound.liquid @@ -0,0 +1 @@ +

{{ "The page you are looking for is not found" | t }}

\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsButton.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsButton.liquid new file mode 100644 index 000000000..98f4e5906 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsButton.liquid @@ -0,0 +1,40 @@ +{% assign contentItem = Model.Properties.ContentItem %} +{% assign isOwner = contentItem | is_owner %} + +{% assign successUrl = Model.Properties.SuccessUrl | href %} +{% assign failUrl = Model.Properties.FailUrl | href %} + +{% capture mode %} + {% if Model.Properties.DisplayMode == "Detail" %} + details-view-options-button + {% elsif Model.Properties.DisplayMode == "Summary" %} + list-view-options-button + {% endif %} +{% endcapture %} +{% if isOwner == true %} + + + + + + + +

{{ "Update" | t }}

+
+
+
+ + + + + {{ "DeleteButton" | shape_new: ContentItem: contentItem, SuccessUrl: successUrl, FailUrl: failUrl, ParentId: Model.Properties.ParentId | shape_render }} + + + +
+
+{% endif %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsLoader.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsLoader.liquid new file mode 100644 index 000000000..15ffca44e --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/OptionsLoader.liquid @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/RadarFormPart.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarFormPart.liquid new file mode 100644 index 000000000..f418e4520 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarFormPart.liquid @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenu.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenu.liquid new file mode 100644 index 000000000..76e1078dc --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenu.liquid @@ -0,0 +1,22 @@ +{% assign menu = Model.Properties.Model %} + +{% assign canView = menu | user_can_view %} + +{% if canView %} +{% assign menuItems = menu.Content.MenuItemsListPart.MenuItems | remove_unauthorized_items %} + + + + + + {% for item in menuItems %} + {{ "RadarMenuItem" | shape_new: Model: item | shape_render }} + + {% endfor %} + + +{% endif %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenuItem.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenuItem.liquid new file mode 100644 index 000000000..4d4934e36 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/RadarMenuItem.liquid @@ -0,0 +1,14 @@ +{% assign menuItem = Model.Properties.Model %} + + + + {{ menuItem.Content.CommonMenuItemPart.IconName.Text }} + + + + + {{ menuItem.Content.LinkMenuItemPart.Name }} + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/StatusLabel.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/StatusLabel.liquid new file mode 100644 index 000000000..537f8353e --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/StatusLabel.liquid @@ -0,0 +1,5 @@ +{% if Model.Properties.Published %} + {{ "Published" | t }} + {% else %} + {{ "Draft" | t }} + {% endif %} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Term-Topic.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Term-Topic.liquid new file mode 100644 index 000000000..47eddec05 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Term-Topic.liquid @@ -0,0 +1,89 @@ +{% script src:"~/StatCan.OrchardCore.Radar/js/table.min.js", debug_src:"~/StatCan.OrchardCore.Radar/js/table.js", at:"Head" %} + +{% zone "Header" %} +
+
+ + + +
+
{{ Model.Properties.TaxonomyContentItem | display_text }}
+
+
{{ Model.Properties.TaxonomyContentItem.Content.TaxonomyPart.Terms | remove_unauthorized_items | size }}
+
+
+
+ + mdi-magnify + + + + + mdi-close + + +
+
+ + + + +
+
+
+
+
+
+
+{% endzone %} + +
+ + + + + + + + + + + + + + {% for item in Model.Items %} + {% assign canView = item.Properties.TermContentItem | user_can_view %} + {% if canView %} + + {{ item | shape_render }} + + {% endif %} + {% endfor %} + +
{{ "Name" | t }}{{ "Popularity" | t }}{{ "Project" | t }}{{ "Community" | t }}{{ "Event" | t }}{{ "Proposal" | t }}
+
+ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/TermContentItem-Topic.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/TermContentItem-Topic.liquid new file mode 100644 index 000000000..c3f9d3c24 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/TermContentItem-Topic.liquid @@ -0,0 +1,83 @@ +{% assign entities = Queries.TaxonomyEntitiesSQL | query: type: "Topics", id: Model.Properties.TermContentItem.ContentItemId | remove_unauthorized_items | filter_current_culture %} + +{% assign projectCount = entities | where: "ContentType", "Project" | size %} +{% assign communityCount = entities | where: "ContentType", "Community" | size %} +{% assign proposalCount = entities | where: "ContentType", "Proposal" | size %} +{% assign eventCount = entities | where: "ContentType", "Event" | size %} + +{% assign popularity = projectCount | plus: communityCount | plus: proposalCount | plus: eventCount %} + + +{% capture failUrl %}~{{ Request.Path }}{% endcapture %} + + + + {{ Model.Properties.TermContentItem | display_text | shortcode }} + + +
+ mdi-star-outline +
{{ popularity }}
+
+ + +
+ mdi-flask-outline +
{{ projectCount }}
+
+ + +
+ mdi-account-multiple +
{{ communityCount }}
+
+ + +
+ mdi-calendar +
{{ eventCount }}
+
+ + +
+ mdi-alert-circle-outline +
{{ proposalCount }}
+
+ +{{ "OptionsButton" | shape_new: ContentItem: Model.Properties.TermContentItem, Relative: false, DisplayMode: "Summary", SuccessUrl: "~/topics", FailUrl: failUrl | shape_render}} + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/TermItem-Topic.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/TermItem-Topic.liquid new file mode 100644 index 000000000..8ed421919 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/TermItem-Topic.liquid @@ -0,0 +1,3 @@ +{% shape_clear_alternates Model %} +{% shape_type Model "TermContentItem__Topic" %} +{{ Model | shape_render }} \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/UserMenuButton.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/UserMenuButton.cshtml new file mode 100644 index 000000000..6a5c1b2a9 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/UserMenuButton.cshtml @@ -0,0 +1,74 @@ +@using Microsoft.AspNetCore.Identity +@using Microsoft.Extensions.Options +@using OrchardCore.Admin +@using OrchardCore.Entities +@using OrchardCore.Settings +@using OrchardCore.Users +@using OrchardCore.Users.Models +@using Microsoft.AspNetCore.Authorization +@inject ISiteService SiteService +@inject SignInManager SignInManager +@inject IOptions AdminOptions +@inject Microsoft.AspNetCore.Authorization.IAuthorizationService authorizationService + +@{ + var allowChangeEmail = (await SiteService.GetSiteSettingsAsync()).As().AllowChangeEmail; + var canAccessAdmin = await authorizationService.AuthorizeAsync(User, OrchardCore.Admin.Permissions.AccessAdminPanel); + var externalAuthenticationSchemes = await SignInManager.GetExternalAuthenticationSchemesAsync(); +} +@if (User.Identity.IsAuthenticated) +{ + + + + + + mdi-account-circle + + + @T["Signed in as"] @User.Identity.Name + + + + @if (canAccessAdmin) + { + + + mdi-monitor-dashboard + + @T["Dashboard"] + + + } + +
+ + + mdi-logout + + @T["Log off"] + +
+
+
+} +else +{ + + + @T["Log in"] + +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-AppBar.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-AppBar.liquid new file mode 100644 index 000000000..9275dfcab --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-AppBar.liquid @@ -0,0 +1,56 @@ +{% assign elevation = Model.ContentItem.Content.AppBar.Elevation.Value %} +{% assign extensionHeight = Model.ContentItem.Content.AppBar.ExtensionHeight.Value %} +{% assign scrollThreshold = Model.ContentItem.Content.AppBar.ScrollThreshold.Value %} +{% assign height = Model.ContentItem.Content.AppBar.Height.Value %} +{% assign width = Model.ContentItem.Content.AppBar.Width.Value %} + + + + + + logo + + + +
+ {{ Model.ContentItem | display_text | shortcode }} +
+
+ + + + + {% if Culture.Name == "en" %} + {% shape "menu", alias: "alias:main-menu-en", cache_id: "main-menu-en", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu-en" %} + {% endif %} + {% if Culture.Name == "fr" %} + {% shape "menu", alias: "alias:main-menu-fr", cache_id: "main-menu-fr", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu-fr" %} + {% endif %} + + + {% if User.Identity.IsAuthenticated == true %} + {{ "GlobalSearch" | shape_new | shape_render }} + {% endif %} + +
+ {% assign user = User | user_id | users_by_id %} + {{ user.Properties.UserProfile.UserProfile.FirstName.Text }} + {{ user.Properties.UserProfile.UserProfile.LastName.Text }} +
+
+ {{ "UserMenuButton" | shape_new | shape_render }} + {{ "LocalizationButton" | shape_new | shape_render }} +
+
diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-Footer.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-Footer.liquid new file mode 100644 index 000000000..efcc4a85c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-Footer.liquid @@ -0,0 +1,45 @@ +{% assign menuAlias = "alias:main-menu-" | append: Culture.Name %} +{% assign menu = Content["alias:main-menu-en"] %} + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-NavigationDrawer.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-NavigationDrawer.liquid new file mode 100644 index 000000000..a12a1e6f9 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Widget-NavigationDrawer.liquid @@ -0,0 +1,40 @@ +{% assign color = Model.ContentItem.Content.NavigationDrawer.Color.Text %} +{% assign mobileBreakpoint = Model.ContentItem.Content.NavigationDrawer.MobileBreakpoint.Value %} +{% assign overlayColor = Model.ContentItem.Content.NavigationDrawer.OverlayColor.Text %} +{% assign overlayOpacity = Model.ContentItem.Content.NavigationDrawer.OverlayOpacity.Value %} +{% assign height = Model.ContentItem.Content.NavigationDrawer.Height.Value %} +{% assign width = Model.ContentItem.Content.NavigationDrawer.Width.Value %} + + + {% assign menu = Content["alias:main-menu"] %} + + + + mdi-home + + + + + {{ "Home" | t }} + + + + + + {% for section in menu.Content.MenuItemsListPart.MenuItems %} + + {% for item in section.MenuItemsListPart.MenuItems %} + {{ "RadarMenuItem" | shape_new: Model: item | shape_render }} + {% endfor %} + {% endfor %} + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/Workspace.liquid b/src/Apps/StatCan.OrchardCore.Radar/Views/Workspace.liquid new file mode 100644 index 000000000..463718bea --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/Workspace.liquid @@ -0,0 +1,35 @@ +{% assign artifacts = Model.Properties.Model.ContentItems | remove_unauthorized_items | remove_unviewable_items %} +{% assign parentLink = Model.Properties.ParentItem | display_url %} +{% assign canAddArtifact = Model.Properties.ParentItem | is_owner %} + +
+
+ {{ "Workspace" | t }} +
+
+
{{ artifacts | size }}
+
+ {% if canAddArtifact %} + + + mdi-plus-thick + + + {% endif %} +
+
+ + + + + + + + + + {% for artifact in artifacts %} + {{ artifact | shape_build_display: "Summary" | shape_render }} + {% endfor %} + +
{{ "Name" | t }}{{ "Status" | t }}{{ "Updated" | t }}
+
\ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/Views/_ViewImports.cshtml b/src/Apps/StatCan.OrchardCore.Radar/Views/_ViewImports.cshtml new file mode 100644 index 000000000..ae902a644 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/Views/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.ResourceManagement +@addTagHelper *, OrchardCore.DisplayManagement + +@using OrchardCore.DisplayManagement +@using OrchardCore.DisplayManagement.Views +@using OrchardCore.Media; + diff --git a/src/Apps/StatCan.OrchardCore.Radar/package-lock.json b/src/Apps/StatCan.OrchardCore.Radar/package-lock.json new file mode 100644 index 000000000..9364a990e --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "StatCan.OrchardCore.Radar", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } + }, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/package.json b/src/Apps/StatCan.OrchardCore.Radar/package.json new file mode 100644 index 000000000..cd574b219 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/src/Apps/StatCan.OrchardCore.Radar/readme.md b/src/Apps/StatCan.OrchardCore.Radar/readme.md new file mode 100644 index 000000000..399503bf3 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/readme.md @@ -0,0 +1,62 @@ +# Radar +This module implements the Digital Radar. + +## Content Types +### Radar entities +The 4 entities listed below are considered primary entities in radar. They are the top level entities being maintained by radar. All primary entities has RadarEntityPart attached to it. + +- Project + +- Event + +- Community + +- Opportunity + +### Taxonomies +The only taxonomy in radar is Topic. They can be attached to primary entities. Topics has its own CRUD views. + +### Secondary entities +Secondary entities are content items that must be attached to primary entities. For example, Artifact is an example of secondary entitiy. Artifact has its own CRUD views but they are dependent on primary entities. + +## Localization +### Primary and secondary entities +Localization of primary and secondary entities are done using the ContentLocalizationPart. When creating new entities from the frontend form, localized versions are created automatically. Note that tranlsations are not done automatically. The form simply creates other localized content items with text in the current localization. + +### Taxonomies +Localization of Topic is done using shortcodes. + +### Other labels +For labels like button labels, the localization is done using .po files. + +## Forms +The approach taken to implement the forms is to create one form content item for each radar entities and load the values at runtime. + +### Scripting +Radar has various script methods related to forms. They help with CRUD operations of radar entities and validation. + +### Frontend +The forms are implemented using vue forms. The forms have their own controller actions defined in `FormController.cs`. The form values and options are filled at runtime in `RadarFormPartDisplayDriver`. The display driver fetches the content item values according to the route. The format of the routes are defined in `Startup.cs`. In the views the initial values are loaded in the `InitialValueLoader.liquid` shape. The shape populates the `formValues` object on the global `window` object with the values. After this shape has been rendered, the form values can then be accessed like `window.formValues.[fieldName]`. The `data` object in the vue forms should refer to this `formValues` object. + +### Backend + +#### FormValueProvider +This is used to populate the form with values of an existing content item. For example, if you want to update experiment1 then the `FormValueProvider` will fetch the values of experiment1 and store them in `RadarFormPart`. + +#### FormOptionsProvider +Same process to `FormValueProvider` + +#### ValueConverters +The `ValueConverters` are used to extract and parse the raw form values into an intermediate state. In other words, they convert raw form values into a json object with only the relevent information. This json object can then be used for validation. + +#### ContentConverters +The `ContentConverters` are used to convert the json object created by `ValueConverters` into Orchard content items. Results converted by `ContentConverters` can be directly saved using `IContentManager`. + +## Custom Permissions +Radar has some custom permissions on top of Orchard and `ContentPermissionPart`. Please refer to `RadarPermissionPartDisplayDriver.cs` for more detail. + +## Custom Api endpoints +Radar has some custom endpoints such as async global search, async user search etc... . Please refer to `FormController.cs` and `ListController.cs` for more detail. + +## Custom Vue Components +Radar has some custom view components for global search and tabs. diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada-light.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada-light.svg new file mode 100644 index 000000000..e2570ea53 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada-light.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada.svg new file mode 100644 index 000000000..936bd1156 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/canada.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/di-logo.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/di-logo.svg new file mode 100644 index 000000000..1a439baed --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/di-logo.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc-light.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc-light.svg new file mode 100644 index 000000000..c1bee5f5c --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc-light.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc.svg new file mode 100644 index 000000000..5e59977b4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/goc.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_communities.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_communities.svg new file mode 100644 index 000000000..40e7b3801 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_communities.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_events.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_events.svg new file mode 100644 index 000000000..26562ce11 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_events.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_experiments.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_experiments.svg new file mode 100644 index 000000000..015470954 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_experiments.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_opportunities.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_opportunities.svg new file mode 100644 index 000000000..ee5b28de4 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_opportunities.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_topics.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_topics.svg new file mode 100644 index 000000000..3f09ba7b7 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/icon_topics.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/radar-icon-white.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/radar-icon-white.svg new file mode 100644 index 000000000..a9cd6f7d2 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/radar-icon-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples-white.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples-white.svg new file mode 100644 index 000000000..ea7ef8adb --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples.svg b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples.svg new file mode 100644 index 000000000..890c02968 --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/images/ripples.svg @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.css b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.css new file mode 100644 index 000000000..7098e7ffc --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.css @@ -0,0 +1,778 @@ +/* +** NOTE: This file is generated by Gulp and should not be edited directly! +** Any changes made directly to this file will be overwritten next time its asset group is processed by Gulp. +*/ + +.appbar { + background-color: #252561 !important; +} + +.menu-button { + background-color: #f9663b !important; + width: 38px !important; + height: 38px !important; + margin-left: 1rem; + color: #ffffff !important; +} + +.app-bar-username-container { + margin-left: 1rem; +} + +.app-bar-buttons-container { + margin-right: 0.5rem; + display: flex; +} + +a { + text-decoration: none; +} + +.details-section { + padding-top: 3rem; +} + +.header { + padding: 3rem 7rem; + background: transparent linear-gradient(180deg, rgba(37, 37, 97, 0.38), rgba(249, 102, 59, 0.38)) 0% no-repeat; +} + +.details-name { + text-align: left; + font-weight: 300; + color: #252561; + overflow-wrap: anywhere; +} + +.details-description { + margin-top: 1rem; + color: #151537; +} + +.topics-container { + margin-top: 2rem !important; +} + +.icon-cell { + display: flex; + align-items: center; +} + +.icon-text { + margin-left: 1rem; +} + +.topics-list-container { + display: flex; +} + +.options-button { + width: 3rem !important; + height: 3rem !important; +} + +.details-view-options-button { + background-color: #ffffff !important; +} + +.workspace-bubble { + margin-left: 2rem; + background-color: #efeff4; + border: 2px solid #252561; + border-radius: 50%; + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 23px; +} + +.workspace-bubble-text { + color: #151537; +} + +.workspace-add-button { + width: 3rem !important; + height: 3rem !important; + margin-left: 1rem; + background-color: #ffffff !important; +} + +.workspace-button-icon { + font-size: 40px !important; +} + +.workspace-add-button:hover { + background-color: #fdc5b5 !important; +} + +.member-table { + width: 50%; +} + +.attendees-table { + width: 20%; +} + +.table-title-container { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.sections-container { + padding: 0 7rem 0 7rem; +} + +.section { + margin-bottom: 2rem; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.details-main-icon { + font-size: 9rem !important; + color: #252561 !important; +} + +.details-attribute-icon { + font-size: 2.5rem !important; + color: #000000 !important; +} +.footer-bottom { + background: #252561; + border-top: 3px solid #f9663b; + display: flex; + justify-content: space-between; + padding: 3rem 6rem 2rem 6rem; +} + +.ripple { + background-color: #252561; + background-size: cover; + background-repeat: no-repeat; + background-position: center bottom -623px; +} + +.footer { + background-color: #252561; +} + +.menus-container { + display: flex; + flex-grow: 5; + justify-content: space-evenly; +} + +.menu-container { + display: flex; + flex-direction: column; +} + +.menu-title { + color: #f9663b; + margin-bottom: 1rem; +} + +.zigzag-footer { + position: relative; + padding: 0; + background: #ffffff; +} + +.zigzag-footer::after { + content: " "; + display: block; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 32px; + background: linear-gradient(-145deg, #ffffff 16px, transparent 0), linear-gradient(145deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.ripple-container { + padding: 5rem 2rem 5rem 2rem; + display: flex; + width: 100%; + background-image: url("images/ripples.svg"); +} + +.menu-link { + margin-bottom: 1rem; +} + +.menu-link:hover { + text-decoration: underline; +} + +.logo-container { + display: flex; + flex-direction: column; + justify-content: center; + flex-grow: 1; +} + +.logo { + display: flex; + flex-direction: column; + justify-content: end; + align-items: flex-start; + flex-grow: 1; +} + +.logo-text { + margin-left: 0.5rem; +} +.global-search-container { + display: flex; + align-items: center; + display: flex; + height: 2.2rem; + padding-left: 6px; + width: fit-content; + border-radius: 4px; + background-color: #00000070; + position: relative; +} + +.global-search { + color: #ffffff; +} + +input:focus { + outline: none; +} + +.dropdown-list { + position: absolute; + width: 100%; + max-height: 500px; + margin-top: 4px; + overflow-y: auto; + background: #ffffff; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + top: 40px; + padding: 1rem; +} + +.dropdown-item { + color: #000000; + padding: 1rem 0 1rem 0; +} + +.dropdown-item:hover { + background-color: #f1f1f1; +} + +.result-caption { + color: #252561; + display: flex; +} +.page-header-image { + margin-left: auto; +} + +.icon-primary { + color: #252561 !important; +} + +.icon-secondary { + color: #f9663b !important; +} +.card-background { + background-color: #efeff4; +} + +.trend-card-body { + display: flex; + flex-direction: column; + margin-top: 1.25rem; +} + +.trend-card-body-icon { + font-size: 9rem !important; +} + +.trend-card-body-container { + margin-left: 0.25rem; +} + +.trend-card-body-topic-container { + margin-bottom: 0.5rem; +} + +.trend-container { + display: flex; + align-items: center; +} + +.card { + display: flex; + flex-direction: column; + height: 100%; +} + +.card-title { + background-color: #252561; + display: flex; + flex-direction: column; +} + +.card-title:hover { + background-color: #3B3B8A; +} + +.card-title-caption { + display: flex; + margin: 1.25rem 0 2.5rem 1.25rem; +} + +.card-icon { + font-size: 4rem !important; + margin-right: 0.75rem; +} + +.card-body { + padding: 1rem 0 0 6rem; + margin-bottom: 1.5rem; + flex-grow: 1; +} + +.card-button { + margin-bottom: 1rem; + margin-left: 1rem; + color: #f9663b !important; +} + +.card-item-link { + color: #000000 !important; +} + +.card-item-link:hover { + color: #252561 !important; + text-decoration: underline; +} + +.card-item-link-container { + margin-bottom: 0.75rem; +} + +.card-button-link { + width: fit-content; +} + +.card-title { + display: flex; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-dark { + background: linear-gradient(-40deg, #efeff4 16px, transparent 0), linear-gradient(40deg, #efeff4 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} +.max-wdith { + max-width: 1200px; +} + +.light-font { + font-weight: lighter; + color: #000000de; + opacity: 1; +} + +.header-list { + display: flex; + flex-direction: column; +} + +.list-item { + display: flex; + margin-bottom: 8px; +} + +.list-item-text { + font-size: 1.125rem; +} + +.name { + font-size: 3.5rem; + color: #252561; + font-weight: 300; + white-space: nowrap; +} + +.description { + font-size: 25px; + font-weight: 300; + margin: auto; + line-height: normal; +} + +.list { + display: flex; + justify-content: center; +} + +.section-title { + color: #252561; +} + +.section-background { + background: #efeff461; +} + +.front-footer { + background-color: #252561 !important; + padding: 1.75rem 0 2rem 0; +} + +.front-footer:hover { + background-color: #3B3B8A !important; +} + +.front-footer-card-icon { + font-size: 7.5rem !important; + margin-bottom: 0.5rem; +} + +.front-footer-card-title { + margin-bottom: 0.25rem; +} + +.header-footer { + display: flex; + justify-content: space-between; + padding: 0 10rem 0 10rem; + margin-top: 1.75rem; + flex-wrap: wrap; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +.rippleFront { + background-color: #cfece9; + background-image: url("images/ripples-white.svg"), linear-gradient(#252561, rgba(249, 102, 59, 0.7)); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + padding: 12rem 0rem 3rem 0rem; +} + +a { + text-decoration: none; +} +.list-header { + padding: 3rem 13rem 4rem 13rem; + background: transparent linear-gradient(180deg, rgba(37, 37, 97, 0.38), rgba(249, 102, 59, 0.38)) 0% no-repeat; +} + +.details-section { + padding-top: 3rem; +} + +.list-title { + color: #252561; +} + +.title-container { + display: flex; + align-items: center; +} + +.bubble { + margin-left: 2rem; + background-color: #efeff4; + border: 2px solid #252561; + border-radius: 50%; + width: 5rem; + height: 5rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 38px; +} + +.bubble-text { + color: #151537; +} + +.table-container { + padding: 0 2rem 0 2rem; + width: 80%; + margin: auto; +} + +.search-container { + margin: 1rem 0 1.5rem 0; + display: flex; + background: #f6fcfb; + align-items: center; + border-bottom: 2px solid #252561; + display: flex; + height: 2.2rem; + padding-left: 1rem; + width: fit-content; +} + +.search-icon { + margin-right: 1.25rem; + color: #000000 !important; +} + +input:focus { + outline: none; +} + +.zigzag-container { + position: relative; + width: 100%; +} + +.zigzag { + content: " "; + display: block; + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: 32px; +} + +.zigzag-white { + background: linear-gradient(-40deg, #ffffff 16px, transparent 0), linear-gradient(40deg, #ffffff 16px, transparent 0); + background-position: left bottom; + background-repeat: repeat-x; + background-size: 32px 32px; +} + +a { + text-decoration: none; + color: black !important; +} + +.list-view-options-button { + background-color: rgba(255, 255, 255, 0); +} +.table-row { + background-color: #feece6; + height: 48px; + border-bottom: 1px solid #fdc5b5; + cursor: pointer; + overflow-wrap: anywhere; +} + +.table-row:hover { + background: #fdc5b5; +} + +.name-col { + text-align: left; + padding-left: 1rem; +} + +.table-col { + text-align: center; +} + +.table-icon-cell { + display: flex; + align-items: center; + justify-content: center; +} + +a { + text-decoration: none; + color: #000000 !important; +} +.table-header { + border-bottom: 2px solid #252561; +} + +.table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; +} + +.name-col { + text-align: left; + padding-left: 1rem; +} + +th:hover { + cursor: pointer; +} +.topic { + background: #f9663b 0% 0% no-repeat padding-box; + border-radius: 75px; + display: inline-block; + padding: 2px 10px 0px 9px; + margin-right: 0.9rem; + color: #FFFFFF; + width: fit-content; + display: flex; + border: #f9663b solid 2px; +} + +.topic:hover { + border: #252561 solid 2px; +} + +.topic-tag-icon { + margin-right: 0.25rem; + font-size: 1.25rem; +} + +.label { + overflow: hidden; + text-overflow: ellipsis; +} + +a { + text-decoration: none; +} + +.form-topic-field { + margin-bottom: 0.6rem; +} + +.form-topic-field-remove-button:hover { + cursor: pointer; +} + +.multiselect__option--highlight { + background-color: #252561 !important; +} + +.multiselect__option--highlight::after { + background-color: #252561 !important; +} +.tabs { + display: flex; +} + +.selected { + color: #212121; + background-color: #fdc5b5; + border-bottom: 2px solid #252561; +} + +.selected:hover { + background-color: #fdc5b5; +} + +.unselected { + color: #212121; + background-color: #feece6; + border-bottom: 2px solid #ffffff; +} + +.unselected:hover { + background-color: #fdc5b5; +} + +.tab-button { + padding: 0.5rem 1rem; +} + +.tab-header-container { + display: flex; +} + +.count { + border-radius: 11px; + display: flex; + height: 22px; + min-width: 22px; + margin-left: 1rem; + padding: 0 0.4rem 0 0.4rem; +} + +.count-selected { + background: #efeff4; + border: 1px solid #252561; +} + +.count-unselected { + background-color: #eeeeee; + border: 1px solid #9c9c9c; +} + +.count-label { + margin: auto; + text-align: center; + font-size: 12px; +} + +.count-label-Selected { + color: #227069; +} + +.count-label-Unselected { + color: #212121; +} +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcGJhci5zY3NzIiwiY29sb3JzLmNzcyIsImRldGFpbHMtdmlldy5zY3NzIiwiY29sb3JzLnNjc3MiLCJmb290ZXIuc2NzcyIsImdsb2JhbC1zZWFyY2guc2NzcyIsImljb24uc2NzcyIsImxhbmRpbmctcGFnZS1jYXJkLnNjc3MiLCJsYW5kaW5nLXBhZ2Uuc2NzcyIsImxpc3Qtdmlldy5zY3NzIiwidGFibGUtcm93LnNjc3MiLCJ0YWJsZS5zY3NzIiwidG9waWMuc2NzcyIsInZ1ZS1jb21wb25lbnRzLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFFQTtFQUNJOzs7QUFHSjtFQUNJO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQUdKO0VBQ0k7OztBQUdKO0VBQ0k7RUFDQTs7O0FBR0o7RUFDRTs7QUN4QkY7QUNFQTtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7OztBQUtGO0VBQ0U7RUFDQTtFQUNBLE9DaEJRO0VEaUJSOzs7QUFHRjtFQUNFO0VBQ0EsT0NyQmlCOzs7QUR3Qm5CO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0Esa0JDL0NXO0VEZ0RYO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRSxPQ2pFaUI7OztBRG9FbkI7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTtFQUVBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7O0FFdElGO0VBQ0UsWURIUTtFQ0lSO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFLGtCRFhRO0VDWVI7RUFDQTtFQUNBOzs7QUFHRjtFQUNFLGtCRGxCUTs7O0FDcUJWO0VBQ0k7RUFDQTtFQUNBOzs7QUFHSjtFQUNJO0VBQ0E7OztBQUdKO0VBQ0ksT0Q5QlE7RUMrQlI7OztBQUdKO0VBQ0U7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0k7OztBQUdKO0VBQ0k7RUFDQTtFQUNBO0VBQ0E7OztBQUdKO0VBQ0k7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0o7RUFDRTs7QUN2RkY7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRSxPRjlDUTtFRStDUjs7QUM3Q0Y7RUFDRTs7O0FBR0Y7RUFDSTs7O0FBR0o7RUFDSTs7QUNUSjtFQUNFLGtCSkdXOzs7QUlBYjtFQUNFO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRSxrQkpwQ1E7RUlxQ1I7RUFDQTs7O0FBR0Y7RUFDRSxrQkp4Q1c7OztBSTJDYjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUVBO0VBQ0E7RUFDQTs7QUMxR0Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0EsT0w1QlE7RUs2QlI7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFLE9MOUNROzs7QUtpRFY7RUFDRSxZTDNDb0I7OztBSzhDdEI7RUFDRTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBRUE7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTs7QUM5R0Y7RUFDRTtFQUNBOzs7QUFLRjtFQUNFOzs7QUFHRjtFQUNFLE9OZFE7OztBTWlCVjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQSxrQk5sQlc7RU1tQlg7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFLE9OcENpQjs7O0FNdUNuQjtFQUNFO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBRUE7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7O0FDOUZGO0VBQ0Usa0JQRW1CO0VPRG5CO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFLFlQUG1COzs7QU9VckI7RUFDRTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOztBQzdCRjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFHRjtFQUNFOztBQ2hCRjtFQUNJO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0o7RUFDSTs7O0FBR0o7RUFDSTtFQUNBOzs7QUFHSjtFQUNJO0VBQ0E7OztBQUdKO0VBQ0k7OztBQUdKO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7O0FDM0NGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQSxrQlZKbUI7RVVLbkI7OztBQUdGO0VBQ0Usa0JWVG1COzs7QVVZckI7RUFDRTtFQUNBLGtCVmJtQjtFVWNuQjs7O0FBR0Y7RUFDRSxrQlZuQm1COzs7QVVzQnJCO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFFRjtFQUNFLFlWckNXO0VVc0NYOzs7QUFFRjtFQUNFO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTtFQUNBOzs7QUFHRjtFQUNFOzs7QUFHRjtFQUNFIiwiZmlsZSI6InJhZGFyLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIkBpbXBvcnQgXCJjb2xvcnNcIjtcclxuXHJcbi5hcHBiYXIge1xyXG4gICAgYmFja2dyb3VuZC1jb2xvcjogJHByaW1hcnkgIWltcG9ydGFudDtcclxufVxyXG5cclxuLm1lbnUtYnV0dG9uIHtcclxuICAgIGJhY2tncm91bmQtY29sb3I6ICRzZWNvbmRhcnkgIWltcG9ydGFudDtcclxuICAgIHdpZHRoOiAzOHB4ICFpbXBvcnRhbnQ7XHJcbiAgICBoZWlnaHQ6IDM4cHggIWltcG9ydGFudDtcclxuICAgIG1hcmdpbi1sZWZ0OiAxcmVtO1xyXG4gICAgY29sb3I6ICNmZmZmZmYgIWltcG9ydGFudDtcclxufVxyXG5cclxuLmFwcC1iYXItdXNlcm5hbWUtY29udGFpbmVyIHtcclxuICAgIG1hcmdpbi1sZWZ0OiAxcmVtO1xyXG59XHJcblxyXG4uYXBwLWJhci1idXR0b25zLWNvbnRhaW5lciB7XHJcbiAgICBtYXJnaW4tcmlnaHQ6IDAuNXJlbTtcclxuICAgIGRpc3BsYXk6IGZsZXg7XHJcbn1cclxuXHJcbmEge1xyXG4gIHRleHQtZGVjb3JhdGlvbjogbm9uZTtcclxufSIsbnVsbCwiQGltcG9ydCBcImNvbG9yc1wiO1xuXG4uZGV0YWlscy1zZWN0aW9uIHtcbiAgcGFkZGluZy10b3A6IDNyZW07XG59XG5cbi5oZWFkZXIge1xuICBwYWRkaW5nOiAzcmVtIDdyZW07XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50XG4gICAgbGluZWFyLWdyYWRpZW50KDE4MGRlZywgcmdiYSgkcHJpbWFyeSwgMC4zOCksIHJnYmEoJHNlY29uZGFyeSwgMC4zOCkpIDAlXG4gICAgbm8tcmVwZWF0O1xufVxuXG4uZGV0YWlscy1uYW1lIHtcbiAgdGV4dC1hbGlnbjogbGVmdDtcbiAgZm9udC13ZWlnaHQ6IDMwMDtcbiAgY29sb3I6ICRwcmltYXJ5O1xuICBvdmVyZmxvdy13cmFwOiBhbnl3aGVyZTtcbn1cblxuLmRldGFpbHMtZGVzY3JpcHRpb24ge1xuICBtYXJnaW4tdG9wOiAxcmVtO1xuICBjb2xvcjogJHByaW1hcnktdmFyaWFudDE7XG59XG5cbi50b3BpY3MtY29udGFpbmVyIHtcbiAgbWFyZ2luLXRvcDogMnJlbSAhaW1wb3J0YW50O1xufVxuXG4uaWNvbi1jZWxsIHtcbiAgZGlzcGxheTogZmxleDtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbn1cblxuLmljb24tdGV4dCB7XG4gIG1hcmdpbi1sZWZ0OiAxcmVtO1xufVxuXG4udG9waWNzLWxpc3QtY29udGFpbmVyIHtcbiAgZGlzcGxheTogZmxleDtcbn1cblxuLm9wdGlvbnMtYnV0dG9uIHtcbiAgd2lkdGg6IDNyZW0gIWltcG9ydGFudDtcbiAgaGVpZ2h0OiAzcmVtICFpbXBvcnRhbnQ7XG59XG5cbi5kZXRhaWxzLXZpZXctb3B0aW9ucy1idXR0b24ge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmZmZmICFpbXBvcnRhbnQ7XG59XG5cbi53b3Jrc3BhY2UtYnViYmxlIHtcbiAgbWFyZ2luLWxlZnQ6IDJyZW07XG4gIGJhY2tncm91bmQtY29sb3I6ICRiYWNrZ3JvdW5kO1xuICBib3JkZXI6IDJweCBzb2xpZCAkcHJpbWFyeTtcbiAgYm9yZGVyLXJhZGl1czogNTAlO1xuICB3aWR0aDogM3JlbTtcbiAgaGVpZ2h0OiAzcmVtO1xuICBkaXNwbGF5OiBmbGV4O1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgZm9udC13ZWlnaHQ6IDQwMDtcbiAgZm9udC1zaXplOiAyM3B4O1xufVxuXG4ud29ya3NwYWNlLWJ1YmJsZS10ZXh0IHtcbiAgY29sb3I6ICRwcmltYXJ5LXZhcmlhbnQxO1xufVxuXG4ud29ya3NwYWNlLWFkZC1idXR0b24ge1xuICB3aWR0aDogM3JlbSAhaW1wb3J0YW50O1xuICBoZWlnaHQ6IDNyZW0gIWltcG9ydGFudDtcbiAgbWFyZ2luLWxlZnQ6IDFyZW07XG4gIGJhY2tncm91bmQtY29sb3I6ICNmZmZmZmYgIWltcG9ydGFudDtcbn1cblxuLndvcmtzcGFjZS1idXR0b24taWNvbiB7XG4gIGZvbnQtc2l6ZTogNDBweCAhaW1wb3J0YW50O1xufVxuXG4ud29ya3NwYWNlLWFkZC1idXR0b246aG92ZXIge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAkc2Vjb25kYXJ5LXZhcmlhbnQxICFpbXBvcnRhbnQ7XG59XG5cbi5tZW1iZXItdGFibGUge1xuICB3aWR0aDogNTAlO1xufVxuXG4uYXR0ZW5kZWVzLXRhYmxlIHtcbiAgd2lkdGg6IDIwJTtcbn1cblxuLnRhYmxlLXRpdGxlLWNvbnRhaW5lciB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIG1hcmdpbi1ib3R0b206IDFyZW07XG59XG5cbi5zZWN0aW9ucy1jb250YWluZXIge1xuICBwYWRkaW5nOiAwIDdyZW0gMCA3cmVtO1xufVxuXG4uc2VjdGlvbiB7XG4gIG1hcmdpbi1ib3R0b206IDJyZW07XG59XG5cbi56aWd6YWcge1xuICBjb250ZW50OiBcIiBcIjtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgYm90dG9tOiAwcHg7XG4gIGxlZnQ6IDBweDtcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMzJweDtcbn1cblxuLnppZ3phZy1jb250YWluZXIge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHdpZHRoOiAxMDAlO1xufVxuXG4uemlnemFnLXdoaXRlIHtcbiAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KC00MGRlZywgI2ZmZmZmZiAxNnB4LCB0cmFuc3BhcmVudCAwKSxcbiAgICBsaW5lYXItZ3JhZGllbnQoNDBkZWcsICNmZmZmZmYgMTZweCwgdHJhbnNwYXJlbnQgMCk7XG4gIGJhY2tncm91bmQtcG9zaXRpb246IGxlZnQgYm90dG9tO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogcmVwZWF0LXg7XG4gIGJhY2tncm91bmQtc2l6ZTogMzJweCAzMnB4O1xufVxuXG4uZGV0YWlscy1tYWluLWljb24ge1xuICBmb250LXNpemU6IDlyZW0gIWltcG9ydGFudDtcbiAgY29sb3I6ICRwcmltYXJ5ICFpbXBvcnRhbnQ7XG59XG5cbi5kZXRhaWxzLWF0dHJpYnV0ZS1pY29uIHtcbiAgZm9udC1zaXplOiAyLjVyZW0gIWltcG9ydGFudDtcbiAgY29sb3I6ICMwMDAwMDAgIWltcG9ydGFudDtcbn1cbiIsIiRwcmltYXJ5OiAjMjUyNTYxO1xyXG4kcHJpbWFyeS12YXJpYW50MTogIzE1MTUzNztcclxuJGNhcmQtaG92ZXI6ICMzQjNCOEE7XHJcbiRzZWNvbmRhcnk6ICNmOTY2M2I7XHJcbiRzZWNvbmRhcnktdmFyaWFudDE6ICNmZGM1YjU7XHJcbiRzZWNvbmRhcnktdmFyaWFudDI6ICNmZWVjZTY7XHJcbiRiYWNrZ3JvdW5kOiAjZWZlZmY0O1xyXG4kYmFja2dyb3VuZC12YXJpYW50MTogI2VmZWZmNDYxOyIsIkBpbXBvcnQgXCJjb2xvcnNcIjtcclxuXHJcbi5mb290ZXItYm90dG9tIHtcclxuICBiYWNrZ3JvdW5kOiAkcHJpbWFyeTtcclxuICBib3JkZXItdG9wOiAzcHggc29saWQgJHNlY29uZGFyeTtcclxuICBkaXNwbGF5OiBmbGV4O1xyXG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjtcclxuICBwYWRkaW5nOiAzcmVtIDZyZW0gMnJlbSA2cmVtO1xyXG59XHJcblxyXG4ucmlwcGxlIHtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkcHJpbWFyeTtcclxuICBiYWNrZ3JvdW5kLXNpemU6IGNvdmVyO1xyXG4gIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XHJcbiAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyIGJvdHRvbSAtNjIzcHg7XHJcbn1cclxuXHJcbi5mb290ZXIge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICRwcmltYXJ5O1xyXG59XHJcblxyXG4ubWVudXMtY29udGFpbmVyIHtcclxuICAgIGRpc3BsYXk6IGZsZXg7XHJcbiAgICBmbGV4LWdyb3c6IDU7XHJcbiAgICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWV2ZW5seTtcclxufVxyXG5cclxuLm1lbnUtY29udGFpbmVyIHtcclxuICAgIGRpc3BsYXk6IGZsZXg7XHJcbiAgICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xyXG59XHJcblxyXG4ubWVudS10aXRsZSB7XHJcbiAgICBjb2xvcjogJHNlY29uZGFyeTtcclxuICAgIG1hcmdpbi1ib3R0b206IDFyZW07XHJcbn1cclxuXHJcbi56aWd6YWctZm9vdGVyIHtcclxuICBwb3NpdGlvbjogcmVsYXRpdmU7XHJcbiAgcGFkZGluZzogMDtcclxuICBiYWNrZ3JvdW5kOiAjZmZmZmZmO1xyXG59XHJcblxyXG4uemlnemFnLWZvb3Rlcjo6YWZ0ZXIge1xyXG4gIGNvbnRlbnQ6IFwiIFwiO1xyXG4gIGRpc3BsYXk6IGJsb2NrO1xyXG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcclxuICB0b3A6IDBweDtcclxuICBsZWZ0OiAwcHg7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbiAgaGVpZ2h0OiAzMnB4O1xyXG4gIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCgtMTQ1ZGVnLCAjZmZmZmZmIDE2cHgsIHRyYW5zcGFyZW50IDApLFxyXG4gICAgbGluZWFyLWdyYWRpZW50KDE0NWRlZywgI2ZmZmZmZiAxNnB4LCB0cmFuc3BhcmVudCAwKTtcclxuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBsZWZ0IGJvdHRvbTtcclxuICBiYWNrZ3JvdW5kLXJlcGVhdDogcmVwZWF0LXg7XHJcbiAgYmFja2dyb3VuZC1zaXplOiAzMnB4IDMycHg7XHJcbn1cclxuXHJcbi5yaXBwbGUtY29udGFpbmVyIHtcclxuICBwYWRkaW5nOiA1cmVtIDJyZW0gNXJlbSAycmVtO1xyXG4gIGRpc3BsYXk6IGZsZXg7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKFwiaW1hZ2VzL3JpcHBsZXMuc3ZnXCIpO1xyXG59XHJcblxyXG4ubWVudS1saW5rIHtcclxuICBtYXJnaW4tYm90dG9tOiAxcmVtO1xyXG59XHJcblxyXG4ubWVudS1saW5rOmhvdmVyIHtcclxuICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lO1xyXG59XHJcblxyXG4ubG9nby1jb250YWluZXIge1xyXG4gICAgZGlzcGxheTogZmxleDtcclxuICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XHJcbiAgICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcclxuICAgIGZsZXgtZ3JvdzogMTtcclxufVxyXG5cclxuLmxvZ28ge1xyXG4gICAgZGlzcGxheTogZmxleDtcclxuICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XHJcbiAgICBqdXN0aWZ5LWNvbnRlbnQ6IGVuZDtcclxuICAgIGFsaWduLWl0ZW1zOiBmbGV4LXN0YXJ0O1xyXG4gICAgZmxleC1ncm93OiAxXHJcbn1cclxuXHJcbi5sb2dvLXRleHQge1xyXG4gIG1hcmdpbi1sZWZ0OiAwLjVyZW07XHJcbn1cclxuIiwiQGltcG9ydCBcImNvbG9yc1wiO1xyXG5cclxuLmdsb2JhbC1zZWFyY2gtY29udGFpbmVyIHtcclxuICBkaXNwbGF5OiBmbGV4O1xyXG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBoZWlnaHQ6IDIuMnJlbTtcclxuICBwYWRkaW5nLWxlZnQ6IDZweDtcclxuICB3aWR0aDogZml0LWNvbnRlbnQ7XHJcbiAgYm9yZGVyLXJhZGl1czogNHB4O1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICMwMDAwMDA3MDtcclxuICBwb3NpdGlvbjogcmVsYXRpdmU7XHJcbn1cclxuXHJcbi5nbG9iYWwtc2VhcmNoIHtcclxuICBjb2xvcjogI2ZmZmZmZjtcclxufVxyXG5cclxuaW5wdXQ6Zm9jdXMge1xyXG4gIG91dGxpbmU6IG5vbmU7XHJcbn1cclxuXHJcbi5kcm9wZG93bi1saXN0IHtcclxuICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbiAgbWF4LWhlaWdodDogNTAwcHg7XHJcbiAgbWFyZ2luLXRvcDogNHB4O1xyXG4gIG92ZXJmbG93LXk6IGF1dG87XHJcbiAgYmFja2dyb3VuZDogI2ZmZmZmZjtcclxuICBib3gtc2hhZG93OiAwIDEwcHggMTVweCAtM3B4IHJnYmEoMCwgMCwgMCwgMC4xKSxcclxuICAgIDAgNHB4IDZweCAtMnB4IHJnYmEoMCwgMCwgMCwgMC4wNSk7XHJcbiAgYm9yZGVyLXJhZGl1czogOHB4O1xyXG4gIHRvcDogNDBweDtcclxuICBwYWRkaW5nOiAxcmVtO1xyXG59XHJcblxyXG4uZHJvcGRvd24taXRlbSB7XHJcbiAgY29sb3I6ICMwMDAwMDA7XHJcbiAgcGFkZGluZzogMXJlbSAwIDFyZW0gMDtcclxufVxyXG5cclxuLmRyb3Bkb3duLWl0ZW06aG92ZXIge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICNmMWYxZjE7XHJcbn1cclxuXHJcbi5yZXN1bHQtY2FwdGlvbiB7XHJcbiAgY29sb3I6ICRwcmltYXJ5O1xyXG4gIGRpc3BsYXk6IGZsZXg7XHJcbn1cclxuIiwiQGltcG9ydCBcImNvbG9yc1wiO1xyXG5cclxuLnBhZ2UtaGVhZGVyLWltYWdlIHtcclxuICBtYXJnaW4tbGVmdDogYXV0bztcclxufVxyXG5cclxuLmljb24tcHJpbWFyeSB7XHJcbiAgICBjb2xvcjogJHByaW1hcnkgIWltcG9ydGFudDtcclxufVxyXG5cclxuLmljb24tc2Vjb25kYXJ5IHtcclxuICAgIGNvbG9yOiAkc2Vjb25kYXJ5ICFpbXBvcnRhbnQ7XHJcbn0iLCJAaW1wb3J0IFwiY29sb3JzXCI7XHJcblxyXG4uY2FyZC1iYWNrZ3JvdW5kIHtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkYmFja2dyb3VuZDtcclxufVxyXG5cclxuLnRyZW5kLWNhcmQtYm9keSB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xyXG4gIG1hcmdpbi10b3A6IDEuMjVyZW07XHJcbn1cclxuXHJcbi50cmVuZC1jYXJkLWJvZHktaWNvbiB7XHJcbiAgZm9udC1zaXplOiA5cmVtICFpbXBvcnRhbnQ7XHJcbn1cclxuXHJcbi50cmVuZC1jYXJkLWJvZHktY29udGFpbmVyIHtcclxuICBtYXJnaW4tbGVmdDogMC4yNXJlbTtcclxufVxyXG5cclxuLnRyZW5kLWNhcmQtYm9keS10b3BpYy1jb250YWluZXIge1xyXG4gIG1hcmdpbi1ib3R0b206IDAuNXJlbTtcclxufVxyXG5cclxuLnRyZW5kLWNvbnRhaW5lciB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBhbGlnbi1pdGVtczogY2VudGVyO1xyXG59XHJcblxyXG4uY2FyZCB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xyXG4gIGhlaWdodDogMTAwJTtcclxufVxyXG5cclxuLmNhcmQtdGl0bGUge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICRwcmltYXJ5O1xyXG4gIGRpc3BsYXk6IGZsZXg7XHJcbiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcclxufVxyXG5cclxuLmNhcmQtdGl0bGU6aG92ZXIge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICRjYXJkLWhvdmVyO1xyXG59XHJcblxyXG4uY2FyZC10aXRsZS1jYXB0aW9uIHtcclxuICBkaXNwbGF5OiBmbGV4O1xyXG4gIG1hcmdpbjogMS4yNXJlbSAwIDIuNXJlbSAxLjI1cmVtO1xyXG59XHJcblxyXG4uY2FyZC1pY29uIHtcclxuICBmb250LXNpemU6IDRyZW0gIWltcG9ydGFudDtcclxuICBtYXJnaW4tcmlnaHQ6IDAuNzVyZW07XHJcbn1cclxuXHJcbi5jYXJkLWJvZHkge1xyXG4gIHBhZGRpbmc6IDFyZW0gMCAwIDZyZW07XHJcbiAgbWFyZ2luLWJvdHRvbTogMS41cmVtO1xyXG4gIGZsZXgtZ3JvdzogMTtcclxufVxyXG5cclxuLmNhcmQtYnV0dG9uIHtcclxuICBtYXJnaW4tYm90dG9tOiAxcmVtO1xyXG4gIG1hcmdpbi1sZWZ0OiAxcmVtO1xyXG4gIGNvbG9yOiAkc2Vjb25kYXJ5ICFpbXBvcnRhbnQ7XHJcbn1cclxuXHJcbi5jYXJkLWl0ZW0tbGluayB7XHJcbiAgY29sb3I6ICMwMDAwMDAgIWltcG9ydGFudDtcclxufVxyXG5cclxuLmNhcmQtaXRlbS1saW5rOmhvdmVyIHtcclxuICBjb2xvcjogJHByaW1hcnkgIWltcG9ydGFudDtcclxuICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTtcclxufVxyXG5cclxuLmNhcmQtaXRlbS1saW5rLWNvbnRhaW5lciB7XHJcbiAgbWFyZ2luLWJvdHRvbTogMC43NXJlbTtcclxufVxyXG5cclxuLmNhcmQtYnV0dG9uLWxpbmsge1xyXG4gIHdpZHRoOiBmaXQtY29udGVudDtcclxufVxyXG5cclxuLmNhcmQtdGl0bGUge1xyXG4gIGRpc3BsYXk6IGZsZXg7XHJcbn1cclxuXHJcbi56aWd6YWctY29udGFpbmVyIHtcclxuICBwb3NpdGlvbjogcmVsYXRpdmU7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbn1cclxuXHJcbi56aWd6YWcge1xyXG4gIGNvbnRlbnQ6IFwiIFwiO1xyXG4gIGRpc3BsYXk6IGJsb2NrO1xyXG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcclxuICBib3R0b206IDBweDtcclxuICBsZWZ0OiAwcHg7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbiAgaGVpZ2h0OiAzMnB4O1xyXG59XHJcblxyXG4uemlnemFnLWRhcmsge1xyXG4gIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCgtNDBkZWcsICRiYWNrZ3JvdW5kIDE2cHgsIHRyYW5zcGFyZW50IDApLFxyXG4gICAgbGluZWFyLWdyYWRpZW50KDQwZGVnLCAkYmFja2dyb3VuZCAxNnB4LCB0cmFuc3BhcmVudCAwKTtcclxuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBsZWZ0IGJvdHRvbTtcclxuICBiYWNrZ3JvdW5kLXJlcGVhdDogcmVwZWF0LXg7XHJcbiAgYmFja2dyb3VuZC1zaXplOiAzMnB4IDMycHg7XHJcbn1cclxuIiwiQGltcG9ydCBcImNvbG9yc1wiO1xyXG5cclxuLm1heC13ZGl0aCB7XHJcbiAgbWF4LXdpZHRoOiAxMjAwcHg7XHJcbn1cclxuXHJcbi5saWdodC1mb250IHtcclxuICBmb250LXdlaWdodDogbGlnaHRlcjtcclxuICBjb2xvcjogIzAwMDAwMGRlO1xyXG4gIG9wYWNpdHk6IDE7XHJcbn1cclxuXHJcbi5oZWFkZXItbGlzdCB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xyXG59XHJcblxyXG4ubGlzdC1pdGVtIHtcclxuICBkaXNwbGF5OiBmbGV4O1xyXG4gIG1hcmdpbi1ib3R0b206IDhweDtcclxufVxyXG5cclxuLmxpc3QtaXRlbS10ZXh0IHtcclxuICBmb250LXNpemU6IDEuMTI1cmVtO1xyXG59XHJcblxyXG4ubmFtZSB7XHJcbiAgZm9udC1zaXplOiAzLjVyZW07XHJcbiAgY29sb3I6ICRwcmltYXJ5O1xyXG4gIGZvbnQtd2VpZ2h0OiAzMDA7XHJcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcclxufVxyXG5cclxuLmRlc2NyaXB0aW9uIHtcclxuICBmb250LXNpemU6IDI1cHg7XHJcbiAgZm9udC13ZWlnaHQ6IDMwMDtcclxuICBtYXJnaW46IGF1dG87XHJcbiAgbGluZS1oZWlnaHQ6IG5vcm1hbDtcclxufVxyXG5cclxuLmxpc3Qge1xyXG4gIGRpc3BsYXk6IGZsZXg7XHJcbiAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XHJcbn1cclxuXHJcbi5zZWN0aW9uLXRpdGxlIHtcclxuICBjb2xvcjogJHByaW1hcnk7XHJcbn1cclxuXHJcbi5zZWN0aW9uLWJhY2tncm91bmQge1xyXG4gIGJhY2tncm91bmQ6ICRiYWNrZ3JvdW5kLXZhcmlhbnQxO1xyXG59XHJcblxyXG4uZnJvbnQtZm9vdGVyIHtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkcHJpbWFyeSAhaW1wb3J0YW50O1xyXG4gIHBhZGRpbmc6IDEuNzVyZW0gMCAycmVtIDA7XHJcbn1cclxuXHJcbi5mcm9udC1mb290ZXI6aG92ZXIge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICRjYXJkLWhvdmVyICFpbXBvcnRhbnQ7XHJcbn1cclxuXHJcbi5mcm9udC1mb290ZXItY2FyZC1pY29uIHtcclxuICBmb250LXNpemU6IDcuNXJlbSAhaW1wb3J0YW50O1xyXG4gIG1hcmdpbi1ib3R0b206IDAuNXJlbTtcclxufVxyXG5cclxuLmZyb250LWZvb3Rlci1jYXJkLXRpdGxlIHtcclxuICBtYXJnaW4tYm90dG9tOiAwLjI1cmVtO1xyXG59XHJcblxyXG4uaGVhZGVyLWZvb3RlciB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47XHJcbiAgcGFkZGluZzogMCAxMHJlbSAwIDEwcmVtO1xyXG4gIG1hcmdpbi10b3A6IDEuNzVyZW07XHJcbiAgZmxleC13cmFwOiB3cmFwO1xyXG59XHJcblxyXG4uemlnemFnLWNvbnRhaW5lciB7XHJcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xyXG4gIHdpZHRoOiAxMDAlO1xyXG59XHJcblxyXG4uemlnemFnIHtcclxuICBjb250ZW50OiBcIiBcIjtcclxuICBkaXNwbGF5OiBibG9jaztcclxuICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgYm90dG9tOiAwcHg7XHJcbiAgbGVmdDogMHB4O1xyXG4gIHdpZHRoOiAxMDAlO1xyXG4gIGhlaWdodDogMzJweDtcclxufVxyXG5cclxuLnppZ3phZy13aGl0ZSB7XHJcbiAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KC00MGRlZywgI2ZmZmZmZiAxNnB4LCB0cmFuc3BhcmVudCAwKSxcclxuICAgIGxpbmVhci1ncmFkaWVudCg0MGRlZywgI2ZmZmZmZiAxNnB4LCB0cmFuc3BhcmVudCAwKTtcclxuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBsZWZ0IGJvdHRvbTtcclxuICBiYWNrZ3JvdW5kLXJlcGVhdDogcmVwZWF0LXg7XHJcbiAgYmFja2dyb3VuZC1zaXplOiAzMnB4IDMycHg7XHJcbn1cclxuXHJcbi5yaXBwbGVGcm9udCB7XHJcbiAgYmFja2dyb3VuZC1jb2xvcjogI2NmZWNlOTtcclxuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoXCJpbWFnZXMvcmlwcGxlcy13aGl0ZS5zdmdcIiksIGxpbmVhci1ncmFkaWVudChyZ2JhKCRwcmltYXJ5LCAxKSwgcmdiYSgkc2Vjb25kYXJ5LCAwLjcpKTtcclxuICBiYWNrZ3JvdW5kLXNpemU6IGNvdmVyO1xyXG4gIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XHJcbiAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyO1xyXG4gIHBhZGRpbmc6IDEycmVtIDByZW0gM3JlbSAwcmVtO1xyXG59XHJcblxyXG5hIHtcclxuICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XHJcbn1cclxuIiwiQGltcG9ydCBcImNvbG9yc1wiO1xuXG4ubGlzdC1oZWFkZXIge1xuICBwYWRkaW5nOiAzcmVtIDEzcmVtIDRyZW0gMTNyZW07XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50XG4gICAgbGluZWFyLWdyYWRpZW50KDE4MGRlZywgcmdiYSgkcHJpbWFyeSwgMC4zOCksIHJnYmEoJHNlY29uZGFyeSwgMC4zOCkpIDAlXG4gICAgbm8tcmVwZWF0O1xufVxuXG4uZGV0YWlscy1zZWN0aW9uIHtcbiAgcGFkZGluZy10b3A6IDNyZW07XG59XG5cbi5saXN0LXRpdGxlIHtcbiAgY29sb3I6ICRwcmltYXJ5O1xufVxuXG4udGl0bGUtY29udGFpbmVyIHtcbiAgZGlzcGxheTogZmxleDtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbn1cblxuLmJ1YmJsZSB7XG4gIG1hcmdpbi1sZWZ0OiAycmVtO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAkYmFja2dyb3VuZDtcbiAgYm9yZGVyOiAycHggc29saWQgJHByaW1hcnk7XG4gIGJvcmRlci1yYWRpdXM6IDUwJTtcbiAgd2lkdGg6IDVyZW07XG4gIGhlaWdodDogNXJlbTtcbiAgZGlzcGxheTogZmxleDtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gIGZvbnQtd2VpZ2h0OiA0MDA7XG4gIGZvbnQtc2l6ZTogMzhweDtcbn1cblxuLmJ1YmJsZS10ZXh0IHtcbiAgY29sb3I6ICRwcmltYXJ5LXZhcmlhbnQxO1xufVxuXG4udGFibGUtY29udGFpbmVyIHtcbiAgcGFkZGluZzogMCAycmVtIDAgMnJlbTtcbiAgd2lkdGg6IDgwJTtcbiAgbWFyZ2luOiBhdXRvO1xufVxuXG4uc2VhcmNoLWNvbnRhaW5lciB7XG4gIG1hcmdpbjogMXJlbSAwIDEuNXJlbSAwO1xuICBkaXNwbGF5OiBmbGV4O1xuICBiYWNrZ3JvdW5kOiAjZjZmY2ZiO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBib3JkZXItYm90dG9tOiAycHggc29saWQgJHByaW1hcnk7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGhlaWdodDogMi4ycmVtO1xuICBwYWRkaW5nLWxlZnQ6IDFyZW07XG4gIHdpZHRoOiBmaXQtY29udGVudDtcbn1cblxuLnNlYXJjaC1pY29uIHtcbiAgbWFyZ2luLXJpZ2h0OiAxLjI1cmVtO1xuICBjb2xvcjogIzAwMDAwMCAhaW1wb3J0YW50O1xufVxuXG5pbnB1dDpmb2N1cyB7XG4gIG91dGxpbmU6IG5vbmU7XG59XG5cbi56aWd6YWctY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB3aWR0aDogMTAwJTtcbn1cblxuLnppZ3phZyB7XG4gIGNvbnRlbnQ6IFwiIFwiO1xuICBkaXNwbGF5OiBibG9jaztcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBib3R0b206IDBweDtcbiAgbGVmdDogMHB4O1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAzMnB4O1xufVxuXG4uemlnemFnLXdoaXRlIHtcbiAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KC00MGRlZywgI2ZmZmZmZiAxNnB4LCB0cmFuc3BhcmVudCAwKSxcbiAgICBsaW5lYXItZ3JhZGllbnQoNDBkZWcsICNmZmZmZmYgMTZweCwgdHJhbnNwYXJlbnQgMCk7XG4gIGJhY2tncm91bmQtcG9zaXRpb246IGxlZnQgYm90dG9tO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogcmVwZWF0LXg7XG4gIGJhY2tncm91bmQtc2l6ZTogMzJweCAzMnB4O1xufVxuXG5hIHtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xuICBjb2xvcjogYmxhY2sgIWltcG9ydGFudDtcbn1cblxuLmxpc3Qtdmlldy1vcHRpb25zLWJ1dHRvbiB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMCk7XG59XG4iLCJAaW1wb3J0IFwiY29sb3JzXCI7XHJcblxyXG4udGFibGUtcm93IHtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkc2Vjb25kYXJ5LXZhcmlhbnQyO1xyXG4gIGhlaWdodDogNDhweDtcclxuICBib3JkZXItYm90dG9tOiAxcHggc29saWQgJHNlY29uZGFyeS12YXJpYW50MTtcclxuICBjdXJzb3I6IHBvaW50ZXI7XHJcbiAgb3ZlcmZsb3ctd3JhcDogYW55d2hlcmU7XHJcbn1cclxuXHJcbi50YWJsZS1yb3c6aG92ZXIge1xyXG4gIGJhY2tncm91bmQ6ICRzZWNvbmRhcnktdmFyaWFudDE7XHJcbn1cclxuXHJcbi5uYW1lLWNvbCB7XHJcbiAgdGV4dC1hbGlnbjogbGVmdDtcclxuICBwYWRkaW5nLWxlZnQ6IDFyZW07XHJcbn1cclxuXHJcbi50YWJsZS1jb2wge1xyXG4gIHRleHQtYWxpZ246IGNlbnRlcjtcclxufVxyXG5cclxuLnRhYmxlLWljb24tY2VsbCB7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBhbGlnbi1pdGVtczogY2VudGVyO1xyXG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xyXG59XHJcblxyXG5hIHtcclxuICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XHJcbiAgY29sb3I6ICMwMDAwMDAgIWltcG9ydGFudDtcclxufVxyXG4iLCJAaW1wb3J0IFwiY29sb3JzXCI7XHJcblxyXG4udGFibGUtaGVhZGVyIHtcclxuICBib3JkZXItYm90dG9tOiAycHggc29saWQgJHByaW1hcnk7XHJcbn1cclxuXHJcbi50YWJsZSB7XHJcbiAgd2lkdGg6IDEwMCU7XHJcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcclxuICBib3JkZXItc3BhY2luZzogMDtcclxufVxyXG5cclxuLm5hbWUtY29sIHtcclxuICB0ZXh0LWFsaWduOiBsZWZ0O1xyXG4gIHBhZGRpbmctbGVmdDogMXJlbTtcclxufVxyXG5cclxudGg6aG92ZXIge1xyXG4gIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG4iLCJAaW1wb3J0IFwiY29sb3JzXCI7XG5cbi50b3BpYyB7XG4gICAgYmFja2dyb3VuZDogJHNlY29uZGFyeSAwJSAwJSBuby1yZXBlYXQgcGFkZGluZy1ib3g7XG4gICAgYm9yZGVyLXJhZGl1czogNzVweDtcbiAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gICAgcGFkZGluZzogMnB4IDEwcHggMHB4IDlweDtcbiAgICBtYXJnaW4tcmlnaHQ6IDAuOXJlbTtcbiAgICBjb2xvcjogI0ZGRkZGRjtcbiAgICB3aWR0aDogZml0LWNvbnRlbnQ7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICBib3JkZXI6ICRzZWNvbmRhcnkgc29saWQgMnB4O1xufVxuXG4udG9waWM6aG92ZXIge1xuICAgIGJvcmRlcjogJHByaW1hcnkgc29saWQgMnB4O1xufVxuXG4udG9waWMtdGFnLWljb24ge1xuICAgIG1hcmdpbi1yaWdodDogMC4yNXJlbTtcbiAgICBmb250LXNpemU6IDEuMjVyZW07XG59XG5cbi5sYWJlbCB7XG4gICAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbn1cblxuYSB7XG4gICAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuXG4uZm9ybS10b3BpYy1maWVsZCB7XG4gIG1hcmdpbi1ib3R0b206IDAuNnJlbTtcbn1cblxuLmZvcm0tdG9waWMtZmllbGQtcmVtb3ZlLWJ1dHRvbjpob3ZlciB7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cblxuLm11bHRpc2VsZWN0X19vcHRpb24tLWhpZ2hsaWdodCB7XG4gIGJhY2tncm91bmQtY29sb3I6ICRwcmltYXJ5ICFpbXBvcnRhbnQ7XG59XG5cbi5tdWx0aXNlbGVjdF9fb3B0aW9uLS1oaWdobGlnaHQ6OmFmdGVyIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogJHByaW1hcnkgIWltcG9ydGFudDtcbn0iLCJAaW1wb3J0IFwiY29sb3JzXCI7XHJcblxyXG4udGFicyB7XHJcbiAgZGlzcGxheTogZmxleDtcclxufVxyXG5cclxuLnNlbGVjdGVkIHtcclxuICBjb2xvcjogIzIxMjEyMTtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkc2Vjb25kYXJ5LXZhcmlhbnQxO1xyXG4gIGJvcmRlci1ib3R0b206IDJweCBzb2xpZCAkcHJpbWFyeTtcclxufVxyXG5cclxuLnNlbGVjdGVkOmhvdmVyIHtcclxuICBiYWNrZ3JvdW5kLWNvbG9yOiAkc2Vjb25kYXJ5LXZhcmlhbnQxO1xyXG59XHJcblxyXG4udW5zZWxlY3RlZCB7XHJcbiAgY29sb3I6ICMyMTIxMjE7XHJcbiAgYmFja2dyb3VuZC1jb2xvcjogJHNlY29uZGFyeS12YXJpYW50MjtcclxuICBib3JkZXItYm90dG9tOiAycHggc29saWQgI2ZmZmZmZjtcclxufVxyXG5cclxuLnVuc2VsZWN0ZWQ6aG92ZXIge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICRzZWNvbmRhcnktdmFyaWFudDE7XHJcbn1cclxuXHJcbi50YWItYnV0dG9uIHtcclxuICBwYWRkaW5nOiAwLjVyZW0gMXJlbTtcclxufVxyXG5cclxuLnRhYi1oZWFkZXItY29udGFpbmVyIHtcclxuICBkaXNwbGF5OiBmbGV4O1xyXG59XHJcblxyXG4uY291bnQge1xyXG4gIGJvcmRlci1yYWRpdXM6IDExcHg7XHJcbiAgZGlzcGxheTogZmxleDtcclxuICBoZWlnaHQ6IDIycHg7XHJcbiAgbWluLXdpZHRoOiAyMnB4O1xyXG4gIG1hcmdpbi1sZWZ0OiAxcmVtO1xyXG4gIHBhZGRpbmc6IDAgMC40cmVtIDAgMC40cmVtO1xyXG59XHJcbi5jb3VudC1zZWxlY3RlZCB7XHJcbiAgYmFja2dyb3VuZDogJGJhY2tncm91bmQ7XHJcbiAgYm9yZGVyOiAxcHggc29saWQgJHByaW1hcnk7XHJcbn1cclxuLmNvdW50LXVuc2VsZWN0ZWQge1xyXG4gIGJhY2tncm91bmQtY29sb3I6ICNlZWVlZWU7XHJcbiAgYm9yZGVyOiAxcHggc29saWQgIzljOWM5YztcclxufVxyXG5cclxuLmNvdW50LWxhYmVsIHtcclxuICBtYXJnaW46IGF1dG87XHJcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xyXG4gIGZvbnQtc2l6ZTogMTJweDtcclxufVxyXG5cclxuLmNvdW50LWxhYmVsLVNlbGVjdGVkIHtcclxuICBjb2xvcjogIzIyNzA2OTtcclxufVxyXG5cclxuLmNvdW50LWxhYmVsLVVuc2VsZWN0ZWQge1xyXG4gIGNvbG9yOiAjMjEyMTIxO1xyXG59XHJcbiJdfQ== */ diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.min.css b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.min.css new file mode 100644 index 000000000..d6565d03e --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/css/radar.min.css @@ -0,0 +1 @@ +.appbar{background-color:#252561!important}.menu-button{background-color:#f9663b!important;width:38px!important;height:38px!important;margin-left:1rem;color:#fff!important}.app-bar-username-container{margin-left:1rem}.app-bar-buttons-container{margin-right:.5rem;display:flex}a{text-decoration:none}.details-section{padding-top:3rem}.header{padding:3rem 7rem;background:transparent linear-gradient(180deg,rgba(37,37,97,.38),rgba(249,102,59,.38)) 0 no-repeat}.details-name{text-align:left;font-weight:300;color:#252561;overflow-wrap:anywhere}.details-description{margin-top:1rem;color:#151537}.topics-container{margin-top:2rem!important}.icon-cell{display:flex;align-items:center}.icon-text{margin-left:1rem}.topics-list-container{display:flex}.options-button{width:3rem!important;height:3rem!important}.details-view-options-button{background-color:#fff!important}.workspace-bubble{margin-left:2rem;background-color:#efeff4;border:2px solid #252561;border-radius:50%;width:3rem;height:3rem;display:flex;align-items:center;justify-content:center;font-weight:400;font-size:23px}.workspace-bubble-text{color:#151537}.workspace-add-button{width:3rem!important;height:3rem!important;margin-left:1rem;background-color:#fff!important}.workspace-button-icon{font-size:40px!important}.workspace-add-button:hover{background-color:#fdc5b5!important}.member-table{width:50%}.attendees-table{width:20%}.table-title-container{display:flex;align-items:center;margin-bottom:1rem}.sections-container{padding:0 7rem 0 7rem}.section{margin-bottom:2rem}.zigzag{content:" ";display:block;position:absolute;bottom:0;left:0;width:100%;height:32px}.zigzag-container{position:relative;width:100%}.zigzag-white{background:linear-gradient(-40deg,#fff 16px,transparent 0),linear-gradient(40deg,#fff 16px,transparent 0);background-position:left bottom;background-repeat:repeat-x;background-size:32px 32px}.details-main-icon{font-size:9rem!important;color:#252561!important}.details-attribute-icon{font-size:2.5rem!important;color:#000!important}.footer-bottom{background:#252561;border-top:3px solid #f9663b;display:flex;justify-content:space-between;padding:3rem 6rem 2rem 6rem}.ripple{background-color:#252561;background-size:cover;background-repeat:no-repeat;background-position:center bottom -623px}.footer{background-color:#252561}.menus-container{display:flex;flex-grow:5;justify-content:space-evenly}.menu-container{display:flex;flex-direction:column}.menu-title{color:#f9663b;margin-bottom:1rem}.zigzag-footer{position:relative;padding:0;background:#fff}.zigzag-footer::after{content:" ";display:block;position:absolute;top:0;left:0;width:100%;height:32px;background:linear-gradient(-145deg,#fff 16px,transparent 0),linear-gradient(145deg,#fff 16px,transparent 0);background-position:left bottom;background-repeat:repeat-x;background-size:32px 32px}.ripple-container{padding:5rem 2rem 5rem 2rem;display:flex;width:100%;background-image:url(images/ripples.svg)}.menu-link{margin-bottom:1rem}.menu-link:hover{text-decoration:underline}.logo-container{display:flex;flex-direction:column;justify-content:center;flex-grow:1}.logo{display:flex;flex-direction:column;justify-content:end;align-items:flex-start;flex-grow:1}.logo-text{margin-left:.5rem}.global-search-container{display:flex;align-items:center;display:flex;height:2.2rem;padding-left:6px;width:fit-content;border-radius:4px;background-color:#00000070;position:relative}.global-search{color:#fff}input:focus{outline:0}.dropdown-list{position:absolute;width:100%;max-height:500px;margin-top:4px;overflow-y:auto;background:#fff;box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);border-radius:8px;top:40px;padding:1rem}.dropdown-item{color:#000;padding:1rem 0 1rem 0}.dropdown-item:hover{background-color:#f1f1f1}.result-caption{color:#252561;display:flex}.page-header-image{margin-left:auto}.icon-primary{color:#252561!important}.icon-secondary{color:#f9663b!important}.card-background{background-color:#efeff4}.trend-card-body{display:flex;flex-direction:column;margin-top:1.25rem}.trend-card-body-icon{font-size:9rem!important}.trend-card-body-container{margin-left:.25rem}.trend-card-body-topic-container{margin-bottom:.5rem}.trend-container{display:flex;align-items:center}.card{display:flex;flex-direction:column;height:100%}.card-title{background-color:#252561;display:flex;flex-direction:column}.card-title:hover{background-color:#3b3b8a}.card-title-caption{display:flex;margin:1.25rem 0 2.5rem 1.25rem}.card-icon{font-size:4rem!important;margin-right:.75rem}.card-body{padding:1rem 0 0 6rem;margin-bottom:1.5rem;flex-grow:1}.card-button{margin-bottom:1rem;margin-left:1rem;color:#f9663b!important}.card-item-link{color:#000!important}.card-item-link:hover{color:#252561!important;text-decoration:underline}.card-item-link-container{margin-bottom:.75rem}.card-button-link{width:fit-content}.card-title{display:flex}.zigzag-container{position:relative;width:100%}.zigzag{content:" ";display:block;position:absolute;bottom:0;left:0;width:100%;height:32px}.zigzag-dark{background:linear-gradient(-40deg,#efeff4 16px,transparent 0),linear-gradient(40deg,#efeff4 16px,transparent 0);background-position:left bottom;background-repeat:repeat-x;background-size:32px 32px}.max-wdith{max-width:1200px}.light-font{font-weight:lighter;color:#000000de;opacity:1}.header-list{display:flex;flex-direction:column}.list-item{display:flex;margin-bottom:8px}.list-item-text{font-size:1.125rem}.name{font-size:3.5rem;color:#252561;font-weight:300;white-space:nowrap}.description{font-size:25px;font-weight:300;margin:auto;line-height:normal}.list{display:flex;justify-content:center}.section-title{color:#252561}.section-background{background:#efeff461}.front-footer{background-color:#252561!important;padding:1.75rem 0 2rem 0}.front-footer:hover{background-color:#3b3b8a!important}.front-footer-card-icon{font-size:7.5rem!important;margin-bottom:.5rem}.front-footer-card-title{margin-bottom:.25rem}.header-footer{display:flex;justify-content:space-between;padding:0 10rem 0 10rem;margin-top:1.75rem;flex-wrap:wrap}.zigzag-container{position:relative;width:100%}.zigzag{content:" ";display:block;position:absolute;bottom:0;left:0;width:100%;height:32px}.zigzag-white{background:linear-gradient(-40deg,#fff 16px,transparent 0),linear-gradient(40deg,#fff 16px,transparent 0);background-position:left bottom;background-repeat:repeat-x;background-size:32px 32px}.rippleFront{background-color:#cfece9;background-image:url(images/ripples-white.svg),linear-gradient(#252561,rgba(249,102,59,.7));background-size:cover;background-repeat:no-repeat;background-position:center;padding:12rem 0 3rem 0}a{text-decoration:none}.list-header{padding:3rem 13rem 4rem 13rem;background:transparent linear-gradient(180deg,rgba(37,37,97,.38),rgba(249,102,59,.38)) 0 no-repeat}.details-section{padding-top:3rem}.list-title{color:#252561}.title-container{display:flex;align-items:center}.bubble{margin-left:2rem;background-color:#efeff4;border:2px solid #252561;border-radius:50%;width:5rem;height:5rem;display:flex;align-items:center;justify-content:center;font-weight:400;font-size:38px}.bubble-text{color:#151537}.table-container{padding:0 2rem 0 2rem;width:80%;margin:auto}.search-container{margin:1rem 0 1.5rem 0;display:flex;background:#f6fcfb;align-items:center;border-bottom:2px solid #252561;display:flex;height:2.2rem;padding-left:1rem;width:fit-content}.search-icon{margin-right:1.25rem;color:#000!important}input:focus{outline:0}.zigzag-container{position:relative;width:100%}.zigzag{content:" ";display:block;position:absolute;bottom:0;left:0;width:100%;height:32px}.zigzag-white{background:linear-gradient(-40deg,#fff 16px,transparent 0),linear-gradient(40deg,#fff 16px,transparent 0);background-position:left bottom;background-repeat:repeat-x;background-size:32px 32px}a{text-decoration:none;color:#000!important}.list-view-options-button{background-color:rgba(255,255,255,0)}.table-row{background-color:#feece6;height:48px;border-bottom:1px solid #fdc5b5;cursor:pointer;overflow-wrap:anywhere}.table-row:hover{background:#fdc5b5}.name-col{text-align:left;padding-left:1rem}.table-col{text-align:center}.table-icon-cell{display:flex;align-items:center;justify-content:center}a{text-decoration:none;color:#000!important}.table-header{border-bottom:2px solid #252561}.table{width:100%;border-collapse:collapse;border-spacing:0}.name-col{text-align:left;padding-left:1rem}th:hover{cursor:pointer}.topic{background:#f9663b 0 0 no-repeat padding-box;border-radius:75px;display:inline-block;padding:2px 10px 0 9px;margin-right:.9rem;color:#fff;width:fit-content;display:flex;border:#f9663b solid 2px}.topic:hover{border:#252561 solid 2px}.topic-tag-icon{margin-right:.25rem;font-size:1.25rem}.label{overflow:hidden;text-overflow:ellipsis}a{text-decoration:none}.form-topic-field{margin-bottom:.6rem}.form-topic-field-remove-button:hover{cursor:pointer}.multiselect__option--highlight{background-color:#252561!important}.multiselect__option--highlight::after{background-color:#252561!important}.tabs{display:flex}.selected{color:#212121;background-color:#fdc5b5;border-bottom:2px solid #252561}.selected:hover{background-color:#fdc5b5}.unselected{color:#212121;background-color:#feece6;border-bottom:2px solid #fff}.unselected:hover{background-color:#fdc5b5}.tab-button{padding:.5rem 1rem}.tab-header-container{display:flex}.count{border-radius:11px;display:flex;height:22px;min-width:22px;margin-left:1rem;padding:0 .4rem 0 .4rem}.count-selected{background:#efeff4;border:1px solid #252561}.count-unselected{background-color:#eee;border:1px solid #9c9c9c}.count-label{margin:auto;text-align:center;font-size:12px}.count-label-Selected{color:#227069}.count-label-Unselected{color:#212121} diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.js b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.js new file mode 100644 index 000000000..57afe6e3d --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.js @@ -0,0 +1,121 @@ +/* +** NOTE: This file is generated by Gulp and should not be edited directly! +** Any changes made directly to this file will be overwritten next time its asset group is processed by Gulp. +*/ + +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +var attachSorter = function attachSorter() { + var tables = document.querySelectorAll("#entity-table"); + + var _iterator = _createForOfIteratorHelper(tables), + _step; + + try { + var _loop = function _loop() { + var table = _step.value; + var headers = table.querySelectorAll("th"); + var tableBody = table.querySelector("tbody"); + var rows = tableBody.querySelectorAll("tr"); // Track sort directions + + var directions = Array.from(headers).map(function (header) { + return ""; + }); // Transform the content of given cell in given column + + var transform = function transform(index, content) { + // Get the data type of column + var type = headers[index].getAttribute("data-type"); + + if (type === "name") { + content = content.childNodes[0].innerHTML; + } else if (type === "number") { + content = content.childNodes[0].childNodes[2].innerHTML; + } else if (type === "date-range") { + content = content.childNodes[0].innerHTML.split("-")[0]; + } + + switch (type) { + case "number": + return parseFloat(content); + + case "date": + return Date.parse(content); + + case "string": + default: + return content; + } + }; + + var sortColumn = function sortColumn(index) { + // Get the current direction + var direction = directions[index] || "asc"; // A factor based on the direction + + var multiplier = direction === "asc" ? 1 : -1; + var newRows = Array.from(rows); + newRows.sort(function (rowA, rowB) { + var cellA = rowA.querySelectorAll("td")[index]; + var cellB = rowB.querySelectorAll("td")[index]; + var a = transform(index, cellA); + var b = transform(index, cellB); + + switch (true) { + case a > b: + return 1 * multiplier; + + case a < b: + return -1 * multiplier; + + case a === b: + return 0; + } + }); // Remove old rows + + [].forEach.call(rows, function (row) { + tableBody.removeChild(row); + }); // Reverse the direction + + directions[index] = direction === "asc" ? "desc" : "asc"; // Append new row + + newRows.forEach(function (newRow) { + tableBody.appendChild(newRow); + }); + }; + + [].forEach.call(headers, function (header, index) { + header.addEventListener("click", function () { + sortColumn(index); + }); + }); + }; + + for (_iterator.s(); !(_step = _iterator.n()).done;) { + _loop(); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } +}; + +var attachSearchClear = function attachSearchClear() { + // Hook into the rendered button + var clearButton = document.getElementById("search-clear-button"); + + if (clearButton) { + clearButton.addEventListener("click", function () { + document.getElementById("search-input").value = ""; + document.getElementById("search-form").submit(); + }); + } +}; + +window.addEventListener("DOMContentLoaded", function () { + attachSorter(); + attachSearchClear(); +}); \ No newline at end of file diff --git a/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.min.js b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.min.js new file mode 100644 index 000000000..1eef8e1fb --- /dev/null +++ b/src/Apps/StatCan.OrchardCore.Radar/wwwroot/js/table.min.js @@ -0,0 +1 @@ +function _createForOfIteratorHelper(r,e){var t="undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(!t){if(Array.isArray(r)||(t=_unsupportedIterableToArray(r))||e&&r&&"number"==typeof r.length){t&&(r=t);var n=0,a=function(){};return{s:a,n:function(){return n>=r.length?{done:!0}:{done:!1,value:r[n++]}},e:function(r){throw r},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,c=!0,i=!1;return{s:function(){t=t.call(r)},n:function(){var r=t.next();return c=r.done,r},e:function(r){i=!0,o=r},f:function(){try{c||null==t.return||t.return()}finally{if(i)throw o}}}}function _unsupportedIterableToArray(r,e){if(r){if("string"==typeof r)return _arrayLikeToArray(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);return"Object"===t&&r.constructor&&(t=r.constructor.name),"Map"===t||"Set"===t?Array.from(r):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?_arrayLikeToArray(r,e):void 0}}function _arrayLikeToArray(r,e){(null==e||e>r.length)&&(e=r.length);for(var t=0,n=new Array(e);tu:return 1*t;case i Logs the number of milliseconds it took for the deferred invocation. + */ +var now = function() { + return root.Date.now(); +}; + +module.exports = now; + + +/***/ }), + +/***/ "3133": +/***/ (function(module, exports, __webpack_require__) { + +var trimmedEndIndex = __webpack_require__("034a"); + +/** Used to match leading whitespace. */ +var reTrimStart = /^\s+/; + +/** + * The base implementation of `_.trim`. + * + * @private + * @param {string} string The string to trim. + * @returns {string} Returns the trimmed string. + */ +function baseTrim(string) { + return string + ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '') + : string; +} + +module.exports = baseTrim; + + +/***/ }), + +/***/ "3221": +/***/ (function(module, exports) { + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +module.exports = isObjectLike; + + +/***/ }), + +/***/ "376c": +/***/ (function(module, exports, __webpack_require__) { + +var freeGlobal = __webpack_require__("88ed"); + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +module.exports = root; + + +/***/ }), + +/***/ "4804": +/***/ (function(module, exports, __webpack_require__) { + +// extracted by mini-css-extract-plugin + +/***/ }), + +/***/ "6544": +/***/ (function(module, exports) { + +// IMPORTANT: Do NOT use ES2015 features in this file (except for modules). +// This module is a runtime utility for cleaner component module output and will +// be included in the final webpack user bundle. + +module.exports = function installComponents (component, components) { + var options = typeof component.exports === 'function' + ? component.exports.extendOptions + : component.options + + if (typeof component.exports === 'function') { + options.components = component.exports.options.components + } + + options.components = options.components || {} + + for (var i in components) { + options.components[i] = options.components[i] || components[i] + } +} + + +/***/ }), + +/***/ "6f16": +/***/ (function(module, exports, __webpack_require__) { + +var isObject = __webpack_require__("ca26"), + now = __webpack_require__("0ad6"), + toNumber = __webpack_require__("a62e"); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max, + nativeMin = Math.min; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + clearTimeout(timerId); + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +module.exports = debounce; + + +/***/ }), + +/***/ "7ebe": +/***/ (function(module, exports, __webpack_require__) { + +var baseGetTag = __webpack_require__("e41f"), + isObjectLike = __webpack_require__("3221"); + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); +} + +module.exports = isSymbol; + + +/***/ }), + +/***/ "8875": +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// addapted from the document.currentScript polyfill by Adam Miller +// MIT license +// source: https://github.com/amiller-gh/currentScript-polyfill + +// added support for Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1620505 + +(function (root, factory) { + if (true) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else {} +}(typeof self !== 'undefined' ? self : this, function () { + function getCurrentScript () { + var descriptor = Object.getOwnPropertyDescriptor(document, 'currentScript') + // for chrome + if (!descriptor && 'currentScript' in document && document.currentScript) { + return document.currentScript + } + + // for other browsers with native support for currentScript + if (descriptor && descriptor.get !== getCurrentScript && document.currentScript) { + return document.currentScript + } + + // IE 8-10 support script readyState + // IE 11+ & Firefox support stack trace + try { + throw new Error(); + } + catch (err) { + // Find the second match for the "at" string to get file src url from stack. + var ieStackRegExp = /.*at [^(]*\((.*):(.+):(.+)\)$/ig, + ffStackRegExp = /@([^@]*):(\d+):(\d+)\s*$/ig, + stackDetails = ieStackRegExp.exec(err.stack) || ffStackRegExp.exec(err.stack), + scriptLocation = (stackDetails && stackDetails[1]) || false, + line = (stackDetails && stackDetails[2]) || false, + currentLocation = document.location.href.replace(document.location.hash, ''), + pageSource, + inlineScriptSourceRegExp, + inlineScriptSource, + scripts = document.getElementsByTagName('script'); // Live NodeList collection + + if (scriptLocation === currentLocation) { + pageSource = document.documentElement.outerHTML; + inlineScriptSourceRegExp = new RegExp('(?:[^\\n]+?\\n){0,' + (line - 2) + '}[^<]*\r\n","import mod from \"-!../../../../../../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../../../../../../node_modules/thread-loader/dist/cjs.js!../../../../../../../node_modules/babel-loader/lib/index.js!../../../../../../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Tabs.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../../../../../../node_modules/thread-loader/dist/cjs.js!../../../../../../../node_modules/babel-loader/lib/index.js!../../../../../../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Tabs.vue?vue&type=script&lang=js&\"","/* globals __VUE_SSR_CONTEXT__ */\n\n// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).\n// This module is a runtime utility for cleaner component module output and will\n// be included in the final webpack user bundle.\n\nexport default function normalizeComponent (\n scriptExports,\n render,\n staticRenderFns,\n functionalTemplate,\n injectStyles,\n scopeId,\n moduleIdentifier, /* server only */\n shadowMode /* vue-cli only */\n) {\n // Vue.extend constructor export interop\n var options = typeof scriptExports === 'function'\n ? scriptExports.options\n : scriptExports\n\n // render functions\n if (render) {\n options.render = render\n options.staticRenderFns = staticRenderFns\n options._compiled = true\n }\n\n // functional template\n if (functionalTemplate) {\n options.functional = true\n }\n\n // scopedId\n if (scopeId) {\n options._scopeId = 'data-v-' + scopeId\n }\n\n var hook\n if (moduleIdentifier) { // server build\n hook = function (context) {\n // 2.3 injection\n context =\n context || // cached call\n (this.$vnode && this.$vnode.ssrContext) || // stateful\n (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional\n // 2.2 with runInNewContext: true\n if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {\n context = __VUE_SSR_CONTEXT__\n }\n // inject component styles\n if (injectStyles) {\n injectStyles.call(this, context)\n }\n // register component module identifier for async chunk inferrence\n if (context && context._registeredComponents) {\n context._registeredComponents.add(moduleIdentifier)\n }\n }\n // used by ssr in case component is cached and beforeCreate\n // never gets called\n options._ssrRegister = hook\n } else if (injectStyles) {\n hook = shadowMode\n ? function () {\n injectStyles.call(\n this,\n (options.functional ? this.parent : this).$root.$options.shadowRoot\n )\n }\n : injectStyles\n }\n\n if (hook) {\n if (options.functional) {\n // for template-only hot-reload because in that case the render fn doesn't\n // go through the normalizer\n options._injectStyles = hook\n // register for functional component in vue file\n var originalRender = options.render\n options.render = function renderWithStyleInjection (h, context) {\n hook.call(context)\n return originalRender(h, context)\n }\n } else {\n // inject component registration as beforeCreate hook\n var existing = options.beforeCreate\n options.beforeCreate = existing\n ? [].concat(existing, hook)\n : [hook]\n }\n }\n\n return {\n exports: scriptExports,\n options: options\n }\n}\n","import Vue from 'vue';\n/**\n * This mixin provides `attrs$` and `listeners$` to work around\n * vue bug https://github.com/vuejs/vue/issues/10115\n */\n\nfunction makeWatcher(property) {\n return function (val, oldVal) {\n for (const attr in oldVal) {\n if (!Object.prototype.hasOwnProperty.call(val, attr)) {\n this.$delete(this.$data[property], attr);\n }\n }\n\n for (const attr in val) {\n this.$set(this.$data[property], attr, val[attr]);\n }\n };\n}\n\nexport default Vue.extend({\n data: () => ({\n attrs$: {},\n listeners$: {}\n }),\n\n created() {\n // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115\n // Make sure to use `attrs$` instead of `$attrs` (confusing right?)\n this.$watch('$attrs', makeWatcher('attrs$'), {\n immediate: true\n });\n this.$watch('$listeners', makeWatcher('listeners$'), {\n immediate: true\n });\n }\n\n});\n//# sourceMappingURL=index.js.map","import OurVue from 'vue';\nimport { consoleError } from './util/console';\nexport function install(Vue, args = {}) {\n if (install.installed) return;\n install.installed = true;\n\n if (OurVue !== Vue) {\n consoleError(`Multiple instances of Vue detected\nSee https://github.com/vuetifyjs/vuetify/issues/4068\n\nIf you're seeing \"$attrs is readonly\", it's caused by this`);\n }\n\n const components = args.components || {};\n const directives = args.directives || {};\n\n for (const name in directives) {\n const directive = directives[name];\n Vue.directive(name, directive);\n }\n\n (function registerComponents(components) {\n if (components) {\n for (const key in components) {\n const component = components[key];\n\n if (component && !registerComponents(component.$_vuetify_subcomponents)) {\n Vue.component(key, component);\n }\n }\n\n return true;\n }\n\n return false;\n })(components); // Used to avoid multiple mixins being setup\n // when in dev mode and hot module reload\n // https://github.com/vuejs/vue/issues/5089#issuecomment-284260111\n\n\n if (Vue.$_vuetify_installed) return;\n Vue.$_vuetify_installed = true;\n Vue.mixin({\n beforeCreate() {\n const options = this.$options;\n\n if (options.vuetify) {\n options.vuetify.init(this, this.$ssrContext);\n this.$vuetify = Vue.observable(options.vuetify.framework);\n } else {\n this.$vuetify = options.parent && options.parent.$vuetify || this;\n }\n },\n\n beforeMount() {\n // @ts-ignore\n if (this.$options.vuetify && this.$el && this.$el.hasAttribute('data-server-rendered')) {\n // @ts-ignore\n this.$vuetify.isHydrating = true; // @ts-ignore\n\n this.$vuetify.breakpoint.update(true);\n }\n },\n\n mounted() {\n // @ts-ignore\n if (this.$options.vuetify && this.$vuetify.isHydrating) {\n // @ts-ignore\n this.$vuetify.isHydrating = false; // @ts-ignore\n\n this.$vuetify.breakpoint.update();\n }\n }\n\n });\n}\n//# sourceMappingURL=install.js.map","export default {\n badge: 'Badge',\n close: 'Close',\n dataIterator: {\n noResultsText: 'No matching records found',\n loadingText: 'Loading items...'\n },\n dataTable: {\n itemsPerPageText: 'Rows per page:',\n ariaLabel: {\n sortDescending: 'Sorted descending.',\n sortAscending: 'Sorted ascending.',\n sortNone: 'Not sorted.',\n activateNone: 'Activate to remove sorting.',\n activateDescending: 'Activate to sort descending.',\n activateAscending: 'Activate to sort ascending.'\n },\n sortBy: 'Sort by'\n },\n dataFooter: {\n itemsPerPageText: 'Items per page:',\n itemsPerPageAll: 'All',\n nextPage: 'Next page',\n prevPage: 'Previous page',\n firstPage: 'First page',\n lastPage: 'Last page',\n pageText: '{0}-{1} of {2}'\n },\n datePicker: {\n itemsSelected: '{0} selected',\n nextMonthAriaLabel: 'Next month',\n nextYearAriaLabel: 'Next year',\n prevMonthAriaLabel: 'Previous month',\n prevYearAriaLabel: 'Previous year'\n },\n noDataText: 'No data available',\n carousel: {\n prev: 'Previous visual',\n next: 'Next visual',\n ariaLabel: {\n delimiter: 'Carousel slide {0} of {1}'\n }\n },\n calendar: {\n moreEvents: '{0} more'\n },\n fileInput: {\n counter: '{0} files',\n counterSize: '{0} files ({1} in total)'\n },\n timePicker: {\n am: 'AM',\n pm: 'PM'\n },\n pagination: {\n ariaLabel: {\n wrapper: 'Pagination Navigation',\n next: 'Next page',\n previous: 'Previous page',\n page: 'Goto Page {0}',\n currentPage: 'Current Page, Page {0}'\n }\n },\n rating: {\n ariaLabel: {\n icon: 'Rating {0} of {1}'\n }\n }\n};\n//# sourceMappingURL=en.js.map","// Styles\nimport \"../../../src/styles/main.sass\"; // Locale\n\nimport { en } from '../../locale';\nexport const preset = {\n breakpoint: {\n // TODO: update to MD2 spec in v3 - 1280\n mobileBreakpoint: 1264,\n scrollBarWidth: 16,\n thresholds: {\n xs: 600,\n sm: 960,\n md: 1280,\n lg: 1920\n }\n },\n icons: {\n // TODO: remove v3\n iconfont: 'mdi',\n values: {}\n },\n lang: {\n current: 'en',\n locales: {\n en\n },\n // Default translator exists in lang service\n t: undefined\n },\n rtl: false,\n theme: {\n dark: false,\n default: 'light',\n disable: false,\n options: {\n cspNonce: undefined,\n customProperties: undefined,\n minifyTheme: undefined,\n themeCache: undefined,\n variations: true\n },\n themes: {\n light: {\n primary: '#1976D2',\n secondary: '#424242',\n accent: '#82B1FF',\n error: '#FF5252',\n info: '#2196F3',\n success: '#4CAF50',\n warning: '#FB8C00'\n },\n dark: {\n primary: '#2196F3',\n secondary: '#424242',\n accent: '#FF4081',\n error: '#FF5252',\n info: '#2196F3',\n success: '#4CAF50',\n warning: '#FB8C00'\n }\n }\n }\n};\n//# sourceMappingURL=index.js.map","import Vue from 'vue';\nexport function createSimpleFunctional(c, el = 'div', name) {\n return Vue.extend({\n name: name || c.replace(/__/g, '-'),\n functional: true,\n\n render(h, {\n data,\n children\n }) {\n data.staticClass = `${c} ${data.staticClass || ''}`.trim();\n return h(el, data, children);\n }\n\n });\n}\nexport function directiveConfig(binding, defaults = {}) {\n return { ...defaults,\n ...binding.modifiers,\n value: binding.arg,\n ...(binding.value || {})\n };\n}\nexport function addOnceEventListener(el, eventName, cb, options = false) {\n var once = event => {\n cb(event);\n el.removeEventListener(eventName, once, options);\n };\n\n el.addEventListener(eventName, once, options);\n}\nlet passiveSupported = false;\n\ntry {\n if (typeof window !== 'undefined') {\n const testListenerOpts = Object.defineProperty({}, 'passive', {\n get: () => {\n passiveSupported = true;\n }\n });\n window.addEventListener('testListener', testListenerOpts, testListenerOpts);\n window.removeEventListener('testListener', testListenerOpts, testListenerOpts);\n }\n} catch (e) {\n console.warn(e);\n}\n/* eslint-disable-line no-console */\n\n\nexport { passiveSupported };\nexport function addPassiveEventListener(el, event, cb, options) {\n el.addEventListener(event, cb, passiveSupported ? options : false);\n}\nexport function getNestedValue(obj, path, fallback) {\n const last = path.length - 1;\n if (last < 0) return obj === undefined ? fallback : obj;\n\n for (let i = 0; i < last; i++) {\n if (obj == null) {\n return fallback;\n }\n\n obj = obj[path[i]];\n }\n\n if (obj == null) return fallback;\n return obj[path[last]] === undefined ? fallback : obj[path[last]];\n}\nexport function deepEqual(a, b) {\n if (a === b) return true;\n\n if (a instanceof Date && b instanceof Date && a.getTime() !== b.getTime()) {\n // If the values are Date, compare them as timestamps\n return false;\n }\n\n if (a !== Object(a) || b !== Object(b)) {\n // If the values aren't objects, they were already checked for equality\n return false;\n }\n\n const props = Object.keys(a);\n\n if (props.length !== Object.keys(b).length) {\n // Different number of props, don't bother to check\n return false;\n }\n\n return props.every(p => deepEqual(a[p], b[p]));\n}\nexport function getObjectValueByPath(obj, path, fallback) {\n // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621\n if (obj == null || !path || typeof path !== 'string') return fallback;\n if (obj[path] !== undefined) return obj[path];\n path = path.replace(/\\[(\\w+)\\]/g, '.$1'); // convert indexes to properties\n\n path = path.replace(/^\\./, ''); // strip a leading dot\n\n return getNestedValue(obj, path.split('.'), fallback);\n}\nexport function getPropertyFromItem(item, property, fallback) {\n if (property == null) return item === undefined ? fallback : item;\n if (item !== Object(item)) return fallback === undefined ? item : fallback;\n if (typeof property === 'string') return getObjectValueByPath(item, property, fallback);\n if (Array.isArray(property)) return getNestedValue(item, property, fallback);\n if (typeof property !== 'function') return fallback;\n const value = property(item, fallback);\n return typeof value === 'undefined' ? fallback : value;\n}\nexport function createRange(length) {\n return Array.from({\n length\n }, (v, k) => k);\n}\nexport function getZIndex(el) {\n if (!el || el.nodeType !== Node.ELEMENT_NODE) return 0;\n const index = +window.getComputedStyle(el).getPropertyValue('z-index');\n if (!index) return getZIndex(el.parentNode);\n return index;\n}\nconst tagsToReplace = {\n '&': '&',\n '<': '<',\n '>': '>'\n};\nexport function escapeHTML(str) {\n return str.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag);\n}\nexport function filterObjectOnKeys(obj, keys) {\n const filtered = {};\n\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n\n if (typeof obj[key] !== 'undefined') {\n filtered[key] = obj[key];\n }\n }\n\n return filtered;\n}\nexport function convertToUnit(str, unit = 'px') {\n if (str == null || str === '') {\n return undefined;\n } else if (isNaN(+str)) {\n return String(str);\n } else {\n return `${Number(str)}${unit}`;\n }\n}\nexport function kebabCase(str) {\n return (str || '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n}\nexport function isObject(obj) {\n return obj !== null && typeof obj === 'object';\n} // KeyboardEvent.keyCode aliases\n\nexport const keyCodes = Object.freeze({\n enter: 13,\n tab: 9,\n delete: 46,\n esc: 27,\n space: 32,\n up: 38,\n down: 40,\n left: 37,\n right: 39,\n end: 35,\n home: 36,\n del: 46,\n backspace: 8,\n insert: 45,\n pageup: 33,\n pagedown: 34,\n shift: 16\n});\n/**\n * This remaps internal names like '$cancel' or '$vuetify.icons.cancel'\n * to the current name or component for that icon.\n */\n\nexport function remapInternalIcon(vm, iconName) {\n // Look for custom component in the configuration\n const component = vm.$vuetify.icons.component; // Look for overrides\n\n if (iconName.startsWith('$')) {\n // Get the target icon name\n const iconPath = `$vuetify.icons.values.${iconName.split('$').pop().split('.').pop()}`; // Now look up icon indirection name,\n // e.g. '$vuetify.icons.values.cancel'\n\n const override = getObjectValueByPath(vm, iconPath, iconName);\n if (typeof override === 'string') iconName = override;else return override;\n }\n\n if (component == null) {\n return iconName;\n }\n\n return {\n component,\n props: {\n icon: iconName\n }\n };\n}\nexport function keys(o) {\n return Object.keys(o);\n}\n/**\n * Camelize a hyphen-delimited string.\n */\n\nconst camelizeRE = /-(\\w)/g;\nexport const camelize = str => {\n return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '');\n};\n/**\n * Returns the set difference of B and A, i.e. the set of elements in B but not in A\n */\n\nexport function arrayDiff(a, b) {\n const diff = [];\n\n for (let i = 0; i < b.length; i++) {\n if (a.indexOf(b[i]) < 0) diff.push(b[i]);\n }\n\n return diff;\n}\n/**\n * Makes the first character of a string uppercase\n */\n\nexport function upperFirst(str) {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\nexport function groupItems(items, groupBy, groupDesc) {\n const key = groupBy[0];\n const groups = [];\n let current;\n\n for (var i = 0; i < items.length; i++) {\n const item = items[i];\n const val = getObjectValueByPath(item, key, null);\n\n if (current !== val) {\n var _val;\n\n current = val;\n groups.push({\n name: (_val = val) != null ? _val : '',\n items: []\n });\n }\n\n groups[groups.length - 1].items.push(item);\n }\n\n return groups;\n}\nexport function wrapInArray(v) {\n return v != null ? Array.isArray(v) ? v : [v] : [];\n}\nexport function sortItems(items, sortBy, sortDesc, locale, customSorters) {\n if (sortBy === null || !sortBy.length) return items;\n const stringCollator = new Intl.Collator(locale, {\n sensitivity: 'accent',\n usage: 'sort'\n });\n return items.sort((a, b) => {\n for (let i = 0; i < sortBy.length; i++) {\n const sortKey = sortBy[i];\n let sortA = getObjectValueByPath(a, sortKey);\n let sortB = getObjectValueByPath(b, sortKey);\n\n if (sortDesc[i]) {\n [sortA, sortB] = [sortB, sortA];\n }\n\n if (customSorters && customSorters[sortKey]) {\n const customResult = customSorters[sortKey](sortA, sortB);\n if (!customResult) continue;\n return customResult;\n } // Check if both cannot be evaluated\n\n\n if (sortA === null && sortB === null) {\n continue;\n }\n\n [sortA, sortB] = [sortA, sortB].map(s => (s || '').toString().toLocaleLowerCase());\n\n if (sortA !== sortB) {\n if (!isNaN(sortA) && !isNaN(sortB)) return Number(sortA) - Number(sortB);\n return stringCollator.compare(sortA, sortB);\n }\n }\n\n return 0;\n });\n}\nexport function defaultFilter(value, search, item) {\n return value != null && search != null && typeof value !== 'boolean' && value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;\n}\nexport function searchItems(items, search) {\n if (!search) return items;\n search = search.toString().toLowerCase();\n if (search.trim() === '') return items;\n return items.filter(item => Object.keys(item).some(key => defaultFilter(getObjectValueByPath(item, key), search, item)));\n}\n/**\n * Returns:\n * - 'normal' for old style slots - `