├── PlanetaryDocs ├── Shared │ ├── DisabledNavLink.razor.css │ ├── DocHistory.razor.css │ ├── HtmlPreview.razor │ ├── DisabledNavLink.razor │ ├── Loading.razor │ ├── MultiLineEdit.razor │ ├── ValidatedInput.razor.css │ ├── Editor.razor.css │ ├── Saving.razor │ ├── TagSearch.razor │ ├── AliasSearch.razor │ ├── AutoComplete.razor.css │ ├── HtmlPreview.razor.css │ ├── MainLayout.razor │ ├── AutoComplete.razor │ ├── EditBar.razor │ ├── ValidatedInput.razor │ ├── TagPicker.razor │ ├── NavMenu.razor.css │ ├── DocHistory.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ ├── EditBarBase.cs │ ├── TagSearchBase.cs │ ├── DocHistoryBase.cs │ ├── HtmlPreviewBase.cs │ ├── AliasSearchBase.cs │ ├── MultiLineEditBase.cs │ ├── TagPickerBase.cs │ ├── NavMenuBase.cs │ ├── Editor.razor │ └── ValidatedInputBase.cs ├── wwwroot │ ├── favicon.ico │ ├── images │ │ └── m42hubble.jpg │ ├── css │ │ ├── open-iconic │ │ │ ├── font │ │ │ │ └── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ └── open-iconic.woff │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── FONT-LICENSE │ │ └── site.css │ └── js │ │ ├── titleService.js │ │ └── markdownExtensions.js ├── Pages │ ├── View.razor.css │ ├── Index.razor.css │ ├── Add.razor │ ├── ErrorModel.cs │ ├── _Host.cshtml │ ├── Edit.razor │ ├── Error.cshtml │ ├── AddBase.cs │ ├── View.razor │ ├── Index.razor │ ├── ViewBase.cs │ └── EditBase.cs ├── appsettings.Development.json ├── App.razor ├── _Imports.razor ├── appsettings.json ├── PlanetaryDocs.csproj ├── Services │ ├── KeyNames.cs │ ├── HistoryService.cs │ ├── TitleService.cs │ ├── NavigationHelper.cs │ ├── MultiLineEditService.cs │ └── LoadingService.cs ├── Properties │ └── launchSettings.json ├── CosmosSettings.cs ├── Program.cs └── Startup.cs ├── Tests ├── Directory.Build.props └── DomainTests │ ├── ValidationStateTests.cs │ ├── DomainTests.csproj │ ├── AuthorTests.cs │ ├── TagTests.cs │ ├── DocumentTests.cs │ ├── DocumentAuditTests.cs │ ├── DocumentSummaryTests.cs │ └── DocumentAuditSummaryTests.cs ├── PlanetaryDocs.Domain ├── PlanetaryDocs.Domain.csproj ├── IDocSummaries.cs ├── ValidationState.cs ├── Author.cs ├── Tag.cs ├── DocumentAudit.cs ├── DocumentSummary.cs ├── Document.cs ├── DocumentAuditSummary.cs └── IDocumentService.cs ├── version.json ├── stylecop.json ├── PlanetaryDocs.DataAccess └── PlanetaryDocs.DataAccess.csproj ├── PlanetaryDocsLoader ├── PlanetaryDocsLoader.csproj ├── Program.cs ├── FileSystemParser.cs └── MarkdownParser.cs ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── tests.yml │ └── dotnet.yml ├── LICENSE ├── Directory.Build.props ├── .gitattributes ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── PlanetaryDocs.sln ├── README.md └── .editorconfig /PlanetaryDocs/Shared/DisabledNavLink.razor.css: -------------------------------------------------------------------------------- 1 | a:hover { 2 | cursor: not-allowed; 3 | } -------------------------------------------------------------------------------- /Tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/DocHistory.razor.css: -------------------------------------------------------------------------------- 1 | .summary:hover { 2 | cursor: pointer; 3 | background-color: lightgray; 4 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/HtmlPreview.razor: -------------------------------------------------------------------------------- 1 | @inherits HtmlPreviewBase 2 | 3 |
4 | @HtmlToRender 5 |
-------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/favicon.ico -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/images/m42hubble.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/images/m42hubble.jpg -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/PlanetaryDocs/HEAD/PlanetaryDocs/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/DisabledNavLink.razor: -------------------------------------------------------------------------------- 1 | @inherits NavLink 2 | 3 | 6 | @ChildContent 7 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/Loading.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 📀 Parsing the bits... 5 |
6 |
7 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Shared/MultiLineEdit.razor: -------------------------------------------------------------------------------- 1 | @inherits MultiLineEditBase 2 | 3 | 8 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/ValidatedInput.razor.css: -------------------------------------------------------------------------------- 1 | .error { 2 | border: solid 1px red; 3 | background-color: lightcoral; 4 | } 5 | 6 | input { 7 | width: 100%; 8 | padding: 0.3em; 9 | } 10 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/Editor.razor.css: -------------------------------------------------------------------------------- 1 | li { 2 | font-weight: bold; 3 | color: darkred; 4 | } 5 | 6 | .field-label { 7 | font-weight: bold; 8 | margin-left: 1em; 9 | color: #202020; 10 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/Saving.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 📀 Pushing bits to the cosmos... 5 |
6 |
7 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Pages/View.razor.css: -------------------------------------------------------------------------------- 1 | .code { 2 | padding-top: 1em; 3 | white-space: pre; 4 | height: 60vh; 5 | color: green; 6 | background: black; 7 | font-family: Consolas, monospace; 8 | width: 60vw; 9 | overflow: scroll; 10 | } -------------------------------------------------------------------------------- /PlanetaryDocs/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/TagSearch.razor: -------------------------------------------------------------------------------- 1 | @inherits TagSearchBase 2 | 3 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/PlanetaryDocs.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 10.0 6 | Domain models and business logic. 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/AliasSearch.razor: -------------------------------------------------------------------------------- 1 | @inherits AliasSearchBase 2 | 3 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/js/titleService.js: -------------------------------------------------------------------------------- 1 | window.titleService = { 2 | titleRef: null, 3 | setTitle: (title) => { 4 | var _self = window.titleService; 5 | if (_self.titleRef == null) { 6 | _self.titleRef = document.getElementsByTagName("title")[0]; 7 | } 8 | setTimeout(() => _self.titleRef.innerText = title, 0); 9 | } 10 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/AutoComplete.razor.css: -------------------------------------------------------------------------------- 1 | div { 2 | display: inline-table; 3 | } 4 | 5 | p:hover, p.active { 6 | font-weight: bold; 7 | cursor: pointer; 8 | } 9 | 10 | .labelArea { 11 | font-weight: bold; 12 | text-align: right; 13 | margin-right: 1em; 14 | display: table-cell; 15 | } 16 | 17 | a, input { 18 | width: 95%; 19 | display: table-cell; 20 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "0.2.0-beta", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/master$", 6 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "buildNumber": { 10 | "enabled": true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/HtmlPreview.razor.css: -------------------------------------------------------------------------------- 1 | .web { 2 | padding: 0.5em; 3 | height: 60vh; 4 | background-color: lightgray; 5 | font-size: 0.8em; 6 | width: 60vw; 7 | overflow: scroll; 8 | } 9 | 10 | .webedit { 11 | padding: 0.5em; 12 | height: 40vh; 13 | background-color: lightgray; 14 | font-size: 0.8em; 15 | width: 30vw; 16 | overflow: scroll; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /PlanetaryDocs/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/Index.razor.css: -------------------------------------------------------------------------------- 1 | .fixed { 2 | height: 70vh; 3 | width: 70vw; 4 | overflow-y: auto; 5 | overflow-x: hidden; 6 | } 7 | 8 | .result { 9 | border-top: solid 1px cyan; 10 | } 11 | 12 | .result:hover { 13 | background-color: lightgray; 14 | } 15 | 16 | .clickable { 17 | cursor: pointer; 18 | } 19 | 20 | .loading { 21 | background-color: yellow; 22 | border: solid 1px red thin; 23 | } -------------------------------------------------------------------------------- /PlanetaryDocs/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using PlanetaryDocs 10 | @using PlanetaryDocs.Services 11 | @using PlanetaryDocs.Shared 12 | @using PlanetaryDocs.Domain 13 | -------------------------------------------------------------------------------- /PlanetaryDocs/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "CosmosSettings": { 11 | "EndPoint": "https://localhost:8081/", 12 | "AccessKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", 13 | "EnableMigration": true, 14 | "DocumentToCheck": "web-api_index" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Jeremy Likness", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the MIT License. See LICENSE in the repository root for license information.", 7 | "xmlHeader": false, 8 | "documentInterfaces": false, 9 | "documentInternalElements": false 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /PlanetaryDocs.DataAccess/PlanetaryDocs.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 10.0 6 | Data access for Planetary Docs. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/IDocSummaries.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace PlanetaryDocs.Domain 7 | { 8 | /// 9 | /// Indicates classes with summaries. 10 | /// 11 | public interface IDocSummaries 12 | { 13 | /// 14 | /// Gets the list of summaries. 15 | /// 16 | List Documents { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PlanetaryDocsLoader/PlanetaryDocsLoader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 10.0 7 | Seed data for the Azure Cosmos DB database. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /PlanetaryDocs/PlanetaryDocs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Blazor Server application to demonstrate Create, Read, Update, and Delete (CRUD) for an Azure Cosmos DB database using Entity Framework Core. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/js/markdownExtensions.js: -------------------------------------------------------------------------------- 1 | window.markdownExtensions = { 2 | toHtml: (txt, target) => { 3 | const area = document.createElement("textarea"); 4 | setTimeout(() => { 5 | area.innerHTML = txt; 6 | target.innerHTML = area.value; 7 | }, 0); 8 | }, 9 | setText: (id, txt, target) => { 10 | target.value = txt; 11 | target.oninput = () => window.markdownExtensions.getText(id, target); 12 | }, 13 | getText: (id, target) => DotNet.invokeMethodAsync( 14 | 'PlanetaryDocs', 15 | 'UpdateTextAsync', 16 | id, 17 | target.value) 18 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inject LoadingService LoadingService 2 | 3 | @inherits LayoutComponentBase 4 | 5 |
6 | 9 | 10 |
11 | 14 |
15 | @Body 16 |
17 |
18 |
19 |
20 | 21 | @code { 22 | protected override void OnInitialized() 23 | { 24 | LoadingService.StateChangedCallback = StateHasChanged; 25 | base.OnInitialized(); 26 | } 27 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/Add.razor: -------------------------------------------------------------------------------- 1 | @page "/Add" 2 | 3 | @inherits AddBase 4 | 5 |
6 | @if (Saving) 7 | { 8 | 9 | } 10 | else if (Loading) 11 | { 12 | 13 | } 14 | else 15 | { 16 | 20 |
21 |
22 | 27 |
28 |
29 | } 30 |
-------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: .NET 6 Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | if: "!contains(github.event.head_commit.message, '#skip_ci')" 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Fetch 18 | run: git fetch --unshallow 19 | - name: Setup .NET Core 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 6.0.x 23 | include-prerelease: true 24 | source-url: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json 25 | env: 26 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | - name: Run all tests 28 | run: dotnet test PlanetaryDocs.sln --logger trx 29 | -------------------------------------------------------------------------------- /PlanetaryDocs/Services/KeyNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | namespace PlanetaryDocs.Services 5 | { 6 | /// 7 | /// Key names for input processing. 8 | /// 9 | public static class KeyNames 10 | { 11 | /// 12 | /// Enter key. 13 | /// 14 | public const string Enter = nameof(Enter); 15 | 16 | /// 17 | /// Arrow down key. 18 | /// 19 | public const string ArrowDown = nameof(ArrowDown); 20 | 21 | /// 22 | /// Arrow up key. 23 | /// 24 | public const string ArrowUp = nameof(ArrowUp); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PlanetaryDocs/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:7084", 7 | "sslPort": 44309 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "PlanetaryDocs": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/ValidationState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | namespace PlanetaryDocs.Domain 5 | { 6 | /// 7 | /// The result of a validation. 8 | /// 9 | public class ValidationState 10 | { 11 | /// 12 | /// Gets or sets a value indicating whether the property is valid. 13 | /// 14 | public bool IsValid { get; set; } 15 | 16 | /// 17 | /// Gets or sets the reason why validation failed. 18 | /// 19 | public string Message { get; set; } 20 | 21 | /// 22 | /// String representation. 23 | /// 24 | /// The string represention. 25 | public override string ToString() => 26 | IsValid ? "Valid" : $"Invalid: {Message}"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/DomainTests/ValidationStateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PlanetaryDocs.Domain; 3 | using Xunit; 4 | 5 | namespace DomainTests 6 | { 7 | public class ValidationStateTests 8 | { 9 | [Theory] 10 | [InlineData(true)] 11 | [InlineData(false)] 12 | public void ToString_Includes_State_And_Message( 13 | bool isValid) 14 | { 15 | // arrange 16 | var state = new ValidationState 17 | { 18 | IsValid = isValid, 19 | Message = Guid.NewGuid().ToString() 20 | }; 21 | 22 | // act 23 | var str = state.ToString(); 24 | 25 | // assert 26 | if (isValid) 27 | { 28 | Assert.Contains("Valid", str); 29 | } 30 | else 31 | { 32 | Assert.Contains("Invalid", str); 33 | Assert.Contains(state.Message, str); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /Tests/DomainTests/DomainTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /PlanetaryDocs/CosmosSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | namespace PlanetaryDocs 5 | { 6 | /// 7 | /// Settings to connect to Cosmos DB. 8 | /// 9 | public class CosmosSettings 10 | { 11 | /// 12 | /// Gets or sets the endpoint. 13 | /// 14 | public string EndPoint { get; set; } 15 | 16 | /// 17 | /// Gets or sets the access key. 18 | /// 19 | public string AccessKey { get; set; } 20 | 21 | /// 22 | /// Gets or sets a value indicating whether startup should check for migrations. 23 | /// 24 | public bool EnableMigration { get; set; } 25 | 26 | /// 27 | /// Gets or sets the id of the document to check for migration. 28 | /// 29 | public string DocumentToCheck { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/AutoComplete.razor: -------------------------------------------------------------------------------- 1 | @inherits AutoCompleteBase 2 | 3 |
4 | 5 | @LabelText 6 | 7 | @if (Selected) 8 | { 9 | 14 | @SelectedValue 15 | 16 | } 17 | else 18 | { 19 | 24 | } 25 | 26 | @if (!Selected && Values != null) 27 | { 28 | foreach (var result in Values) 29 | { 30 |

@result

31 | } 32 | } 33 |
34 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/EditBar.razor: -------------------------------------------------------------------------------- 1 | @inherits EditBarBase 2 | 3 |
4 |
5 | 11 |  (@ChangeCount changes detected)  12 | 17 |   18 | 23 |
24 |
25 |
26 |
27 |   28 |
29 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Services/HistoryService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.JSInterop; 7 | 8 | namespace PlanetaryDocs.Services 9 | { 10 | /// 11 | /// Service to handle back button. 12 | /// 13 | public class HistoryService 14 | { 15 | private readonly Func goBack; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The implementation of the JavaScript runtime. 21 | public HistoryService(IJSRuntime jsRuntime) => 22 | goBack = () => jsRuntime.InvokeVoidAsync("history.go", "-1"); 23 | 24 | /// 25 | /// Exposes the go back function. 26 | /// 27 | /// An asynchronous task. 28 | public ValueTask GoBackAsync() => goBack(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jeremy Likness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PlanetaryDocsLoader/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using PlanetaryDocs.Domain; 8 | using PlanetaryDocsLoader; 9 | 10 | // path to repository 11 | const string DocsPath = @"C:\path\to\aspnetcore.docs"; 12 | 13 | // Azure Cosmos DB endpoint 14 | const string EndPoint = "https://.documents.azure.com:443/"; 15 | 16 | // Secret key for Azure Cosmos DB 17 | const string AccessKey = ""; 18 | 19 | // set to true to re-run tests without rebuilding db 20 | var testsOnly = false; 21 | 22 | if (!testsOnly && !Directory.Exists(DocsPath)) 23 | { 24 | Console.WriteLine($"Invalid path to docs: {DocsPath}"); 25 | return; 26 | } 27 | 28 | List docsList = null; 29 | 30 | if (!testsOnly) 31 | { 32 | var filesToParse = FileSystemParser.FindCandidateFiles(DocsPath); 33 | docsList = MarkdownParser.ParseFiles(filesToParse); 34 | } 35 | 36 | await CosmosLoader.LoadDocumentsAsync(docsList, EndPoint, AccessKey); 37 | -------------------------------------------------------------------------------- /PlanetaryDocs/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace PlanetaryDocs 8 | { 9 | /// 10 | /// Main program. 11 | /// 12 | public static class Program 13 | { 14 | /// 15 | /// Main method. 16 | /// 17 | /// Arguments passed in. 18 | public static void Main(string[] args) => 19 | CreateHostBuilder(args).Build().Run(); 20 | 21 | /// 22 | /// Create the host builder for the app. 23 | /// 24 | /// Command line arguments. 25 | /// The instance. 26 | public static IHostBuilder CreateHostBuilder(string[] args) => 27 | Host.CreateDefaultBuilder(args) 28 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/ErrorModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Diagnostics; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace PlanetaryDocs.Pages 9 | { 10 | /// 11 | /// The templated error page. 12 | /// 13 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 14 | [IgnoreAntiforgeryToken] 15 | public class ErrorModel : PageModel 16 | { 17 | /// 18 | /// Gets or sets the request identifier. 19 | /// 20 | public string RequestId { get; set; } 21 | 22 | /// 23 | /// Gets a value indicating whether to show the request. 24 | /// 25 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 26 | 27 | /// 28 | /// On get method. 29 | /// 30 | public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/ValidatedInput.razor: -------------------------------------------------------------------------------- 1 | @inherits ValidatedInputBase 2 | 3 |
4 |
5 |
6 | @if (UseTextArea) 7 | { 8 | 16 | } 17 | else 18 | { 19 | 25 | } 26 |
27 |
28 | @if (Validation != null && Validation.IsValid == false) 29 | { 30 |
31 |
32 |

@Validation.Message

33 |
34 |
35 | } 36 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Shared/TagPicker.razor: -------------------------------------------------------------------------------- 1 | @inherits TagPickerBase 2 | 3 | @if (Tags != null && Tags.Count > 0) 4 | { 5 | foreach (var tag in Tags) 6 | { 7 | 8 | "@tag" [x] 13 |   14 | 15 | } 16 | } 17 | @if (PickNew) 18 | { 19 | 21 | } 22 | else 23 | { 24 | Pick Existing 28 | } 29 |   30 | [ 34 | @if (string.IsNullOrWhiteSpace(AddTag)) 35 | { 36 |   37 | } 38 | else 39 | { 40 | + 45 | }] -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 6 Builds 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | if: "!contains(github.event.head_commit.message, '#skip_ci')" 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Fetch 22 | run: git fetch --unshallow 23 | - name: Setup .NET Core 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: 6.0.x 27 | include-prerelease: true 28 | source-url: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json 29 | env: 30 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | - name: install versioning tool 32 | run: dotnet tool install --tool-path . nbgv 33 | - name: Install dependencies 34 | run: dotnet restore 35 | - name: Build Loader 36 | run: dotnet build --configuration Release --no-restore PlanetaryDocsLoader/PlanetaryDocsLoader.csproj 37 | - name: Build Blazor Server App 38 | run: dotnet build --configuration Release --no-restore PlanetaryDocs/PlanetaryDocs.csproj 39 | 40 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace PlanetaryDocs.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | Planetary Docs 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | An error has occurred. This application may no longer respond until reloaded. 25 | 26 | 27 | An unhandled exception has occurred. See browser dev tools for details. 28 | 29 | Reload 30 | 🗙 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 400px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | 64 | .version { 65 | font-size: 0.7em; 66 | color: white; 67 | text-align: center; 68 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/DocHistory.razor: -------------------------------------------------------------------------------- 1 | @inherits DocHistoryBase 2 | 3 | @if (History != null) 4 | { 5 |
6 | @if (History.Count == 0) 7 | { 8 |
9 |
10 |
 No audits found for the document.
11 |
12 |
13 | } 14 | @if (History.Count > 0) 15 | { 16 |
17 |
18 |
 @History.Count audit@(History.Count == 1 ? string.Empty : "s") found for the document.
19 |
20 |
21 |
22 |
Date Modified
23 |
Alias
24 |
Title
25 |
26 | @foreach (var item in History) 27 | { 28 |
29 |
@item.Timestamp
30 |
@item.Alias
31 |
@item.Title
32 |
33 | } 34 | } 35 |
36 | } 37 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | body::before { 8 | content: ""; 9 | background-image: url(../images/m42hubble.jpg); 10 | background-size: cover; 11 | position: absolute; 12 | top: 0px; 13 | right: 0px; 14 | bottom: 0px; 15 | left: 0px; 16 | opacity: 0.5; 17 | } 18 | 19 | 20 | a, .btn-link { 21 | color: #0366d6; 22 | } 23 | 24 | .btn-primary { 25 | color: #fff; 26 | background-color: #1b6ec2; 27 | border-color: #1861ac; 28 | } 29 | 30 | .content { 31 | padding-top: 1.1rem; 32 | height: 100%; 33 | } 34 | 35 | .valid.modified:not([type=checkbox]) { 36 | outline: 1px solid #26b050; 37 | } 38 | 39 | .invalid { 40 | outline: 1px solid red; 41 | } 42 | 43 | .validation-message { 44 | color: red; 45 | } 46 | 47 | #blazor-error-ui { 48 | background: lightyellow; 49 | bottom: 0; 50 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 51 | display: none; 52 | left: 0; 53 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 54 | position: fixed; 55 | width: 100%; 56 | z-index: 1000; 57 | } 58 | 59 | #blazor-error-ui .dismiss { 60 | cursor: pointer; 61 | position: absolute; 62 | right: 0.75rem; 63 | top: 0.5rem; 64 | } 65 | 66 | .row { 67 | padding: 0.2em; 68 | } -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/Edit.razor: -------------------------------------------------------------------------------- 1 | @page "/Edit/{Uid}" 2 | 3 | @inherits EditBase 4 | 5 |
6 | @if (NotFound) 7 | { 8 |
9 |
10 |
11 |  A document with the id '@Uid' was not found. 12 |
13 |
14 |
15 | } 16 | else if (Saving) 17 | { 18 | 19 | } 20 | else if (Loading) 21 | { 22 | 23 | } 24 | else 25 | { 26 | 30 | @if (Concurrency) 31 | { 32 |
33 |
34 |
35 |  The document was updated since it was last loaded. You can save again to overwrite changes or reset to reload the document and view the changes. 36 |
37 |
38 |
39 | } 40 |
41 |
42 | 46 |
47 |
48 | } 49 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @inherits NavMenuBase 2 | 3 | 14 | 15 |
16 | 40 |
41 | 42 |
43 | version: @Version 44 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model PlanetaryDocs.Pages.ErrorModel 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Error 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

Error.

20 |

An error occurred while processing your request.

21 | 22 | @if (Model.ShowRequestId) 23 | { 24 |

25 | Request ID: @Model.RequestId 26 |

27 | } 28 | 29 |

Development Mode

30 |

31 | Swapping to the Development environment displays detailed information about the error that occurred. 32 |

33 |

34 | The Development environment shouldn't be enabled for deployed applications. 35 | It can result in displaying sensitive information from exceptions to end users. 36 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 37 | and restarting the app. 38 |

39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | (c) Copyright 2021 Jeremy Likness. 5 | MIT 6 | Jeremy Likness 7 | 9.0 8 | true 9 | 10 | 11 | 12 | net5.0 13 | 14 | 15 | 16 | true 17 | 18 | 19 | 20 | https://github.com/JeremyLikness/PlanetaryDocs 21 | git 22 | Planetary Docs 23 | true 24 | false 25 | 26 | 27 | 28 | 29 | 3.4.194 30 | all 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers; buildtransitive 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/Author.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace PlanetaryDocs.Domain 7 | { 8 | /// 9 | /// A document author. 10 | /// 11 | public class Author : IDocSummaries 12 | { 13 | /// 14 | /// Gets or sets the alias. 15 | /// 16 | public string Alias { get; set; } 17 | 18 | /// 19 | /// Gets the list of documents by this author. 20 | /// 21 | public List Documents { get; } 22 | = new List(); 23 | 24 | /// 25 | /// Gets or sets the concurrency tag. 26 | /// 27 | public string ETag { get; set; } 28 | 29 | /// 30 | /// Gets the hash code. 31 | /// 32 | /// The hashcode of the alias. 33 | public override int GetHashCode() => Alias.GetHashCode(); 34 | 35 | /// 36 | /// Implements equality. 37 | /// 38 | /// The object to compare. 39 | /// A value indicating whether the aliases match. 40 | public override bool Equals(object obj) => 41 | obj is Author author && author.Alias == Alias; 42 | 43 | /// 44 | /// Gets the string representation. 45 | /// 46 | /// The string representation. 47 | public override string ToString() => 48 | $"Author {Alias} has {Documents.Count} documents."; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/Tag.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace PlanetaryDocs.Domain 7 | { 8 | /// 9 | /// A tag. 10 | /// 11 | public class Tag : IDocSummaries 12 | { 13 | /// 14 | /// Gets or sets the name of the tag. 15 | /// 16 | public string TagName { get; set; } 17 | 18 | /// 19 | /// Gets or sets a summary of documents with the tag. 20 | /// 21 | public List Documents { get; set; } 22 | = new List(); 23 | 24 | /// 25 | /// Gets or sets the concurrency token. 26 | /// 27 | public string ETag { get; set; } 28 | 29 | /// 30 | /// Gets the hash code. 31 | /// 32 | /// The hash code of the tag name. 33 | public override int GetHashCode() => TagName.GetHashCode(); 34 | 35 | /// 36 | /// Implements equality. 37 | /// 38 | /// The object to compare to. 39 | /// A value indicating whether the tag names match. 40 | public override bool Equals(object obj) => 41 | obj is Tag tag && tag.TagName == TagName; 42 | 43 | /// 44 | /// Gets the string representation. 45 | /// 46 | /// The string representation. 47 | public override string ToString() => 48 | $"Tag {TagName} tagged by {Documents.Count} documents."; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PlanetaryDocs/Services/TitleService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.JSInterop; 7 | 8 | namespace PlanetaryDocs.Services 9 | { 10 | /// 11 | /// Service to set the browser title. 12 | /// 13 | public class TitleService 14 | { 15 | private const string DefaultTitle = "Planetary Docs"; 16 | private readonly NavigationManager navigationManager; 17 | private readonly IJSRuntime jsRuntime; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The . 23 | /// The JavaScript runtime. 24 | public TitleService( 25 | NavigationManager manager, 26 | IJSRuntime jsRuntime) 27 | { 28 | navigationManager = manager; 29 | navigationManager.LocationChanged += async (o, e) => 30 | await SetTitleAsync(DefaultTitle); 31 | this.jsRuntime = jsRuntime; 32 | } 33 | 34 | /// 35 | /// Gets or sets the title. 36 | /// 37 | public string Title { get; set; } 38 | 39 | /// 40 | /// Main task to set the title. 41 | /// 42 | /// The title to use. 43 | /// An asynchronous task. 44 | public async Task SetTitleAsync(string title) 45 | { 46 | Title = title; 47 | await jsRuntime.InvokeVoidAsync("titleService.setTitle", title); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/DocumentAudit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Text.Json; 6 | 7 | namespace PlanetaryDocs.Domain 8 | { 9 | /// 10 | /// Represents a snapshot of the document. 11 | /// 12 | public class DocumentAudit 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public DocumentAudit() 18 | { 19 | } 20 | 21 | /// 22 | /// Initializes a new instance of the class 23 | /// and configures it with the passed in. 24 | /// 25 | /// The document to audit. 26 | public DocumentAudit(Document document) 27 | { 28 | Id = Guid.NewGuid(); 29 | Uid = document.Uid; 30 | Document = JsonSerializer.Serialize(document); 31 | Timestamp = DateTimeOffset.UtcNow; 32 | } 33 | 34 | /// 35 | /// Gets or sets a unique identifier. 36 | /// 37 | public Guid Id { get; set; } 38 | 39 | /// 40 | /// Gets or sets the identifier of the document. 41 | /// 42 | public string Uid { get; set; } 43 | 44 | /// 45 | /// Gets or sets the timestamp of the audit. 46 | /// 47 | public DateTimeOffset Timestamp { get; set; } 48 | 49 | /// 50 | /// Gets or sets the JSON serialized snapshot. 51 | /// 52 | public string Document { get; set; } 53 | 54 | /// 55 | /// Deserializes the snapshot. 56 | /// 57 | /// The snapshot. 58 | public Document GetDocumentSnapshot() => 59 | JsonSerializer.Deserialize(Document); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/EditBarBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Services; 8 | 9 | namespace PlanetaryDocs.Shared 10 | { 11 | /// 12 | /// Code for the component. 13 | /// 14 | public class EditBarBase : ComponentBase 15 | { 16 | /// 17 | /// Gets or sets the . 18 | /// 19 | [Inject] 20 | public NavigationManager NavigationService { get; set; } 21 | 22 | /// 23 | /// Gets or sets the . 24 | /// 25 | [Inject] 26 | public HistoryService HistoryService { get; set; } 27 | 28 | /// 29 | /// Gets or sets a value indicating whether the document 30 | /// has been modified. 31 | /// 32 | [Parameter] 33 | public bool IsDirty { get; set; } 34 | 35 | /// 36 | /// Gets or sets a value indicating whether the document is in a valid 37 | /// state to update or insert. 38 | /// 39 | [Parameter] 40 | public bool IsValid { get; set; } 41 | 42 | /// 43 | /// Gets or sets the count of changes that are detected. 44 | /// 45 | [Parameter] 46 | public int ChangeCount { get; set; } 47 | 48 | /// 49 | /// Gets or sets the method to call to commit (save) changes. 50 | /// 51 | [Parameter] 52 | public Func SaveAsync { get; set; } 53 | 54 | /// 55 | /// Reset method returns to pre-edited state. 56 | /// 57 | protected void Reset() => NavigationService.NavigateTo( 58 | NavigationService.Uri, 59 | true); 60 | 61 | /// 62 | /// Cancel goes back to previous page. 63 | /// 64 | protected void Cancel() => InvokeAsync(async () => 65 | await HistoryService.GoBackAsync()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /PlanetaryDocsLoader/FileSystemParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace PlanetaryDocsLoader 9 | { 10 | /// 11 | /// Handles recursing the docs repo and parsing markdown files. 12 | /// 13 | public static class FileSystemParser 14 | { 15 | /// 16 | /// Produces a list of files with the .md markdown extension. 17 | /// 18 | /// The path to start. 19 | /// 20 | /// Verbose. Outputs ":" for top level directory, "#" for subdirectories 21 | /// found, "!" for markdown files and "-" for skipped. 22 | /// 23 | /// A unique list of file names. 24 | public static HashSet FindCandidateFiles(string docsPath) 25 | { 26 | var dirsToVisit = new Stack(); 27 | var filesToParse = new HashSet(); 28 | 29 | Console.WriteLine("Recursing folders..."); 30 | dirsToVisit.Push(docsPath); 31 | 32 | while (dirsToVisit.Count > 0) 33 | { 34 | var dir = dirsToVisit.Pop(); 35 | Console.Write(":"); 36 | 37 | foreach (var subDirectory in Directory.EnumerateDirectories(dir)) 38 | { 39 | Console.Write("#"); 40 | dirsToVisit.Push(subDirectory); 41 | } 42 | 43 | foreach (var file in Directory.EnumerateFiles(dir)) 44 | { 45 | if (Path.GetExtension(file) == ".md") 46 | { 47 | Console.Write("!"); 48 | filesToParse.Add(file); 49 | } 50 | else 51 | { 52 | Console.Write("-"); 53 | } 54 | } 55 | } 56 | 57 | Console.WriteLine(); 58 | Console.WriteLine($"Found {filesToParse.Count} candidate documents."); 59 | 60 | return filesToParse; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/DocumentSummary.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | namespace PlanetaryDocs.Domain 5 | { 6 | /// 7 | /// Represents a summary of a document. 8 | /// 9 | public class DocumentSummary 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public DocumentSummary() 15 | { 16 | } 17 | 18 | /// 19 | /// Initializes a new instance of the class 20 | /// and initializes it with the . 21 | /// 22 | /// The to summarize. 23 | public DocumentSummary(Document doc) 24 | { 25 | Uid = doc.Uid; 26 | Title = doc.Title; 27 | AuthorAlias = doc.AuthorAlias; 28 | } 29 | 30 | /// 31 | /// Gets or sets the unique id of the . 32 | /// 33 | public string Uid { get; set; } 34 | 35 | /// 36 | /// Gets or sets the title. 37 | /// 38 | public string Title { get; set; } 39 | 40 | /// 41 | /// Gets or sets the alias of the author. 42 | /// 43 | public string AuthorAlias { get; set; } 44 | 45 | /// 46 | /// Gets the hash code. 47 | /// 48 | /// The hash code of the document identifier. 49 | public override int GetHashCode() => Uid.GetHashCode(); 50 | 51 | /// 52 | /// Implements equality. 53 | /// 54 | /// The object to compare to. 55 | /// A value indicating whether the unique identifiers match. 56 | public override bool Equals(object obj) => 57 | obj is DocumentSummary ds && ds.Uid == Uid; 58 | 59 | /// 60 | /// Gets the string representation. 61 | /// 62 | /// The string representation. 63 | public override string ToString() => $"Summary for {Uid} by {AuthorAlias}: {Title}."; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/TagSearchBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Domain; 8 | using PlanetaryDocs.Services; 9 | 10 | namespace PlanetaryDocs.Shared 11 | { 12 | /// 13 | /// Code for the component. 14 | /// 15 | public class TagSearchBase : ComponentBase 16 | { 17 | private string tag; 18 | 19 | /// 20 | /// Gets or sets the implementation of . 21 | /// 22 | [Inject] 23 | public IDocumentService DocumentService { get; set; } 24 | 25 | /// 26 | /// Gets or sets the . 27 | /// 28 | [CascadingParameter] 29 | public LoadingService LoadingService { get; set; } 30 | 31 | /// 32 | /// Gets or sets the tab index. 33 | /// 34 | [Parameter] 35 | public string TabIndex { get; set; } 36 | 37 | /// 38 | /// Gets or sets the selected tag. 39 | /// 40 | [Parameter] 41 | public string Tag 42 | { 43 | get => tag; 44 | set 45 | { 46 | if (value != tag) 47 | { 48 | tag = value; 49 | InvokeAsync( 50 | async () => 51 | await TagChanged.InvokeAsync(tag)); 52 | } 53 | } 54 | } 55 | 56 | /// 57 | /// Gets or sets the callback to notify on tag changes. 58 | /// 59 | [Parameter] 60 | public EventCallback TagChanged { get; set; } 61 | 62 | /// 63 | /// Call the search and obtain results. 64 | /// 65 | /// The text to search. 66 | /// The list of results. 67 | public async Task> SearchAsync(string searchText) 68 | { 69 | List results = null; 70 | 71 | if (string.IsNullOrWhiteSpace(searchText)) 72 | { 73 | return results; 74 | } 75 | 76 | await LoadingService.WrapExecutionAsync( 77 | async () => results = 78 | await DocumentService.SearchTagsAsync(searchText)); 79 | 80 | return results; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/Document.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace PlanetaryDocs.Domain 8 | { 9 | /// 10 | /// A document item. 11 | /// 12 | public class Document 13 | { 14 | /// 15 | /// Gets or sets the unique identifier. 16 | /// 17 | public string Uid { get; set; } 18 | 19 | /// 20 | /// Gets or sets the title. 21 | /// 22 | public string Title { get; set; } 23 | 24 | /// 25 | /// Gets or sets the description. 26 | /// 27 | public string Description { get; set; } 28 | 29 | /// 30 | /// Gets or sets the published date. 31 | /// 32 | public DateTime PublishDate { get; set; } 33 | 34 | /// 35 | /// Gets or sets the markdown content. 36 | /// 37 | public string Markdown { get; set; } 38 | 39 | /// 40 | /// Gets or sets the generated html. 41 | /// 42 | public string Html { get; set; } 43 | 44 | /// 45 | /// Gets or sets the author's alias. 46 | /// 47 | public string AuthorAlias { get; set; } 48 | 49 | /// 50 | /// Gets or sets the list of related tags. 51 | /// 52 | public List Tags { get; set; } 53 | = new List(); 54 | 55 | /// 56 | /// Gets or sets the concurrency token. 57 | /// 58 | public string ETag { get; set; } 59 | 60 | /// 61 | /// Gets the hash code. 62 | /// 63 | /// The hash code of the unique identifier. 64 | public override int GetHashCode() => Uid.GetHashCode(); 65 | 66 | /// 67 | /// Implements equality. 68 | /// 69 | /// The object to compare to. 70 | /// A value indicating whether the unique identifiers match. 71 | public override bool Equals(object obj) => 72 | obj is Document document && document.Uid == Uid; 73 | 74 | /// 75 | /// Gets the string representation. 76 | /// 77 | /// The string representation. 78 | public override string ToString() => 79 | $"Document {Uid} by {AuthorAlias} with {Tags.Count} tags: {Title}."; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | One of the easiest ways to contribute is to participate in discussions on GitHub issues. You can also contribute by submitting pull requests with code changes. 4 | 5 | ## General feedback and discussions? 6 | 7 | Start a discussion on the [Planetry Docs issue tracker](https://github.com/JeremyLikness/PlanetaryDocs/issues). 8 | 9 | ## Bugs and feature requests? 10 | 11 | To request a feature or report a new bug, [file a new issue](https://github.com/JeremyLikness/PlanetaryDocs/issues/new/choose) and follow the template suggestions. 12 | 13 | ## Contributing code and content 14 | 15 | We accept fixes and features! See items with [help wanted](https://github.com/JeremyLikness/PlanetaryDocs/labels/help%20wanted) and features or bug fixes 16 | we think make [a good first issue](https://github.com/JeremyLikness/PlanetaryDocs/labels/good%20first%20issue). 17 | 18 | ### Identifying the scale 19 | 20 | If you would like to contribute to one of our repositories, first identify the scale of what you would like to contribute. If it is small (grammar/spelling or a bug fix) feel 21 | free to start working on a fix. If you are submitting a feature or substantial code contribution, please discuss it with the team and ensure it follows the product roadmap. 22 | You might also read these two blogs posts on contributing code: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza 23 | and [Don't "Push" Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. All code submissions will be rigorously reviewed 24 | and tested by the Expression Power Tools team before being merged into the source. 25 | 26 | ### Submitting a pull request 27 | 28 | If you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests. Make sure the repository can build and all tests pass. 29 | Familiarize yourself with the project workflow and our coding conventions. The project contains an [`.editorConfig`](https://github.com/JeremyLikness/PlanetaryDocs/blob/master/.editorconfig) 30 | that contains rules for conventions and is recognized automatically by Visual Studio 2019. 31 | 32 | In general, the checklist for pull requests is: 33 | 34 | - Check with the maintainers for larger requests 35 | - Ensure the project compiles in _both_ `Debug` and `Release` modes 36 | - All files added should have complete XML documentation with examples where appropriate and the required heading 37 | - Fill out the PR template as directed 38 | 39 | Project maintainers will review PRs and merge/update versions as appropriate. 40 | 41 | ## Code of conduct 42 | 43 | See [Code of Conduct](https://github.com/JeremyLikness/PlanetaryDocs/blob/master/CODE_OF_CONDUCT.md) 44 | -------------------------------------------------------------------------------- /Tests/DomainTests/AuthorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PlanetaryDocs.Domain; 4 | using Xunit; 5 | 6 | namespace DomainTests 7 | { 8 | public class AuthorTests 9 | { 10 | [Fact] 11 | public void New_Instance_Initializes_Documents() 12 | { 13 | // arange and act 14 | var author = new Author(); 15 | 16 | // assert 17 | Assert.NotNull(author.Documents); 18 | } 19 | 20 | [Fact] 21 | public void HashCode_Of_Author_Is_HashCode_Of_Alias() 22 | { 23 | // arrange 24 | const string alias = nameof(AuthorTests); 25 | var author = new Author { Alias = alias }; 26 | 27 | // act 28 | var expected = alias.GetHashCode(); 29 | var actual = author.GetHashCode(); 30 | 31 | // assert 32 | Assert.Equal(expected, actual); 33 | } 34 | 35 | public static IEnumerable AuthorEqualityTests() 36 | { 37 | yield return new object[] 38 | { 39 | new Author { Alias = nameof(AuthorTests) }, true 40 | }; 41 | 42 | yield return new object[] 43 | { 44 | new Author { Alias = nameof(AuthorEqualityTests) }, false 45 | }; 46 | 47 | yield return new object[] 48 | { 49 | new object(), false 50 | }; 51 | 52 | yield break; 53 | } 54 | 55 | [Theory] 56 | [MemberData(nameof(AuthorEqualityTests))] 57 | public void Equality_Compares_Type_And_Alias(object compare, bool equal) 58 | { 59 | // arrange 60 | var author = new Author { Alias = nameof(AuthorTests) }; 61 | 62 | // act 63 | var areEqual = author.Equals(compare); 64 | 65 | // assert 66 | Assert.Equal(equal, areEqual); 67 | } 68 | 69 | [Theory] 70 | [InlineData("jeremy", 1)] 71 | [InlineData("jeremy", 2)] 72 | [InlineData("randomperson", 3)] 73 | public void Author_ToString_Includes_Alias_And_Docs_Count( 74 | string alias, 75 | int expectedDocs) 76 | { 77 | // arrange 78 | var author = new Author { Alias = alias }; 79 | for (var idx = 0; idx < expectedDocs; idx++) 80 | { 81 | var summary = new DocumentSummary 82 | { 83 | AuthorAlias = alias, 84 | Title = Guid.NewGuid().ToString(), 85 | }; 86 | summary.Uid = summary.Title.Split('-')[^1]; 87 | author.Documents.Add(summary); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/DocHistoryBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Domain; 8 | using PlanetaryDocs.Services; 9 | 10 | namespace PlanetaryDocs.Shared 11 | { 12 | /// 13 | /// Code for the component. 14 | /// 15 | public class DocHistoryBase : ComponentBase 16 | { 17 | private string lastUid = string.Empty; 18 | 19 | /// 20 | /// Gets or sets the . 21 | /// 22 | [CascadingParameter] 23 | public LoadingService LoadingService { get; set; } 24 | 25 | /// 26 | /// Gets or sets the implementation of . 27 | /// 28 | [Inject] 29 | public IDocumentService DocumentService { get; set; } 30 | 31 | /// 32 | /// Gets or sets the . 33 | /// 34 | [Inject] 35 | public NavigationManager NavigationService { get; set; } 36 | 37 | /// 38 | /// Gets or sets the unique identifier of the to get history for. 39 | /// 40 | [Parameter] 41 | public string Uid { get; set; } 42 | 43 | /// 44 | /// Gets or sets the list of audit items that show the history of 45 | /// the . 46 | /// 47 | protected List History { get; set; } = null; 48 | 49 | /// 50 | /// Called when parameters change. 51 | /// 52 | /// The asynchronous test. 53 | protected override async Task OnParametersSetAsync() 54 | { 55 | if (Uid != lastUid) 56 | { 57 | lastUid = Uid; 58 | await LoadingService.WrapExecutionAsync( 59 | async () => 60 | History = 61 | await DocumentService.LoadDocumentHistoryAsync(lastUid)); 62 | } 63 | 64 | await base.OnParametersSetAsync(); 65 | } 66 | 67 | /// 68 | /// Navigate to a specific audit entry. 69 | /// 70 | /// The . 71 | protected void Navigate(DocumentAuditSummary audit) => 72 | NavigationService.NavigateTo( 73 | NavigationHelper.ViewDocument(audit.Uid, audit.Id), true); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/HtmlPreviewBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Components; 6 | 7 | namespace PlanetaryDocs.Shared 8 | { 9 | /// 10 | /// Code for the HTML preview component. 11 | /// 12 | public class HtmlPreviewBase : ComponentBase 13 | { 14 | private bool rendered; 15 | private string html; 16 | 17 | /// 18 | /// Gets or sets a value indicating whether the preview is shown in an 19 | /// edit context. 20 | /// 21 | [Parameter] 22 | public bool IsEdit { get; set; } 23 | 24 | /// 25 | /// Gets or sets the HTML to preview. 26 | /// 27 | [Parameter] 28 | public string Html 29 | { 30 | get => html; 31 | set 32 | { 33 | if (value != html) 34 | { 35 | html = value; 36 | HtmlToRender = new MarkupString(html); 37 | } 38 | } 39 | } 40 | 41 | /// 42 | /// Gets the HTML to render. 43 | /// 44 | protected MarkupString HtmlToRender { get; private set; } 45 | 46 | /// 47 | /// Gets the CSS class to use based on the mode. 48 | /// 49 | protected string WebClass => IsEdit ? "webedit" : "web"; 50 | 51 | /// 52 | /// Method to update the preview. 53 | /// 54 | /// 55 | /// This component uses a "trick" to render HTML by inserting it into a 56 | /// textarea element then moving the value over. The code is in 57 | /// wwwroot/js/markdownExtensions.js. 58 | /// 59 | /// The asynchronous task. 60 | public async Task OnUpdateAsync() 61 | { 62 | if (rendered) 63 | { 64 | await InvokeAsync(() => HtmlToRender = new MarkupString(Html)); 65 | } 66 | } 67 | 68 | /// 69 | /// Called after rendering. 70 | /// 71 | /// A value indicating whether it is the first render. 72 | /// The asynchronous task. 73 | protected override async Task OnAfterRenderAsync(bool firstRender) 74 | { 75 | if (firstRender) 76 | { 77 | rendered = true; 78 | await OnUpdateAsync(); 79 | } 80 | 81 | await base.OnAfterRenderAsync(firstRender); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /PlanetaryDocs/Services/NavigationHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Web = System.Net.WebUtility; 8 | 9 | namespace PlanetaryDocs.Services 10 | { 11 | /// 12 | /// Helper for creating navigation links. 13 | /// 14 | public static class NavigationHelper 15 | { 16 | /// 17 | /// Link to view a document. 18 | /// 19 | /// The unique identifier. 20 | /// The audit id. 21 | /// The view link. 22 | public static string ViewDocument( 23 | string uid, 24 | Guid auditId = default) => 25 | auditId == default 26 | ? $"/View/{Web.UrlEncode(uid)}" 27 | : $"/View/{Web.UrlEncode(uid)}?history={Web.UrlEncode(auditId.ToString())}"; 28 | 29 | /// 30 | /// Link to edit a document. 31 | /// 32 | /// The unique identifier. 33 | /// The view link. 34 | public static string EditDocument(string uid) => 35 | $"/Edit/{Web.UrlEncode(uid)}"; 36 | 37 | /// 38 | /// Decomposes the query string. 39 | /// 40 | /// The full uri. 41 | /// The query string values. 42 | public static IDictionary GetQueryString(string uri) 43 | { 44 | var pairs = new Dictionary(); 45 | 46 | var queryString = uri.Split('?'); 47 | 48 | if (queryString.Length < 2) 49 | { 50 | return pairs; 51 | } 52 | 53 | var keyValuePairs = queryString[1].Split('&'); 54 | 55 | foreach (var keyValuePair in keyValuePairs) 56 | { 57 | if (keyValuePair.IndexOf('=') > 0) 58 | { 59 | var pair = keyValuePair.Split('='); 60 | pairs.Add(pair[0], Web.UrlDecode(pair[1])); 61 | } 62 | } 63 | 64 | return pairs; 65 | } 66 | 67 | /// 68 | /// Create a query string from key value pairs. 69 | /// 70 | /// The values to use. 71 | /// The composed query string. 72 | public static string CreateQueryString( 73 | params (string key, string value)[] values) 74 | { 75 | var queryString = 76 | string.Join( 77 | '&', 78 | values.Select( 79 | v => $"{v.key}={Web.UrlEncode(v.value)}")); 80 | return queryString; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/DocumentAuditSummary.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | 6 | namespace PlanetaryDocs.Domain 7 | { 8 | /// 9 | /// Simple class representing a document snapshot. 10 | /// 11 | public class DocumentAuditSummary 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public DocumentAuditSummary() 17 | { 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class 22 | /// and initializes it with the . 23 | /// 24 | /// The to summarize. 25 | public DocumentAuditSummary(DocumentAudit documentAudit) 26 | { 27 | Id = documentAudit.Id; 28 | Uid = documentAudit.Uid; 29 | Timestamp = documentAudit.Timestamp; 30 | var doc = documentAudit.GetDocumentSnapshot(); 31 | Alias = doc.AuthorAlias; 32 | Title = doc.Title; 33 | } 34 | 35 | /// 36 | /// Gets or sets the unique identifier. 37 | /// 38 | public Guid Id { get; set; } 39 | 40 | /// 41 | /// Gets or sets the timestamp of the audit event. 42 | /// 43 | public DateTimeOffset Timestamp { get; set; } 44 | 45 | /// 46 | /// Gets or sets the unique identifier of the document. 47 | /// 48 | public string Uid { get; set; } 49 | 50 | /// 51 | /// Gets or sets the author alias for the snapshot. 52 | /// 53 | public string Alias { get; set; } 54 | 55 | /// 56 | /// Gets or sets the title at the time of the snapshot. 57 | /// 58 | public string Title { get; set; } 59 | 60 | /// 61 | /// Gets the hash code. 62 | /// 63 | /// The hash code of the . 64 | public override int GetHashCode() => Id.GetHashCode(); 65 | 66 | /// 67 | /// Implement equality. 68 | /// 69 | /// The object to compare to. 70 | /// A value indicating whether the object is a with the same . 71 | public override bool Equals(object obj) => 72 | obj is DocumentAuditSummary das && 73 | das.Id == Id; 74 | 75 | /// 76 | /// The string representation. 77 | /// 78 | /// The string representation of the instance. 79 | public override string ToString() => $"Summary for audit {Id} with document {Uid}."; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/DomainTests/TagTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PlanetaryDocs.Domain; 4 | using Xunit; 5 | 6 | namespace DomainTests 7 | { 8 | public class TagTests 9 | { 10 | [Fact] 11 | public void New_Instance_Initializes_Documents() 12 | { 13 | // arrange and act 14 | var tag = new Tag(); 15 | 16 | // assert 17 | Assert.NotNull(tag.Documents); 18 | } 19 | 20 | [Fact] 21 | public void HashCode_Is_HashCode_Of_TagName() 22 | { 23 | // arrange 24 | var tag = new Tag 25 | { 26 | TagName = nameof(TagTests) 27 | }; 28 | 29 | // act 30 | var expected = tag.TagName.GetHashCode(); 31 | var actual = tag.GetHashCode(); 32 | 33 | // assert 34 | Assert.Equal(expected, actual); 35 | } 36 | 37 | public static IEnumerable TagEqualityTests() 38 | { 39 | yield return new object[] 40 | { 41 | new Tag { TagName = nameof(TagTests) }, 42 | true 43 | }; 44 | 45 | yield return new object[] 46 | { 47 | new Tag { TagName = nameof(TagEqualityTests) }, 48 | false 49 | }; 50 | 51 | yield return new object[] 52 | { 53 | new { TagName = nameof(TagTests) }, 54 | false 55 | }; 56 | } 57 | 58 | [Theory] 59 | [MemberData(nameof(TagEqualityTests))] 60 | public void Equality_Is_Based_On_Type_And_TagName( 61 | object target, 62 | bool areEqual) 63 | { 64 | // arrange 65 | var tag = new Tag { TagName = nameof(TagTests) }; 66 | 67 | // act 68 | var equals = tag.Equals(target); 69 | 70 | // assert 71 | Assert.Equal(areEqual, equals); 72 | } 73 | 74 | [Theory] 75 | [InlineData(0)] 76 | [InlineData(1)] 77 | [InlineData(99)] 78 | public void ToString_Includes_TagName_And_Documents_Count(int docCount) 79 | { 80 | // arrange 81 | var tag = new Tag 82 | { 83 | TagName = Guid.NewGuid().ToString() 84 | }; 85 | 86 | for (var idx = 0; idx < docCount; idx++) 87 | { 88 | tag.Documents.Add( 89 | new DocumentSummary 90 | { 91 | Uid = idx.ToString(), 92 | Title = $"Title #{idx}", 93 | AuthorAlias = "test" 94 | }); 95 | } 96 | 97 | // act 98 | var str = tag.ToString(); 99 | 100 | // assert 101 | Assert.Contains(tag.TagName, str); 102 | Assert.Contains(docCount.ToString(), str); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/AliasSearchBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Domain; 8 | using PlanetaryDocs.Services; 9 | 10 | namespace PlanetaryDocs.Shared 11 | { 12 | /// 13 | /// Base code for the component. 14 | /// 15 | public class AliasSearchBase : ComponentBase 16 | { 17 | private string alias; 18 | 19 | /// 20 | /// Gets or sets the . 21 | /// 22 | [CascadingParameter] 23 | public LoadingService LoadingService { get; set; } 24 | 25 | /// 26 | /// Gets or sets the implementation of the . 27 | /// 28 | [Inject] 29 | public IDocumentService DocumentService { get; set; } 30 | 31 | /// 32 | /// Gets or sets the tab index for keyboard navigation. 33 | /// 34 | [Parameter] 35 | public string TabIndex { get; set; } 36 | 37 | /// 38 | /// Gets or sets the alias selected. 39 | /// 40 | [Parameter] 41 | public string Alias 42 | { 43 | get => alias; 44 | set 45 | { 46 | if (value != alias) 47 | { 48 | alias = value; 49 | InvokeAsync(async () => await AliasChanged.InvokeAsync(alias)); 50 | } 51 | } 52 | } 53 | 54 | /// 55 | /// Gets or sets the callback to notify on alias selection changes. 56 | /// 57 | [Parameter] 58 | public EventCallback AliasChanged { get; set; } 59 | 60 | /// 61 | /// Gets or sets the reference to the child 62 | /// component. 63 | /// 64 | protected AutoComplete AutoComplete { get; set; } 65 | 66 | /// 67 | /// Search alias based on the text passed in. 68 | /// 69 | /// The search text. 70 | /// The list of matching aliases. 71 | public async Task> SearchAsync(string searchText) 72 | { 73 | List results = null; 74 | 75 | if (string.IsNullOrWhiteSpace(searchText)) 76 | { 77 | return results; 78 | } 79 | 80 | await LoadingService.WrapExecutionAsync( 81 | async () => results = 82 | await DocumentService.SearchAuthorsAsync(searchText)); 83 | 84 | return results; 85 | } 86 | 87 | /// 88 | /// Method to set focus to component. 89 | /// 90 | /// The asynchronous task. 91 | public async Task FocusAsync() => await AutoComplete.FocusAsync(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/MultiLineEditBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Services; 8 | 9 | namespace PlanetaryDocs.Shared 10 | { 11 | /// 12 | /// Code for the component. 13 | /// 14 | /// 15 | /// This component enables editing of extremely large fields by bypassing the 16 | /// SignalR communication and using JavaScript interop to marshall values. 17 | /// 18 | public class MultiLineEditBase : ComponentBase, IDisposable 19 | { 20 | private string id; 21 | 22 | /// 23 | /// Gets or sets the . 24 | /// 25 | [Inject] 26 | public MultiLineEditService EditService { get; set; } 27 | 28 | /// 29 | /// Gets or sets the tab index. 30 | /// 31 | [Parameter] 32 | public string TabIndex { get; set; } 33 | 34 | /// 35 | /// Gets or sets the reference to the text area used for editing. 36 | /// 37 | public ElementReference TextArea { get; set; } 38 | 39 | /// 40 | /// Gets or sets the text to edit. 41 | /// 42 | [Parameter] 43 | public string Text { get; set; } 44 | 45 | /// 46 | /// Gets or sets the callback to notify on text changed. 47 | /// 48 | [Parameter] 49 | public EventCallback TextChanged { get; set; } 50 | 51 | /// 52 | /// Implement dispose to remove references. 53 | /// 54 | public void Dispose() 55 | { 56 | if (!string.IsNullOrWhiteSpace(id)) 57 | { 58 | EditService.Unregister(id); 59 | id = string.Empty; 60 | } 61 | 62 | GC.SuppressFinalize(this); 63 | } 64 | 65 | /// 66 | /// Called to update the text from JavaScript. 67 | /// 68 | /// The text to update. 69 | /// The asynchronous task. 70 | public async Task OnUpdateTextAsync(string text) 71 | { 72 | Text = text; 73 | await TextChanged.InvokeAsync(text); 74 | } 75 | 76 | /// 77 | /// After rendering, hook up the interop. 78 | /// 79 | /// A value indicating whether it is the first render. 80 | /// The asynchronous task. 81 | protected override async Task OnAfterRenderAsync(bool firstRender) 82 | { 83 | if (firstRender) 84 | { 85 | id = await EditService.RegisterTextAsync( 86 | Text, 87 | this); 88 | } 89 | 90 | await base.OnAfterRenderAsync(firstRender); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/DomainTests/DocumentTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PlanetaryDocs.Domain; 4 | using Xunit; 5 | 6 | namespace DomainTests 7 | { 8 | public class DocumentTests 9 | { 10 | [Fact] 11 | public void Tags_Are_Initialized() 12 | { 13 | // arrange and act 14 | var doc = new Document(); 15 | 16 | // assert 17 | Assert.NotNull(doc.Tags); 18 | } 19 | 20 | [Fact] 21 | public void HashCode_Is_Uid_HashCode() 22 | { 23 | // arrange 24 | const string uid = nameof(DocumentTests); 25 | var doc = new Document { Uid = uid }; 26 | var expected = uid.GetHashCode(); 27 | 28 | // act 29 | var actual = doc.GetHashCode(); 30 | 31 | // assert 32 | Assert.Equal(expected, actual); 33 | } 34 | 35 | public static IEnumerable DocEqualityTests() 36 | { 37 | var uid = Guid.NewGuid().ToString(); 38 | var altUid = Guid.NewGuid().ToString(); 39 | var doc = new Document { Uid = uid }; 40 | var diffDoc = new Document { Uid = altUid }; 41 | var anonDoc = new { Uid = uid }; 42 | 43 | yield return new object[] 44 | { 45 | uid, 46 | doc, 47 | true 48 | }; 49 | 50 | yield return new object[] 51 | { 52 | uid, 53 | diffDoc, 54 | false 55 | }; 56 | 57 | yield return new object[] 58 | { 59 | uid, 60 | anonDoc, 61 | false 62 | }; 63 | } 64 | 65 | [Theory] 66 | [MemberData(nameof(DocEqualityTests))] 67 | public void Equality_Is_Based_On_Type_And_Uid( 68 | string srcUid, 69 | object target, 70 | bool areEqual) 71 | { 72 | // arrange 73 | var refDoc = new Document { Uid = srcUid }; 74 | 75 | // act 76 | var equal = refDoc.Equals(target); 77 | 78 | // assert 79 | Assert.Equal(areEqual, equal); 80 | } 81 | 82 | [Theory] 83 | [InlineData("one", "author", new string[0])] 84 | [InlineData("two", "author", new[] { "one" })] 85 | [InlineData("three", "author", new[] { "one", "two" })] 86 | public void ToString_Contains_Uid_Alias_And_TagCount( 87 | string uid, 88 | string alias, 89 | string[] tags) 90 | { 91 | // arrange 92 | var doc = new Document 93 | { 94 | Uid = uid, 95 | AuthorAlias = alias, 96 | }; 97 | 98 | doc.Tags.AddRange(tags); 99 | 100 | // act 101 | var str = doc.ToString(); 102 | 103 | // assert 104 | Assert.Contains(uid, str); 105 | Assert.Contains(alias, str); 106 | Assert.Contains(tags.Length.ToString(), str); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /PlanetaryDocs/Services/MultiLineEditService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Microsoft.JSInterop; 8 | using PlanetaryDocs.Shared; 9 | 10 | namespace PlanetaryDocs.Services 11 | { 12 | /// 13 | /// Service to handle multi line edit controls. 14 | /// 15 | /// 16 | /// Uses JavaScript interop to bypass SignalR restrictions on large fields. 17 | /// The component registers and an id is tracked on the client. As the user 18 | /// edits, the JavaScript calls back to the exposed static method to marshall 19 | /// the data back. 20 | /// 21 | public class MultiLineEditService 22 | { 23 | /// 24 | /// Keeps track of services across all users. 25 | /// 26 | private static readonly Dictionary Services 27 | = new (); 28 | 29 | /// 30 | /// Components for a specific user. 31 | /// 32 | private readonly Dictionary components 33 | = new (); 34 | 35 | private readonly IJSRuntime jsRuntime = null; 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The JavaScript runtime. 41 | public MultiLineEditService(IJSRuntime jsRuntime) => 42 | this.jsRuntime = jsRuntime; 43 | 44 | /// 45 | /// Update the text. 46 | /// 47 | /// The unique id. 48 | /// The text to update. 49 | /// The asynchronous task. 50 | [JSInvokable] 51 | public static async Task UpdateTextAsync(string id, string text) 52 | { 53 | var service = Services[id]; 54 | var component = service.components[id]; 55 | await component.OnUpdateTextAsync(text); 56 | } 57 | 58 | /// 59 | /// Register the text to start the process. 60 | /// 61 | /// The text. 62 | /// The instance. 63 | /// A unique identifier. 64 | public async Task RegisterTextAsync( 65 | string text, 66 | MultiLineEditBase component) 67 | { 68 | var id = Guid.NewGuid().ToString(); 69 | await jsRuntime.InvokeVoidAsync( 70 | "markdownExtensions.setText", 71 | id, 72 | text, 73 | component.TextArea); 74 | components.Add(id, component); 75 | Services.Add(id, this); 76 | return id; 77 | } 78 | 79 | /// 80 | /// Unregister references. 81 | /// 82 | /// The id of the instance. 83 | public void Unregister(string id) 84 | { 85 | var service = Services[id]; 86 | Services.Remove(id); 87 | service.components.Remove(id); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/TagPickerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Components; 8 | 9 | namespace PlanetaryDocs.Shared 10 | { 11 | /// 12 | /// Code for the component. 13 | /// 14 | public class TagPickerBase : ComponentBase 15 | { 16 | private string newTag; 17 | 18 | /// 19 | /// Gets or sets the list of tags to choose from. 20 | /// 21 | [Parameter] 22 | public List Tags { get; set; } 23 | 24 | /// 25 | /// Gets or sets the callback to notify when tags change. 26 | /// 27 | [Parameter] 28 | public EventCallback> TagsChanged { get; set; } 29 | 30 | /// 31 | /// Gets or sets the tab index. 32 | /// 33 | [Parameter] 34 | public string TabIndex { get; set; } 35 | 36 | /// 37 | /// Gets or sets the new tag. 38 | /// 39 | public string NewTag 40 | { 41 | get => newTag; 42 | set 43 | { 44 | if (!string.IsNullOrWhiteSpace(value) && 45 | !Tags.Contains(value)) 46 | { 47 | Tags.Add(value); 48 | newTag = string.Empty; 49 | AddTag = string.Empty; 50 | PickNew = false; 51 | InvokeAsync(async () => await TagsChanged.InvokeAsync( 52 | Tags.ToList())); 53 | } 54 | } 55 | } 56 | 57 | /// 58 | /// Gets or sets a value indicating whether the user is picking 59 | /// a new tag. 60 | /// 61 | protected bool PickNew { get; set; } = false; 62 | 63 | /// 64 | /// Gets or sets the name of a new tag to add. 65 | /// 66 | protected string AddTag { get; set; } = string.Empty; 67 | 68 | /// 69 | /// Gets the base index for tabs. 70 | /// 71 | protected int BaseIndex => 72 | int.TryParse(TabIndex, out var idx) ? idx : 73 | 100; 74 | 75 | /// 76 | /// Gets the tab index after the tag list. 77 | /// 78 | protected int AltTabIndex => 79 | BaseIndex + Tags.Count; 80 | 81 | /// 82 | /// Handles removing a tag from the list. 83 | /// 84 | /// The tag to remove. 85 | /// An asynchronous task. 86 | public async Task RemoveAsync(string tag) 87 | { 88 | Tags.Remove(tag); 89 | await TagsChanged.InvokeAsync(Tags.ToList()); 90 | } 91 | 92 | /// 93 | /// Gets the tab index for a tag. 94 | /// 95 | /// The tag to index. 96 | /// The tab index. 97 | protected int IndexForTag(string tag) => 98 | BaseIndex + Tags.IndexOf(tag) + 1; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at expressionpowertools_at_gmail_dot_com. (Replace _at_ with @ and _dot_ with .). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/NavMenuBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using Microsoft.AspNetCore.Components; 5 | using Microsoft.AspNetCore.Components.Routing; 6 | 7 | namespace PlanetaryDocs.Shared 8 | { 9 | /// 10 | /// Code for navigation component. 11 | /// 12 | public class NavMenuBase : ComponentBase 13 | { 14 | /// 15 | /// Gets the list of navigation items. 16 | /// 17 | protected NavItem[] NavItems 18 | { get; } = new[] 19 | { 20 | new NavItem 21 | { 22 | Disabled = false, 23 | Text = "Home", 24 | Icon = "home", 25 | Href = string.Empty, 26 | Match = NavLinkMatch.All, 27 | }, 28 | new NavItem { Disabled = true, Text = "View", Icon = "eye", Href = "/View" }, 29 | new NavItem { Disabled = false, Text = "Add New", Icon = "plus", Href = "/Add" }, 30 | new NavItem { Disabled = true, Text = "Edit", Icon = "pencil", Href = "/Edit" }, 31 | }; 32 | 33 | /// 34 | /// Gets or sets a value indicating whether the menu should be collapsed. 35 | /// 36 | protected bool CollapseNavMenu { get; set; } = true; 37 | 38 | /// 39 | /// Gets or sets the version. 40 | /// 41 | protected string Version { get; set; } = "?"; 42 | 43 | /// 44 | /// Gets the CSS class to apply to the menu based on whether or not it is 45 | /// collapsed. 46 | /// 47 | protected string NavMenuCssClass => CollapseNavMenu ? "collapse" : null; 48 | 49 | /// 50 | /// Toggles the display of the menu. 51 | /// 52 | protected void ToggleNavMenu() => CollapseNavMenu = !CollapseNavMenu; 53 | 54 | /// 55 | /// Code to set version on initialization. 56 | /// 57 | protected override void OnInitialized() 58 | { 59 | if (Version == "?") 60 | { 61 | Version = System.Diagnostics.FileVersionInfo.GetVersionInfo( 62 | GetType().Assembly.Location).ProductVersion; 63 | } 64 | 65 | base.OnInitialized(); 66 | } 67 | 68 | /// 69 | /// Navigation item. 70 | /// 71 | protected class NavItem 72 | { 73 | /// 74 | /// Gets or sets a value indicating whether using this item as navigation 75 | /// is disabled. 76 | /// 77 | public bool Disabled { get; set; } 78 | 79 | /// 80 | /// Gets or sets the text to display for the navigation item. 81 | /// 82 | public string Text { get; set; } 83 | 84 | /// 85 | /// Gets or sets the icon to show. 86 | /// 87 | public string Icon { get; set; } 88 | 89 | /// 90 | /// Gets or sets the path to the target. 91 | /// 92 | public string Href { get; set; } 93 | 94 | /// 95 | /// Gets or sets a value indicating whether the highlight logic should 96 | /// match based on the prefix of the current URL or the full URL. 97 | /// 98 | public NavLinkMatch Match { get; set; } = NavLinkMatch.Prefix; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /PlanetaryDocs.Domain/IDocumentService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace PlanetaryDocs.Domain 9 | { 10 | /// 11 | /// Data service definition. 12 | /// 13 | public interface IDocumentService 14 | { 15 | /// 16 | /// Add a new document. 17 | /// 18 | /// The to add. 19 | /// An asynchronous . 20 | Task InsertDocumentAsync(Document document); 21 | 22 | /// 23 | /// Updates an existing document. 24 | /// 25 | /// The to update. 26 | /// An asynchronous . 27 | Task UpdateDocumentAsync(Document document); 28 | 29 | /// 30 | /// Searches tags. 31 | /// 32 | /// Text to search on. 33 | /// The list of matching tags. 34 | Task> SearchTagsAsync(string searchText); 35 | 36 | /// 37 | /// Searches author aliases. 38 | /// 39 | /// Text to search on. 40 | /// The list of matching aliases. 41 | Task> SearchAuthorsAsync(string searchText); 42 | 43 | /// 44 | /// Query to obtain a list. 45 | /// 46 | /// Text to look for. 47 | /// Restrict to an author. 48 | /// Restrict to a tag. 49 | /// The matching list. 50 | Task> QueryDocumentsAsync( 51 | string searchText, 52 | string authorAlias, 53 | string tag); 54 | 55 | /// 56 | /// Loads a single . 57 | /// 58 | /// The unique identifier. 59 | /// The . 60 | Task LoadDocumentAsync(string uid); 61 | 62 | /// 63 | /// Retrieves the audit history of the . 64 | /// 65 | /// The unique identifier of the . 66 | /// The list of audit entries. 67 | Task> LoadDocumentHistoryAsync(string uid); 68 | 69 | /// 70 | /// Loads a specific snapshot. 71 | /// 72 | /// The unique identifier of the snapshot. 73 | /// The unique identifier of the document. 74 | /// The document snapshot. 75 | Task LoadDocumentSnapshotAsync(Guid guid, string uid); 76 | 77 | /// 78 | /// Deletes a document. 79 | /// 80 | /// The unique identifier. 81 | /// The asynchronous task. 82 | Task DeleteDocumentAsync(string uid); 83 | 84 | /// 85 | /// Restores a version of the deleted document. 86 | /// 87 | /// The id of the audit. 88 | /// The unique identifiers of the document. 89 | /// The restored document. 90 | Task RestoreDocumentAsync(Guid id, string uid); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/DomainTests/DocumentAuditTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using PlanetaryDocs.Domain; 5 | using Xunit; 6 | 7 | namespace DomainTests 8 | { 9 | public class DocumentAuditTests 10 | { 11 | private const string Author = "jeliknes"; 12 | private const string Markdown = "# Hi"; 13 | private const string Html = "

Hi

"; 14 | 15 | private readonly DateTime PublishDate = 16 | DateTime.UtcNow.AddDays(-2); 17 | 18 | private Document NewDoc() 19 | { 20 | var doc = new Document 21 | { 22 | AuthorAlias = Author, 23 | Description = $"Document by {Author}", 24 | Html = Html, 25 | Markdown = Markdown, 26 | PublishDate = PublishDate, 27 | Title = "Title of document" 28 | }; 29 | 30 | doc.Tags.Add("one"); 31 | doc.Tags.Add("two"); 32 | 33 | return doc; 34 | } 35 | 36 | [Fact] 37 | public void New_Instance_With_Doc_Generates_New_Guid() 38 | { 39 | // arrange 40 | var doc = NewDoc(); 41 | 42 | // act 43 | var docAudit = new DocumentAudit(doc); 44 | 45 | // assert 46 | Assert.NotEqual(default, docAudit.Id); 47 | } 48 | 49 | [Fact] 50 | public void Uid_Initializes_To_Uid_Of_Document() 51 | { 52 | // arrange 53 | var doc = NewDoc(); 54 | 55 | // act 56 | var docAudit = new DocumentAudit(doc); 57 | 58 | // assert 59 | Assert.Equal(doc.Uid, docAudit.Uid); 60 | } 61 | 62 | [Fact] 63 | public void Timestamp_Defaults_To_UtcNow() 64 | { 65 | // arrange 66 | var doc = NewDoc(); 67 | 68 | // act 69 | var docAudit = new DocumentAudit(doc); 70 | var now = DateTimeOffset.UtcNow; 71 | var diff = now - docAudit.Timestamp; 72 | var tolerance = TimeSpan.FromMilliseconds(50); 73 | 74 | // assert 75 | Assert.True(diff < tolerance); 76 | } 77 | 78 | [Fact] 79 | public void Document_Is_Json_Serialized_Snapshot() 80 | { 81 | // arrange 82 | var doc = NewDoc(); 83 | 84 | // act 85 | var docAudit = new DocumentAudit(doc); 86 | var serialized = JsonSerializer.Serialize(doc); 87 | 88 | // assert 89 | Assert.Equal(serialized, docAudit.Document); 90 | } 91 | 92 | public static IEnumerable DocumentSnapshotTests() 93 | { 94 | var resolvers = new Func[] 95 | { 96 | doc => doc.AuthorAlias, 97 | doc => doc.Description, 98 | doc => doc.Html, 99 | doc => doc.Markdown, 100 | doc => doc.PublishDate.Ticks.ToString(), 101 | doc => string.Join(", ", doc.Tags), 102 | doc => doc.Title, 103 | doc => doc.Uid 104 | }; 105 | 106 | foreach (var resolver in resolvers) 107 | { 108 | yield return new object[] { resolver }; 109 | } 110 | } 111 | 112 | [Theory] 113 | [MemberData(nameof(DocumentSnapshotTests))] 114 | public void GetDocumentSnapshot_Deserializes_Document( 115 | Func resolver) 116 | { 117 | // arrange 118 | var doc = NewDoc(); 119 | 120 | // act 121 | var docAudit = new DocumentAudit(doc); 122 | var compDoc = docAudit.GetDocumentSnapshot(); 123 | 124 | // assert 125 | Assert.Equal(resolver(doc), resolver(compDoc)); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /PlanetaryDocs.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30803.129 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanetaryDocs", "PlanetaryDocs\PlanetaryDocs.csproj", "{9752E8CA-BCA2-4383-A2C3-9A619C95C2C7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanetaryDocs.Domain", "PlanetaryDocs.Domain\PlanetaryDocs.Domain.csproj", "{41EE61E6-81B4-4816-9E62-25040A4BAC6D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanetaryDocs.DataAccess", "PlanetaryDocs.DataAccess\PlanetaryDocs.DataAccess.csproj", "{8E4FB7E5-BC05-4A5F-9A5C-377FDB0F4172}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanetaryDocsLoader", "PlanetaryDocsLoader\PlanetaryDocsLoader.csproj", "{43DFBA3B-80BE-4DCB-82D2-2EAC03CD5EF7}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B956AEFD-4189-4B9B-86B6-2B23A9B7DE1C}" 15 | ProjectSection(SolutionItems) = preProject 16 | .editorconfig = .editorconfig 17 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 18 | CONTRIBUTING.md = CONTRIBUTING.md 19 | Directory.Build.props = Directory.Build.props 20 | LICENSE = LICENSE 21 | README.md = README.md 22 | stylecop.json = stylecop.json 23 | version.json = version.json 24 | EndProjectSection 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BE2E007C-9286-4061-AF85-D0E641C9AF79}" 27 | ProjectSection(SolutionItems) = preProject 28 | Tests\Directory.Build.props = Tests\Directory.Build.props 29 | EndProjectSection 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DomainTests", "Tests\DomainTests\DomainTests.csproj", "{2D7599D9-6851-4BA3-9B83-1BE219E286B9}" 32 | EndProject 33 | Global 34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 35 | Debug|Any CPU = Debug|Any CPU 36 | Release|Any CPU = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 39 | {9752E8CA-BCA2-4383-A2C3-9A619C95C2C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {9752E8CA-BCA2-4383-A2C3-9A619C95C2C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {9752E8CA-BCA2-4383-A2C3-9A619C95C2C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {9752E8CA-BCA2-4383-A2C3-9A619C95C2C7}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {41EE61E6-81B4-4816-9E62-25040A4BAC6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {41EE61E6-81B4-4816-9E62-25040A4BAC6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {41EE61E6-81B4-4816-9E62-25040A4BAC6D}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {41EE61E6-81B4-4816-9E62-25040A4BAC6D}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {8E4FB7E5-BC05-4A5F-9A5C-377FDB0F4172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {8E4FB7E5-BC05-4A5F-9A5C-377FDB0F4172}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {8E4FB7E5-BC05-4A5F-9A5C-377FDB0F4172}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {8E4FB7E5-BC05-4A5F-9A5C-377FDB0F4172}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {43DFBA3B-80BE-4DCB-82D2-2EAC03CD5EF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {43DFBA3B-80BE-4DCB-82D2-2EAC03CD5EF7}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {43DFBA3B-80BE-4DCB-82D2-2EAC03CD5EF7}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {43DFBA3B-80BE-4DCB-82D2-2EAC03CD5EF7}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {2D7599D9-6851-4BA3-9B83-1BE219E286B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {2D7599D9-6851-4BA3-9B83-1BE219E286B9}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {2D7599D9-6851-4BA3-9B83-1BE219E286B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {2D7599D9-6851-4BA3-9B83-1BE219E286B9}.Release|Any CPU.Build.0 = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | GlobalSection(NestedProjects) = preSolution 64 | {2D7599D9-6851-4BA3-9B83-1BE219E286B9} = {BE2E007C-9286-4061-AF85-D0E641C9AF79} 65 | EndGlobalSection 66 | GlobalSection(ExtensibilityGlobals) = postSolution 67 | SolutionGuid = {2064456E-0D82-443B-ACBD-92A598775C6B} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /PlanetaryDocs/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Options; 12 | using PlanetaryDocs.DataAccess; 13 | using PlanetaryDocs.Domain; 14 | using PlanetaryDocs.Services; 15 | 16 | namespace PlanetaryDocs 17 | { 18 | /// 19 | /// Blazor startup. 20 | /// 21 | public class Startup 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The configuration information. 27 | public Startup(IConfiguration configuration) => Configuration = configuration; 28 | 29 | /// 30 | /// Gets t configuration instance. 31 | /// 32 | public IConfiguration Configuration { get; } 33 | 34 | /// 35 | /// Select the services used by your app. 36 | /// 37 | /// The service collection. 38 | public void ConfigureServices(IServiceCollection services) 39 | { 40 | services.AddRazorPages(); 41 | services.AddServerSideBlazor(); 42 | services.Configure( 43 | Configuration.GetSection(nameof(CosmosSettings))); 44 | services.AddDbContextFactory( 45 | (IServiceProvider sp, DbContextOptionsBuilder opts) => 46 | { 47 | var cosmosSettings = sp 48 | .GetRequiredService>() 49 | .Value; 50 | 51 | opts.UseCosmos( 52 | cosmosSettings.EndPoint, 53 | cosmosSettings.AccessKey, 54 | nameof(DocsContext)); 55 | }); 56 | 57 | services.AddScoped(); 58 | services.AddScoped(); 59 | services.AddScoped(); 60 | services.AddScoped(); 61 | services.AddScoped(); 62 | } 63 | 64 | /// 65 | /// Configure the selected services. 66 | /// 67 | /// The app builder. 68 | /// The current environment. 69 | /// Context factory. 70 | /// The Cosmos settings. 71 | public void Configure( 72 | IApplicationBuilder app, 73 | IWebHostEnvironment env, 74 | IDbContextFactory factory, 75 | IOptions cs) 76 | { 77 | if (env.IsDevelopment()) 78 | { 79 | app.UseDeveloperExceptionPage(); 80 | } 81 | else 82 | { 83 | app.UseExceptionHandler("/Error"); 84 | 85 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 86 | app.UseHsts(); 87 | } 88 | 89 | if (cs.Value.EnableMigration) 90 | { 91 | using var context = factory.CreateDbContext(); 92 | context.CheckAndMigrateTagsAsync(cs.Value.DocumentToCheck). 93 | ConfigureAwait(true) 94 | .GetAwaiter() 95 | .GetResult(); 96 | } 97 | 98 | app.UseHttpsRedirection(); 99 | app.UseStaticFiles(); 100 | 101 | app.UseRouting(); 102 | 103 | app.UseEndpoints(endpoints => 104 | { 105 | endpoints.MapBlazorHub(); 106 | endpoints.MapFallbackToPage("/_Host"); 107 | }); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /PlanetaryDocs/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /PlanetaryDocs/Services/LoadingService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace PlanetaryDocs.Services 9 | { 10 | /// 11 | /// App-wide loading service to keep track of asynchronous requests. 12 | /// 13 | public class LoadingService 14 | { 15 | private readonly Action noop = () => { }; 16 | 17 | private int asyncCount; 18 | 19 | private Action stateChangedCallback = null; 20 | 21 | /// 22 | /// Gets or sets the callback to initiate a state change notification. 23 | /// 24 | public Action StateChangedCallback 25 | { 26 | get => stateChangedCallback ?? noop; 27 | set 28 | { 29 | if (stateChangedCallback != null) 30 | { 31 | throw new InvalidOperationException("Only one callback can be registered at the root level."); 32 | } 33 | 34 | stateChangedCallback = value; 35 | } 36 | } 37 | 38 | /// 39 | /// Gets a value indicating whether a loading operation is happening. 40 | /// 41 | public bool Loading => asyncCount > 0; 42 | 43 | /// 44 | /// Gets a value indicating whether the loading state changed based on 45 | /// the most recent transition. 46 | /// 47 | public bool StateChanged { get; private set; } = false; 48 | 49 | /// 50 | /// Start of an asynchronous operation. 51 | /// 52 | public void AsyncBegin() 53 | { 54 | StateChanged = false; 55 | Interlocked.Increment(ref asyncCount); 56 | if (asyncCount == 1) 57 | { 58 | StateChanged = true; 59 | StateChangedCallback(); 60 | } 61 | } 62 | 63 | /// 64 | /// End of an asynchronous operation. 65 | /// 66 | public void AsyncEnd() 67 | { 68 | StateChanged = false; 69 | Interlocked.Decrement(ref asyncCount); 70 | 71 | if (asyncCount == 0) 72 | { 73 | StateChanged = true; 74 | StateChangedCallback(); 75 | } 76 | } 77 | 78 | /// 79 | /// Single API to increment loading status, run the asynchronous operation, 80 | /// then decrement loading status. 81 | /// 82 | /// The code to execute. 83 | /// The code to execute if the loading status changes. 84 | /// The linear way to use the service looks like this: 85 | /// 90 | /// The non-linear (async op call) looks like: 91 | /// { 95 | /// result = await ThingsToDoAsync(); 96 | /// } 97 | /// }); 98 | /// ]]> 99 | /// 100 | /// An asychronous task. 101 | public async Task WrapExecutionAsync( 102 | Func execution, 103 | Func stateChanged = null) 104 | { 105 | AsyncBegin(); 106 | if (StateChanged && stateChanged != null) 107 | { 108 | await stateChanged(); 109 | } 110 | 111 | try 112 | { 113 | await execution(); 114 | } 115 | catch 116 | { 117 | throw; 118 | } 119 | finally 120 | { 121 | AsyncEnd(); 122 | if (StateChanged && stateChanged != null) 123 | { 124 | await stateChanged(); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/DomainTests/DocumentSummaryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PlanetaryDocs.Domain; 4 | using Xunit; 5 | 6 | namespace DomainTests 7 | { 8 | public class DocumentSummaryTests 9 | { 10 | public static IEnumerable DocSummaryTests() 11 | { 12 | var resolvers = new Func[] 13 | { 14 | doc => doc.Uid, 15 | doc => doc.Title, 16 | doc => doc.AuthorAlias, 17 | }; 18 | 19 | var summaryResolvers = new Func[] 20 | { 21 | summary => summary.Uid, 22 | summary => summary.Title, 23 | summary => summary.AuthorAlias, 24 | }; 25 | 26 | for (var idx = 0; idx < resolvers.Length; idx++) 27 | { 28 | yield return new object[] 29 | { 30 | resolvers[idx], 31 | summaryResolvers[idx] 32 | }; 33 | } 34 | } 35 | 36 | [Theory] 37 | [MemberData(nameof(DocSummaryTests))] 38 | public void Properties_Match_Doc( 39 | Func resolver, 40 | Func summaryResolver) 41 | { 42 | // arrange 43 | var doc = new Document 44 | { 45 | Uid = nameof(DocSummaryTests), 46 | AuthorAlias = "system", 47 | Title = "Document title" 48 | }; 49 | 50 | // act 51 | var summary = new DocumentSummary(doc); 52 | var expected = resolver(doc); 53 | var actual = summaryResolver(summary); 54 | 55 | // assert 56 | Assert.Equal(expected, actual); 57 | } 58 | 59 | [Fact] 60 | public void HashCode_Is_HashCode_Of_Uid() 61 | { 62 | // arrange 63 | var summary = new DocumentSummary 64 | { 65 | Uid = nameof(DocSummaryTests) 66 | }; 67 | 68 | // act 69 | var expected = summary.Uid.GetHashCode(); 70 | var actual = summary.GetHashCode(); 71 | 72 | // assert 73 | Assert.Equal(expected, actual); 74 | } 75 | 76 | public static IEnumerable EqualityTests() 77 | { 78 | var uid = Guid.NewGuid().ToString(); 79 | var sameUid = new DocumentSummary 80 | { 81 | Uid = uid 82 | }; 83 | 84 | var differentUid = new DocumentSummary 85 | { 86 | Uid = Guid.NewGuid().ToString() 87 | }; 88 | 89 | var anonymous = new 90 | { 91 | Uid = uid 92 | }; 93 | 94 | yield return new object[] 95 | { 96 | uid, 97 | sameUid, 98 | true 99 | }; 100 | 101 | yield return new object[] 102 | { 103 | uid, 104 | differentUid, 105 | false 106 | }; 107 | 108 | yield return new object[] 109 | { 110 | uid, 111 | anonymous, 112 | false 113 | }; 114 | } 115 | 116 | [Theory] 117 | [MemberData(nameof(EqualityTests))] 118 | public void Equality_Based_On_Type_And_Id( 119 | string uid, 120 | object target, 121 | bool areEqual) 122 | { 123 | // arrange 124 | var summary = new DocumentSummary 125 | { 126 | Uid = uid 127 | }; 128 | 129 | // act 130 | var equals = summary.Equals(target); 131 | 132 | // assert 133 | Assert.Equal(areEqual, equals); 134 | } 135 | 136 | [Fact] 137 | public void ToString_Includes_Uid_Alias_And_Title() 138 | { 139 | // arrange 140 | var summary = new DocumentSummary 141 | { 142 | Uid = nameof(EqualityTests), 143 | Title = "Something great", 144 | AuthorAlias = "test" 145 | }; 146 | 147 | // act 148 | var str = summary.ToString(); 149 | 150 | // assert 151 | Assert.Contains(summary.Uid, str); 152 | Assert.Contains(summary.Title, str); 153 | Assert.Contains(summary.AuthorAlias, str); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/AddBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Components; 6 | using PlanetaryDocs.Domain; 7 | using PlanetaryDocs.Services; 8 | using PlanetaryDocs.Shared; 9 | 10 | namespace PlanetaryDocs.Pages 11 | { 12 | /// 13 | /// Component to add a new document. 14 | /// 15 | public class AddBase : ComponentBase 16 | { 17 | private bool isValid = false; 18 | 19 | /// 20 | /// Gets or sets the navigation service. 21 | /// 22 | [Inject] 23 | public NavigationManager NavigationService { get; set; } 24 | 25 | /// 26 | /// Gets or sets the document service. 27 | /// 28 | [Inject] 29 | public IDocumentService DocumentService { get; set; } 30 | 31 | /// 32 | /// Gets or sets the loading service. 33 | /// 34 | [CascadingParameter] 35 | public LoadingService LoadingService { get; set; } 36 | 37 | /// 38 | /// Gets or sets the title service. 39 | /// 40 | [Inject] 41 | public TitleService TitleService { get; set; } 42 | 43 | /// 44 | /// Gets or sets a value indicating whether the 45 | /// is in a valid state for add. 46 | /// 47 | public bool IsValid 48 | { 49 | get => isValid; 50 | set 51 | { 52 | if (value != isValid) 53 | { 54 | isValid = value; 55 | InvokeAsync(StateHasChanged); 56 | } 57 | } 58 | } 59 | 60 | /// 61 | /// Gets a value indicating whether changes have been made. 62 | /// 63 | protected bool IsDirty => ChangeCount > 0; 64 | 65 | /// 66 | /// Gets or sets a value indicating whether a loading operation is taking place. 67 | /// 68 | protected bool Loading { get; set; } = true; 69 | 70 | /// 71 | /// Gets or sets a value indicating whether a save operation is in effect. 72 | /// 73 | protected bool Saving { get; set; } = false; 74 | 75 | /// 76 | /// Gets or sets the being added. 77 | /// 78 | protected Document Document { get; set; } = new (); 79 | 80 | /// 81 | /// Gets or sets the count of changes detected. 82 | /// 83 | protected int ChangeCount { get; set; } = 0; 84 | 85 | /// 86 | /// Gets or sets the reference. 87 | /// 88 | protected Editor Editor { get; set; } 89 | 90 | /// 91 | /// Main save method. 92 | /// 93 | /// An asynchronous task. 94 | public async Task SaveAsync() 95 | { 96 | if (!IsDirty || !IsValid || !Editor.ValidateAll(Document)) 97 | { 98 | return; 99 | } 100 | 101 | Saving = true; 102 | 103 | await LoadingService.WrapExecutionAsync(async () => 104 | await DocumentService.InsertDocumentAsync(Document)); 105 | 106 | NavigationService.NavigateTo(NavigationHelper.ViewDocument(Document.Uid), true); 107 | } 108 | 109 | /// 110 | /// Set the title. 111 | /// 112 | /// A flag indicating the render status. 113 | /// The asynchronous task. 114 | protected override async Task OnAfterRenderAsync(bool firstRender) 115 | { 116 | if (firstRender) 117 | { 118 | await TitleService.SetTitleAsync("Adding new document"); 119 | Editor.ValidateAll(Document); 120 | } 121 | 122 | await base.OnAfterRenderAsync(firstRender); 123 | } 124 | 125 | /// 126 | /// Initialize the component. 127 | /// 128 | protected override void OnInitialized() 129 | { 130 | Document = new Document(); 131 | ChangeCount = 0; 132 | Loading = false; 133 | base.OnInitialized(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/View.razor: -------------------------------------------------------------------------------- 1 | @page "/View/{Uid}" 2 | 3 | @inherits ViewBase 4 | 5 |
6 |
7 |
8 | @if (LoadingService.Loading) 9 | { 10 |  Loading... 11 | } 12 |
13 |
14 | @if (NotFound) 15 | { 16 |
17 |
18 |
19 |  Document with id '@Uid' was not found. 20 | Click here to return to search. 21 |
22 |
23 |
24 | } 25 | @if (!Loading && Document != null) 26 | { 27 |
28 | @if (Audit) 29 | { 30 |
31 |

@Title

32 |
33 | } 34 | else 35 | { 36 |
37 |  Edit 39 |
40 |
41 |

@Title

42 |
43 | } 44 |
45 | @if (Audit) 46 | { 47 |
48 |
49 |
50 | You are viewing an archived version of this document. 51 | 53 | to return to the current version. 54 |
55 |
56 |
57 | } 58 |
59 |
60 | Author: @Document.AuthorAlias 61 |
62 |
63 | @if (DateTimeOffset.UtcNow.Ticks > 64 | Document.PublishDate.Ticks) 65 | { 66 | Published: @Document.PublishDate 67 | } 68 | else 69 | { 70 | Scheduled: @Document.PublishDate 71 | } 72 |
73 |
74 | Tags: @foreach (var tag in Document.Tags) 75 | { 76 | '@tag'  77 | } 78 |
79 |
80 |
81 |
82 | @Document.Description 83 |
84 |
85 |
86 |
87 | @if (!ShowHistory && !Audit) 88 | { 89 |   93 | } 94 | 98 | @if (!ShowMarkdown) 99 | { 100 |   101 | 105 | } 106 |
107 |
108 |
109 | @if (ShowHistory) 110 | { 111 | 112 | } 113 | else if (ShowMarkdown) 114 | { 115 |
116 | @Document.Markdown 117 |
118 | } 119 | else if (PreviewHtml) 120 | { 121 |
122 | 123 |
124 | } 125 | else 126 | { 127 |
128 | @Document.Html 129 |
130 | } 131 |
132 | } 133 |
-------------------------------------------------------------------------------- /PlanetaryDocs/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | @inherits IndexBase 4 | 5 |

Planetary Docs

6 |
7 |
8 |
9 | Search Documents: 10 |
11 |
12 |
14 |
15 | @if (Loading) 16 | { 17 |

@Text

18 | } 19 | else 20 | { 21 | 27 | } 28 |
29 |
30 | @if (@Loading) 31 | { 32 |

Alias: @Alias

33 | } 34 | else 35 | { 36 | 37 | } 38 |
39 |
40 | @if (Loading) 41 | { 42 |

Tag: @Tag

43 | } 44 | else 45 | { 46 | 47 | } 48 |
49 |
50 |   56 |
57 |
58 |
59 |
60 | @if (Loading) 61 | { 62 |
63 |
64 |
🔎 Searching...
65 |
66 |
67 | } 68 | @if (DocsList != null && !Loading) 69 | { 70 |
71 |
72 | Search criteria: Alias = '@Alias', Tag = '@Tag', SearchText = '@Text'' 73 |
74 |
75 |
76 | @if (DocsList.Count < 1) 77 | { 78 | 79 |  No documents found. 80 | } 81 | @if (DocsList.Count == 1) 82 | { 83 | 84 |  One document found. 85 | } 86 | @if (DocsList.Count > 1) 87 | { 88 | 89 |  @DocsList.Count documents found. 90 | } 91 |
92 |
93 |
94 |
95 |
96 |   97 |
98 |
99 | Alias 100 |
101 |
102 | Title 103 |
104 |
105 | foreach (var doc in DocsList) 106 | { 107 |
108 |
109 | 110 |   111 | 112 |   113 | 114 |   115 | 116 |
117 |
118 | @doc.AuthorAlias 119 |
120 |
121 | @doc.Title 122 |
123 |
124 | } 125 | } 126 |
127 |
128 |
-------------------------------------------------------------------------------- /Tests/DomainTests/DocumentAuditSummaryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PlanetaryDocs.Domain; 4 | using Xunit; 5 | 6 | namespace DomainTests 7 | { 8 | public class DocumentAuditSummaryTests 9 | { 10 | public static IEnumerable DocAuditSummaryTests() 11 | { 12 | var resolvers = new (Func resolver, bool fromAudit)[] 13 | { 14 | (obj => ((DocumentAudit)obj).Id.ToString(), true), 15 | (obj => ((DocumentAudit)obj).Uid, true), 16 | (obj => ((DocumentAudit)obj).Timestamp.ToString(), true), 17 | (obj => ((Document)obj).AuthorAlias, false), 18 | (obj => ((Document)obj).Title, false), 19 | }; 20 | 21 | var auditResolvers = new Func[] 22 | { 23 | auditSummary => auditSummary.Id.ToString(), 24 | auditSummary => auditSummary.Uid, 25 | auditSummary => auditSummary.Timestamp.ToString(), 26 | auditSummary => auditSummary.Alias, 27 | auditSummary => auditSummary.Title, 28 | }; 29 | 30 | for (var idx = 0; idx < resolvers.Length; idx++) 31 | { 32 | yield return new object[] 33 | { 34 | resolvers[idx].resolver, 35 | resolvers[idx].fromAudit, 36 | auditResolvers[idx] 37 | }; 38 | } 39 | } 40 | 41 | [Theory] 42 | [MemberData(nameof(DocAuditSummaryTests))] 43 | public void Properties_Match_Audit_Or_Doc( 44 | Func resolver, 45 | bool fromAudit, 46 | Func auditResolver) 47 | { 48 | // arrange 49 | var doc = new Document 50 | { 51 | Uid = nameof(DocAuditSummaryTests), 52 | AuthorAlias = "system", 53 | Title = "Document title" 54 | }; 55 | 56 | var audit = new DocumentAudit(doc); 57 | 58 | // act 59 | var auditSummary = new DocumentAuditSummary(audit); 60 | var expected = fromAudit ? resolver(audit) : resolver(doc); 61 | var actual = auditResolver(auditSummary); 62 | 63 | // assert 64 | Assert.Equal(expected, actual); 65 | } 66 | 67 | [Fact] 68 | public void HashCode_Is_HashCode_Of_Id() 69 | { 70 | // arrange 71 | var summary = new DocumentAuditSummary 72 | { 73 | Id = Guid.NewGuid() 74 | }; 75 | 76 | // act 77 | var expected = summary.Id.GetHashCode(); 78 | var actual = summary.GetHashCode(); 79 | 80 | // assert 81 | Assert.Equal(expected, actual); 82 | } 83 | 84 | public static IEnumerable EqualityTests() 85 | { 86 | var guid = Guid.NewGuid(); 87 | var sameGuidSameUid = new DocumentAuditSummary 88 | { 89 | Id = guid, 90 | Uid = guid.ToString() 91 | }; 92 | 93 | var sameGuidDifferentUid = new DocumentAuditSummary 94 | { 95 | Id = guid, 96 | Uid = nameof(EqualityTests) 97 | }; 98 | 99 | var differentGuid = new DocumentAuditSummary 100 | { 101 | Id = Guid.NewGuid() 102 | }; 103 | 104 | var anonymous = new 105 | { 106 | Id = guid 107 | }; 108 | 109 | yield return new object[] 110 | { 111 | guid, 112 | sameGuidSameUid, 113 | true 114 | }; 115 | 116 | yield return new object[] 117 | { 118 | guid, 119 | sameGuidDifferentUid, 120 | true 121 | }; 122 | 123 | yield return new object[] 124 | { 125 | guid, 126 | differentGuid, 127 | false 128 | }; 129 | 130 | yield return new object[] 131 | { 132 | guid, 133 | anonymous, 134 | false 135 | }; 136 | } 137 | 138 | [Theory] 139 | [MemberData(nameof(EqualityTests))] 140 | public void Equality_Based_On_Type_And_Id( 141 | Guid id, 142 | object target, 143 | bool areEqual) 144 | { 145 | // arrange 146 | var summary = new DocumentAuditSummary 147 | { 148 | Id = id 149 | }; 150 | 151 | // act 152 | var equals = summary.Equals(target); 153 | 154 | // assert 155 | Assert.Equal(areEqual, equals); 156 | } 157 | 158 | [Fact] 159 | public void ToString_Includes_Id_And_Uid() 160 | { 161 | // arrange 162 | var summary = new DocumentAuditSummary 163 | { 164 | Id = Guid.NewGuid(), 165 | Uid = nameof(EqualityTests) 166 | }; 167 | 168 | // act 169 | var str = summary.ToString(); 170 | 171 | // assert 172 | Assert.Contains(summary.Id.ToString(), str); 173 | Assert.Contains(summary.Uid, str); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/Editor.razor: -------------------------------------------------------------------------------- 1 | @inherits EditorBase 2 | 3 | @if (DocumentToEdit != null) 4 | { 5 |
6 |
7 | @if (Insert) 8 | { 9 |
10 |  Adding a new document. 11 |
12 | } 13 | else 14 | { 15 |
16 |  Editing the document with id '@DocumentToEdit.Uid'. 17 |
18 | } 19 |
20 | @if (ValidationStates.Any(vr => !vr.IsValid)) 21 | { 22 |
23 |
24 |
    25 | @foreach (var vr in ValidationStates.Where 26 | (v => !v.IsValid)) 27 | { 28 |
  •  @vr.Message
  • 29 | } 30 |
31 |
32 |
33 | } 34 | @if (Insert) 35 | { 36 |
37 |
38 | Unique ID: 39 |
40 |
41 |
42 |
43 | 49 |
50 |
51 | } 52 |
53 |
54 | Title: 55 |
56 |
57 |
58 |
59 | 65 |
66 |
67 |
68 |
69 | @if (ExistingAlias) 70 | { 71 | 74 | } 75 | else 76 | { 77 | Enter new alias:  78 | 84 | } 85 | 89 | @AliasButton 90 |   91 |
92 |
93 |
94 |
95 | Choose tags:  96 | 97 |
98 |
99 |
100 |
101 | Description: 102 |
103 |
104 |
105 |
106 | 111 |
112 |
113 |
114 |
115 | Markdown: 116 |
117 |
118 | Preview:  119 | 123 |
124 |
125 |
126 |
127 | 128 |
129 |
130 | 131 |
132 |
133 |
134 | } 135 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/ViewBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Components; 8 | using PlanetaryDocs.Domain; 9 | using PlanetaryDocs.Services; 10 | 11 | namespace PlanetaryDocs.Pages 12 | { 13 | /// 14 | /// Code for the view component. 15 | /// 16 | public class ViewBase : ComponentBase 17 | { 18 | private string uid = string.Empty; 19 | 20 | /// 21 | /// Gets or sets the implementation of . 22 | /// 23 | [Inject] 24 | public IDocumentService DocumentService { get; set; } 25 | 26 | /// 27 | /// Gets or sets the . 28 | /// 29 | [CascadingParameter] 30 | public LoadingService LoadingService { get; set; } 31 | 32 | /// 33 | /// Gets or sets the . 34 | /// 35 | [Inject] 36 | public NavigationManager NavigationService { get; set; } 37 | 38 | /// 39 | /// Gets or sets the . 40 | /// 41 | [Inject] 42 | public TitleService TitleService { get; set; } 43 | 44 | /// 45 | /// Gets or sets the unique identifier for the documnt. 46 | /// 47 | [Parameter] 48 | public string Uid { get; set; } 49 | 50 | /// 51 | /// Gets or sets a value indicating whether to show the document history. 52 | /// 53 | protected bool ShowHistory { get; set; } = false; 54 | 55 | /// 56 | /// Gets or sets a value indicating whether to show the "Preview HTML" option. 57 | /// 58 | protected bool PreviewHtml { get; set; } = false; 59 | 60 | /// 61 | /// Gets or sets a value indicating whether to show the option to switch to Markdown. 62 | /// 63 | protected bool ShowMarkdown { get; set; } = true; 64 | 65 | /// 66 | /// Gets or sets a value indicating whether a loading operation is happening. 67 | /// 68 | protected bool Loading { get; set; } = true; 69 | 70 | /// 71 | /// Gets or sets a value indicating whether the document exists. 72 | /// 73 | protected bool NotFound { get; set; } = false; 74 | 75 | /// 76 | /// Gets or sets a value indicating whether to show recent audit history. 77 | /// 78 | protected bool Audit { get; set; } = false; 79 | 80 | /// 81 | /// Gets or sets the to view. 82 | /// 83 | protected Document Document { get; set; } = null; 84 | 85 | /// 86 | /// Gets the text to toggle between markdown and preiew or HTML. 87 | /// 88 | protected string ToggleText => ShowMarkdown ? 89 | "Show HTML" : "Show Markdown"; 90 | 91 | /// 92 | /// Gets the text to show preview text or rendered HTML. 93 | /// 94 | protected string PreviewText => PreviewHtml ? 95 | "Show Source" : "Show Preview"; 96 | 97 | /// 98 | /// Gets the title for the current item. 99 | /// 100 | protected string Title => Audit ? $"[ARCHIVE] {Document?.Title}" 101 | : Document?.Title; 102 | 103 | /// 104 | /// Called when the identifier is set. 105 | /// 106 | /// An asynchronous task. 107 | protected override async Task OnParametersSetAsync() 108 | { 109 | var newUid = WebUtility.UrlDecode(Uid); 110 | if (newUid != uid) 111 | { 112 | var history = string.Empty; 113 | var query = NavigationHelper.GetQueryString( 114 | NavigationService.Uri); 115 | if (query.ContainsKey(nameof(history))) 116 | { 117 | history = query[nameof(history)]; 118 | } 119 | 120 | Loading = false; 121 | NotFound = false; 122 | uid = newUid; 123 | try 124 | { 125 | Loading = true; 126 | if (string.IsNullOrWhiteSpace(history)) 127 | { 128 | await LoadingService.WrapExecutionAsync( 129 | async () => Document = await 130 | DocumentService.LoadDocumentAsync(uid)); 131 | NotFound = Document == null; 132 | Audit = false; 133 | } 134 | else 135 | { 136 | await LoadingService.WrapExecutionAsync( 137 | async () => Document = await 138 | DocumentService.LoadDocumentSnapshotAsync(Guid.Parse(history), uid)); 139 | Audit = true; 140 | NotFound = Document == null; 141 | } 142 | 143 | Loading = false; 144 | await TitleService.SetTitleAsync($"Viewing {Title}"); 145 | } 146 | catch 147 | { 148 | NotFound = true; 149 | } 150 | } 151 | 152 | await base.OnParametersSetAsync(); 153 | } 154 | 155 | /// 156 | /// Go back to main version of the document. 157 | /// 158 | protected void BackToMain() => 159 | NavigationService.NavigateTo( 160 | NavigationHelper.ViewDocument(Uid), 161 | true); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /PlanetaryDocs/Shared/ValidatedInputBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Components; 7 | using PlanetaryDocs.Domain; 8 | 9 | namespace PlanetaryDocs.Shared 10 | { 11 | /// 12 | /// Code for component. 13 | /// 14 | public class ValidatedInputBase : ComponentBase 15 | { 16 | private bool focused = false; 17 | private string innerValue = string.Empty; 18 | private ValidationState validationState; 19 | 20 | /// 21 | /// Gets or sets the placeholder text. 22 | /// 23 | [Parameter] 24 | public string PlaceHolder { get; set; } 25 | 26 | /// 27 | /// Gets or sets the external value for data-binding. 28 | /// 29 | [Parameter] 30 | public string Value { get; set; } 31 | 32 | /// 33 | /// Gets or sets the callback used to notify on value changes. 34 | /// 35 | [Parameter] 36 | public EventCallback ValueChanged { get; set; } 37 | 38 | /// 39 | /// Gets or sets a value indicating whether the input 40 | /// should take focus on errors. 41 | /// 42 | [Parameter] 43 | public bool AutoFocus { get; set; } 44 | 45 | /// 46 | /// Gets or sets the method used to validate the input. 47 | /// 48 | [Parameter] 49 | public Func Validate { get; set; } 50 | 51 | /// 52 | /// Gets or sets the state of the validation. 53 | /// 54 | [Parameter] 55 | public ValidationState Validation { get; set; } 56 | 57 | /// 58 | /// Gets or sets the callback to notify when the 59 | /// validation status changes. 60 | /// 61 | [Parameter] 62 | public EventCallback ValidationChanged { get; set; } 63 | 64 | /// 65 | /// Gets or sets a value indicating whether to use 66 | /// a textarea instead of an input. 67 | /// 68 | [Parameter] 69 | public bool UseTextArea { get; set; } 70 | 71 | /// 72 | /// Gets or sets the tab index. 73 | /// 74 | [Parameter] 75 | public string TabIndex { get; set; } = "0"; 76 | 77 | /// 78 | /// Gets or sets the reference to the textarea element. 79 | /// 80 | protected ElementReference TextAreaControl { get; set; } 81 | 82 | /// 83 | /// Gets or sets the reference to the input element. 84 | /// 85 | protected ElementReference InputControl { get; set; } 86 | 87 | /// 88 | /// Gets the active control based on the value of 89 | /// . 90 | /// 91 | protected ElementReference ActiveControl => 92 | UseTextArea ? TextAreaControl : InputControl; 93 | 94 | /// 95 | /// Gets the class to apply for errors. 96 | /// 97 | protected string Error => 98 | Validation != null && Validation.IsValid == false 99 | ? "error" : string.Empty; 100 | 101 | /// 102 | /// Gets or sets the internally tracked value. 103 | /// 104 | protected string InnerValue 105 | { 106 | get => innerValue; 107 | set 108 | { 109 | if (value != innerValue) 110 | { 111 | innerValue = value; 112 | Value = innerValue; 113 | InvokeAsync(async () => 114 | await ValueChanged.InvokeAsync(Value)); 115 | OnValidate(); 116 | } 117 | } 118 | } 119 | 120 | /// 121 | /// Called when validation is needed. 122 | /// 123 | public void OnValidate() 124 | { 125 | validationState = Validate(innerValue); 126 | 127 | if (Validation == null || 128 | validationState.IsValid != Validation.IsValid || 129 | validationState.Message != Validation.Message) 130 | { 131 | Validation = validationState; 132 | InvokeAsync(async () => 133 | await ValidationChanged.InvokeAsync(Validation)); 134 | } 135 | 136 | if (!validationState.IsValid && AutoFocus) 137 | { 138 | InvokeAsync( 139 | async () => 140 | await ActiveControl.FocusAsync()); 141 | } 142 | } 143 | 144 | /// 145 | /// Method to focus the control. 146 | /// 147 | /// An asynchronous task. 148 | public async Task FocusAsync() => await ActiveControl.FocusAsync(); 149 | 150 | /// 151 | /// Called when the value is initially set. 152 | /// 153 | protected override void OnParametersSet() 154 | { 155 | validationState = null; 156 | innerValue = Value; 157 | OnValidate(); 158 | base.OnParametersSet(); 159 | } 160 | 161 | /// 162 | /// Called after render. 163 | /// 164 | /// A value indicating whether the call is in context of the first render. 165 | /// An asynchronous task. 166 | protected override async Task OnAfterRenderAsync(bool firstRender) 167 | { 168 | if (AutoFocus && !focused) 169 | { 170 | focused = true; 171 | await ActiveControl.FocusAsync(); 172 | } 173 | 174 | await base.OnAfterRenderAsync(firstRender); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Planetary Docs 2 | 3 | __NEW__ upgraded to .NET 6 and EF Core 6! 4 | 5 | [![.NET 6 Builds](https://github.com/JeremyLikness/PlanetaryDocs/actions/workflows/dotnet.yml/badge.svg)](https://github.com/JeremyLikness/PlanetaryDocs/actions/workflows/dotnet.yml) 6 | 7 | [![.NET 6 Tests](https://github.com/JeremyLikness/PlanetaryDocs/actions/workflows/tests.yml/badge.svg)](https://github.com/JeremyLikness/PlanetaryDocs/actions/workflows/tests.yml) 8 | 9 | Welcome to Planetary Docs! This repository is intended to showcase a full 10 | application that supports Create, Read, Update, and Delete operations (CRUD) 11 | using Blazor (Server), Entity Framework Core and Azure Cosmos DB. 12 | 13 | Please read our [Code of Conduct](./CODE_OF_CONDUCT.md) for participating in 14 | discussions and contributions. If you are interested in contributing, please 15 | read our [Guide for Contributors](./CONTRIBUTING.md). 16 | 17 | This project is licensed under MIT. See the [license file](./LICENSE) for more information. 18 | 19 | > **Important Security Notice** This app is meant for demo purposes only. As implemented, it 20 | is not a production-ready app. More specifically, there are no users or roles defined and 21 | access is _not_ secured by a login. That means anyone with the URL can modify your 22 | document database. This issue is being tracked at [#2](https://github.com/JeremyLikness/PlanetaryDocs/issues/2). 23 | 24 | ## New: EF Core 6 25 | 26 | This project has been updated to use EF Core 6. This simplified the code a bit: 27 | 28 | - Removed converter for tags as EF Core 6 directly supports collections of primtives 29 | - Removed `HasMany` configuration as EF Core 6 recognizes implicit ownership of complex types for the Azure Cosmos DB provider 30 | - Added code to migrate from the old model that serialized tags as a single JSON string to the new model that stores first class string arrays 31 | 32 | > **Note:** If you are running the project for the first time, set `EnableMigrations` to `false` in the `appsettings.json` file. Leave it as is and make sure the id to check is valid 33 | if you are upgrading from 5.0. The first time you run the app, it will detect the old format and use the new `FromRawSql` capabilities to load the old format and save it 34 | to the new format. 35 | 36 | ## Quickstart 37 | 38 | Here's how to get started in a few easy steps. 39 | 40 | ### Clone this repo 41 | 42 | Using your preferred tools, clone the repository. The `git` commmand looks like this: 43 | 44 | ```bash 45 | git clone https://github.com/JeremyLikness/PlanetaryDocs 46 | ``` 47 | 48 | ### Create an Azure Cosmos DB instance 49 | 50 | To run this demo, you will need to create an Azure Cosmos DB account. You can read 51 | [Create an Azure Cosmos DB account](https://docs.microsoft.com/azure/cosmos-db/create-cosmosdb-resources-portal#create-an-azure-cosmos-db-account) to learn how. Be sure to check out the option 52 | for a [free account](https://docs.microsoft.com/azure/cosmos-db/optimize-dev-test#azure-cosmos-db-free-tier)! Choose the SQL API. 53 | 54 | > No Azure account? No worries! You can also run this project using the [Azure Cosmos DB emulator](https://docs.microsoft.com/azure/cosmos-db/local-emulator). 55 | 56 | ### Clone the ASP.NET Core docs repository 57 | 58 | This is what was used for testing. 59 | 60 | ```bash 61 | git clone https://github.com/dotnet/AspNetCore.Docs 62 | ``` 63 | 64 | ### Initialize the database 65 | 66 | Navigate to the `PlanetaryDocsLoader` project. 67 | 68 | Update `Program.cs` with: 69 | 70 | - The path to the root of the ASPNetCore.Docs repository 71 | - The Azure Cosmos DB endpoint 72 | - The Azure Cosmos DB key 73 | 74 | The endpoint is the `URI` and the key is the `Primary Key` on the **keys** pane of your Azure 75 | Cosmos DB account in the [Azure Portal](https://portal.azure.com/). 76 | 77 | Run the application (`dotnet run` from the command line). You should see status 78 | as it parses documents, loads them to the database and then runs tests. This step 79 | may take several minutes. 80 | 81 | ### Configure and run the Blazor app 82 | 83 | In the `PlanetaryDocs` Blazor Server project, either update the `CosmosSettings` 84 | in the `appsettings.json` file, or create a new section in `appsettings.Development.json` 85 | and add you access key and endpoint. Run the app. You should be ready to go! 86 | 87 | ## Project Details 88 | 89 | The following features were integrated into this project. 90 | 91 | `PlanetaryDocsLoader` parses the docs repository and inserts the 92 | documents into the database. It includes tests to verify the 93 | functionality is working. 94 | 95 | `PlanetaryDocs.Domain` hosts the domain classes, validation logic, 96 | and signature (interface) for data access. 97 | 98 | `PlanetaryDocs.DataAccess` contains the EF Core `DbContext` 99 | and an implementation of the data access service. 100 | 101 | - `DocsContext` 102 | - Has model-building code that shows how to map ownership 103 | - Uses value converters with JSON serialization to support primitives collection and nested 104 | complex types 105 | - Demonstrates use of partition keys, including how to define them for the 106 | model and how to specify them in queries 107 | - Provides an example of specifying the container by entity 108 | - Shows how to turn off the discriminator 109 | - Stores two entity types (aliases and tags) in the same container 110 | - Uses a "shadow property" to track partition keys on aliases and tags 111 | - Hooks into the `SavingChanges` event to automate the generation of audit snapshots 112 | - `DocumentService` 113 | - Shows various strategies for C.R.U.D. operations 114 | - Programmatically synchronizes related entities 115 | - Demonstrates how to handle updates with concurrency to disconnected entities 116 | - Uses the new `IDbContextFactory` implementation to manage context instances 117 | 118 | `PlanetaryDocs` is a Blazor Server app. 119 | 120 | - Examples of JavaScript interop in the `TitleService`, `HistoryService`, and `MultiLineEditService`. 121 | - Uses keyboard handlers to allow keyboard-based navigation and input on the edit page 122 | - Shows a generic autocomplete component with debounce built-in 123 | - `HtmlPreview` uses a phantom `textarea` to render an HTML preview 124 | - `MarkDig` is used to transform markdown into HTML 125 | - The `MultiLineEdit` component shows a workaround using JavaScript interop for limitations with fields that have large input values 126 | - The `Editor` component supports concurrency. If you open a document twice in separate tabs and edit in both, the second will notify that changes were made and provide the option to reset or overwrite 127 | 128 | Your feedback is valuable! Reach me online at [@JeremyLikness](https://twitter.com/JeremyLikness). 129 | 130 | 131 | -------------------------------------------------------------------------------- /PlanetaryDocs/Pages/EditBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.EntityFrameworkCore; 7 | using PlanetaryDocs.Domain; 8 | using PlanetaryDocs.Services; 9 | using PlanetaryDocs.Shared; 10 | 11 | namespace PlanetaryDocs.Pages 12 | { 13 | /// 14 | /// Base for edit component. 15 | /// 16 | public class EditBase : ComponentBase 17 | { 18 | private string uid = string.Empty; 19 | private bool isValid = false; 20 | 21 | /// 22 | /// Gets or sets the . 23 | /// 24 | [Inject] 25 | public NavigationManager NavigationService { get; set; } 26 | 27 | /// 28 | /// Gets or sets the implementation. 29 | /// 30 | [Inject] 31 | public IDocumentService DocumentService { get; set; } 32 | 33 | /// 34 | /// Gets or sets the loading service. 35 | /// 36 | [CascadingParameter] 37 | public LoadingService LoadingService { get; set; } 38 | 39 | /// 40 | /// Gets or sets the title service. 41 | /// 42 | [Inject] 43 | public TitleService TitleService { get; set; } 44 | 45 | /// 46 | /// Gets or sets the unique identifier of the document being edited. 47 | /// 48 | [Parameter] 49 | public string Uid { get; set; } 50 | 51 | /// 52 | /// Gets or sets a value indicating whether the 53 | /// is valid for an update. 54 | /// 55 | public bool IsValid 56 | { 57 | get => isValid; 58 | set 59 | { 60 | if (value != isValid) 61 | { 62 | isValid = value; 63 | InvokeAsync(StateHasChanged); 64 | } 65 | } 66 | } 67 | 68 | /// 69 | /// Gets a value indicating whether or not changes have been made. 70 | /// 71 | protected bool IsDirty => ChangeCount > 0; 72 | 73 | /// 74 | /// Gets or sets the count of detected changes. 75 | /// 76 | protected int ChangeCount { get; set; } 77 | 78 | /// 79 | /// Gets or sets a value indicating whether the 80 | /// could not be found. 81 | /// 82 | protected bool NotFound { get; set; } 83 | 84 | /// 85 | /// Gets or sets a value indicating whether a concurrency error was 86 | /// encountered during the last update attempt. 87 | /// 88 | protected bool Concurrency { get; set; } 89 | 90 | /// 91 | /// Gets or sets a value indicating whether items are being loaded. 92 | /// 93 | protected bool Loading { get; set; } 94 | 95 | /// 96 | /// Gets or sets a value indicating whether the 97 | /// is being saved. 98 | /// 99 | protected bool Saving { get; set; } 100 | 101 | /// 102 | /// Gets or sets the Document being edited. 103 | /// 104 | protected Document Document { get; set; } 105 | 106 | /// 107 | /// Gets or sets a reference to the child component. 108 | /// 109 | protected Editor Editor { get; set; } 110 | 111 | /// 112 | /// Save operation. 113 | /// 114 | /// An asynchronous task. 115 | public async Task SaveAsync() 116 | { 117 | if (!IsDirty || !IsValid || !Editor.ValidateAll(Document)) 118 | { 119 | return; 120 | } 121 | 122 | Saving = true; 123 | 124 | if (Concurrency) 125 | { 126 | Concurrency = false; 127 | Document original = null; 128 | 129 | await LoadingService.WrapExecutionAsync(async () => 130 | original = 131 | await DocumentService.LoadDocumentAsync(Document.Uid)); 132 | 133 | Document.ETag = original.ETag; 134 | } 135 | 136 | try 137 | { 138 | await LoadingService.WrapExecutionAsync(async () => 139 | await DocumentService.UpdateDocumentAsync(Document)); 140 | } 141 | catch (DbUpdateConcurrencyException) 142 | { 143 | Concurrency = true; 144 | } 145 | 146 | if (!Concurrency) 147 | { 148 | NavigationService.NavigateTo(NavigationHelper.ViewDocument(Document.Uid), true); 149 | } 150 | else 151 | { 152 | Saving = false; 153 | } 154 | } 155 | 156 | /// 157 | /// Called after render to set the title. 158 | /// 159 | /// A value indicating whether this is the first render. 160 | /// An asynchronous task. 161 | protected override async Task OnAfterRenderAsync(bool firstRender) 162 | { 163 | if (firstRender && !string.IsNullOrWhiteSpace(Uid)) 164 | { 165 | await TitleService.SetTitleAsync($"Editing '{Uid}'"); 166 | } 167 | 168 | await base.OnAfterRenderAsync(firstRender); 169 | } 170 | 171 | /// 172 | /// Called when the parameters are set. Triggers the document load. 173 | /// 174 | /// An asynchronous task. 175 | protected override async Task OnParametersSetAsync() 176 | { 177 | if (Uid != uid) 178 | { 179 | uid = Uid; 180 | Loading = true; 181 | Document = null; 182 | Concurrency = false; 183 | 184 | await LoadingService.WrapExecutionAsync( 185 | async () => Document = 186 | await DocumentService.LoadDocumentAsync(Uid)); 187 | 188 | NotFound = Document == null; 189 | ChangeCount = 0; 190 | Loading = false; 191 | } 192 | 193 | await base.OnParametersSetAsync(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | # All files 7 | [*] 8 | indent_style = space 9 | # Code files 10 | [*.{cs,csx,vb,vbx}] 11 | indent_size = 4 12 | trim_trailing_whitespace = true; 13 | insert_final_newline = true 14 | charset = utf-8-bom 15 | ############################### 16 | # .NET Coding Conventions # 17 | ############################### 18 | [*.{cs,vb}] 19 | # Organize usings 20 | dotnet_sort_system_directives_first = true 21 | 22 | # this. preferences 23 | dotnet_style_qualification_for_field = false:silent 24 | dotnet_style_qualification_for_property = false:silent 25 | dotnet_style_qualification_for_method = false:silent 26 | dotnet_style_qualification_for_event = false:silent 27 | 28 | # Language keywords vs BCL types preferences 29 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 30 | dotnet_style_predefined_type_for_member_access = true:silent 31 | 32 | # Parentheses preferences 33 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 34 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 35 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 37 | 38 | # Modifier preferences 39 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 40 | dotnet_style_readonly_field = true:suggestion 41 | 42 | # Expression-level preferences 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_explicit_tuple_names = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_coalesce_expression = true:suggestion 48 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 49 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 50 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 51 | dotnet_style_prefer_auto_properties = true:silent 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | 55 | ############################### 56 | # Naming Conventions # 57 | ############################### 58 | 59 | # Style Definitions 60 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 61 | 62 | # Use PascalCase for constant fields 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 65 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 66 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 67 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 68 | dotnet_naming_symbols.constant_fields.required_modifiers = const 69 | 70 | ############################### 71 | # C# Coding Conventions # 72 | ############################### 73 | [*.cs] 74 | 75 | # var preferences 76 | csharp_style_var_for_built_in_types = true:silent 77 | csharp_style_var_when_type_is_apparent = true:silent 78 | csharp_style_var_elsewhere = true:silent 79 | 80 | # Expression-bodied members 81 | csharp_style_expression_bodied_methods = true:suggestion 82 | csharp_style_expression_bodied_constructors = true:suggestion 83 | csharp_style_expression_bodied_operators = true:suggestion 84 | csharp_style_expression_bodied_properties = true:suggestion 85 | csharp_style_expression_bodied_indexers = true:suggestion 86 | csharp_style_expression_bodied_accessors = true:suggestion 87 | 88 | # Pattern matching preferences 89 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 90 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 91 | 92 | # Null-checking preferences 93 | csharp_style_throw_expression = true:suggestion 94 | csharp_style_conditional_delegate_call = true:suggestion 95 | 96 | # Modifier preferences 97 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 98 | 99 | # Expression-level preferences 100 | csharp_prefer_braces = true:silent 101 | csharp_style_deconstructed_variable_declaration = true:suggestion 102 | csharp_prefer_simple_default_expression = true:suggestion 103 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 104 | csharp_style_inlined_variable_declaration = true:suggestion 105 | 106 | ############################### 107 | # C# Formatting Rules # 108 | ############################### 109 | 110 | # New line preferences 111 | csharp_new_line_before_open_brace = all 112 | csharp_new_line_before_else = true 113 | csharp_new_line_before_catch = true 114 | csharp_new_line_before_finally = true 115 | csharp_new_line_before_members_in_object_initializers = true 116 | csharp_new_line_before_members_in_anonymous_types = true 117 | csharp_new_line_between_query_expression_clauses = true 118 | 119 | # Indentation preferences 120 | csharp_indent_case_contents = true 121 | csharp_indent_switch_labels = true 122 | csharp_indent_labels = flush_left 123 | 124 | # Space preferences 125 | csharp_space_after_cast = false 126 | csharp_space_after_keywords_in_control_flow_statements = true 127 | csharp_space_between_method_call_parameter_list_parentheses = false 128 | csharp_space_between_method_declaration_parameter_list_parentheses = false 129 | csharp_space_between_parentheses = false 130 | csharp_space_before_colon_in_inheritance_clause = true 131 | csharp_space_after_colon_in_inheritance_clause = true 132 | csharp_space_around_binary_operators = before_and_after 133 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 134 | csharp_space_between_method_call_name_and_opening_parenthesis = false 135 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 136 | 137 | # Wrapping preferences 138 | csharp_preserve_single_line_statements = true 139 | csharp_preserve_single_line_blocks = true 140 | 141 | ############################### 142 | # VB Coding Conventions # 143 | ############################### 144 | 145 | # SA1200: Using directives should be placed correctly 146 | dotnet_diagnostic.SA1200.severity = none 147 | 148 | # SA1101: Prefix local calls with this 149 | dotnet_diagnostic.SA1101.severity = silent 150 | 151 | [*.vb] 152 | # Modifier preferences 153 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 154 | -------------------------------------------------------------------------------- /PlanetaryDocsLoader/MarkdownParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Linq; 9 | using Markdig; 10 | using PlanetaryDocs.Domain; 11 | 12 | namespace PlanetaryDocsLoader 13 | { 14 | /// 15 | /// Manages transformation of markdown to HTML. 16 | /// 17 | public static class MarkdownParser 18 | { 19 | /// 20 | /// Parses a list of files to a list of instances. 21 | /// 22 | /// The file list. 23 | /// The parsed list. 24 | public static List ParseFiles( 25 | IEnumerable filesToParse) 26 | { 27 | var docsList = new List(); 28 | 29 | foreach (var file in filesToParse) 30 | { 31 | Console.WriteLine("==="); 32 | var fileName = file.Split(Path.DirectorySeparatorChar)[^1]; 33 | Console.WriteLine($"DOC:\t{fileName}"); 34 | var lines = File.ReadAllLines(file); 35 | Console.WriteLine($"LINES:\t{lines.Length}"); 36 | var doc = new Document(); 37 | bool metaStart = false, 38 | metaEnd = false, 39 | titleFound = false, 40 | aliasFound = false, 41 | descriptionFound = false, 42 | dateFound = false, 43 | uidFound = false; 44 | 45 | var markdown = new List(); 46 | 47 | for (var idx = 0; idx < lines.Length; idx++) 48 | { 49 | var line = lines[idx]; 50 | 51 | if (!metaStart) 52 | { 53 | if (line.StartsWith("---")) 54 | { 55 | metaStart = true; 56 | } 57 | 58 | continue; 59 | } 60 | 61 | if (!metaEnd) 62 | { 63 | if (line.StartsWith("---")) 64 | { 65 | metaEnd = true; 66 | continue; 67 | } 68 | else 69 | { 70 | var metadata = line.Split(":"); 71 | var key = metadata[0].Trim().ToLowerInvariant(); 72 | switch (key) 73 | { 74 | case "title": 75 | titleFound = true; 76 | doc.Title = metadata[1].Trim(); 77 | Console.WriteLine($"TITLE:\t{doc.Title}"); 78 | break; 79 | 80 | case "uid": 81 | uidFound = true; 82 | doc.Uid = metadata[1].Trim().Replace('/', '_'); 83 | Console.WriteLine($"UID:\t{doc.Uid}"); 84 | break; 85 | 86 | case "description": 87 | descriptionFound = true; 88 | doc.Description = metadata[1].Trim(); 89 | break; 90 | 91 | case "ms.author": 92 | aliasFound = true; 93 | doc.AuthorAlias = metadata[1].Trim(); 94 | Console.WriteLine($"AUTHOR:\t{doc.AuthorAlias}"); 95 | break; 96 | 97 | case "ms.date": 98 | dateFound = true; 99 | doc.PublishDate = DateTime.ParseExact( 100 | metadata[1].Trim(), 101 | "M/d/yyyy", 102 | CultureInfo.InvariantCulture); 103 | Console.WriteLine($"PUB DATE:\t{doc.PublishDate}"); 104 | break; 105 | 106 | case "no-loc": 107 | var tags = metadata[1].Trim() 108 | .Replace("[", string.Empty) 109 | .Replace("]", string.Empty) 110 | .Replace("\"", string.Empty) 111 | .Split(","); 112 | foreach (var tag in tags.Select( 113 | t => t.Trim()) 114 | .Where(t => !string.IsNullOrWhiteSpace(t)) 115 | .Distinct()) 116 | { 117 | doc.Tags.Add(tag); 118 | } 119 | 120 | var tagList = string.Join(", ", doc.Tags); 121 | Console.WriteLine($"TAGS:\t{tagList}"); 122 | break; 123 | 124 | case "default": 125 | continue; 126 | } 127 | 128 | continue; 129 | } 130 | } 131 | 132 | markdown.Add(line); 133 | } 134 | 135 | var valid = titleFound && aliasFound && descriptionFound && dateFound 136 | && uidFound; 137 | 138 | if (valid) 139 | { 140 | Console.WriteLine("VALID"); 141 | doc.Markdown = string.Join(Environment.NewLine, markdown); 142 | doc.Html = Markdown.ToHtml(doc.Markdown); 143 | docsList.Add(doc); 144 | 145 | // hack 146 | if (doc.Title.Contains("Tutorial")) 147 | { 148 | doc.Title = $"Tutorial: {doc.Description}"; 149 | if (doc.Title.Length > 60) 150 | { 151 | doc.Title = $"{doc.Title.Substring(0, 59)}..."; 152 | } 153 | } 154 | } 155 | else 156 | { 157 | Console.WriteLine("INVALID"); 158 | continue; 159 | } 160 | } 161 | 162 | Console.WriteLine($"Processed {docsList.Count} documents."); 163 | return docsList; 164 | } 165 | } 166 | } 167 | --------------------------------------------------------------------------------