From e4e1245f74bf31a497a73c710d5154d82897548d Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 3 Dec 2024 12:58:13 -0500 Subject: [PATCH 01/15] Removed translations that are not available --- SharpSite.Web/Locales/Configuration.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SharpSite.Web/Locales/Configuration.cs b/SharpSite.Web/Locales/Configuration.cs index 41ee48b..af2f6df 100644 --- a/SharpSite.Web/Locales/Configuration.cs +++ b/SharpSite.Web/Locales/Configuration.cs @@ -8,10 +8,10 @@ public static class Configuration "es", "fi", "fr", - "it", + //"it", "nl", - "pt", - "sv", + //"pt", + //"sv", "sw", "de", }; From e0f8a09024b6d33768553821d12fda62e456e476 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 17 Feb 2025 12:36:42 -0500 Subject: [PATCH 02/15] Updated favicon to be SharpSite S --- src/SharpSite.Web/wwwroot/favicon.png | Bin 1148 -> 11063 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/SharpSite.Web/wwwroot/favicon.png b/src/SharpSite.Web/wwwroot/favicon.png index 8422b59695935d180d11d5dbe99653e711097819..6d52c9941fd5a771eb021f5ab4eb8b1b9dd9aae3 100644 GIT binary patch literal 11063 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 Iq*>_y0wgd8ssI20 literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~ Date: Tue, 18 Feb 2025 12:49:07 -0500 Subject: [PATCH 03/15] Fix post date (#307) --- build-and-test.ps1 | 2 +- e2e/SharpSite.E2E/AuthenticatedPageTests.cs | 54 +++++++++++++- e2e/SharpSite.E2E/CreatePostTests.cs | 71 +++++++++++++++++++ e2e/SharpSite.E2E/FirstLoginTests.cs | 62 ++++++++-------- e2e/SharpSite.E2E/FirstWebsiteTests.cs | 1 + e2e/SharpSite.E2E/ManageXUnitTestSuite.cs | 55 ++++++++++++++ e2e/SharpSite.E2E/PostNavigationExtensions.cs | 23 ++++++ e2e/SharpSite.E2E/ProfileTests.cs | 5 ++ e2e/SharpSite.E2E/SharpSitePageTest.cs | 2 +- e2e/SharpSite.E2E/WithTestNameAttribute.cs | 20 ++++++ .../PgPostRepository.cs | 2 +- .../Components/Admin/EditPost.razor | 6 +- src/SharpSite.Web/Components/PostView.razor | 3 +- 13 files changed, 269 insertions(+), 37 deletions(-) create mode 100644 e2e/SharpSite.E2E/CreatePostTests.cs create mode 100644 e2e/SharpSite.E2E/ManageXUnitTestSuite.cs create mode 100644 e2e/SharpSite.E2E/PostNavigationExtensions.cs create mode 100644 e2e/SharpSite.E2E/WithTestNameAttribute.cs diff --git a/build-and-test.ps1 b/build-and-test.ps1 index 1a32ba8..e78f19e 100644 --- a/build-and-test.ps1 +++ b/build-and-test.ps1 @@ -47,7 +47,7 @@ Write-Host "Website is running!" -ForegroundColor Green # Set-Location -Path "$PSScriptRoot/e2e/SharpSite.E2E" # Run Playwright tests using dotnet test -dotnet test ./e2e/SharpSite.E2E/SharpSite.E2E.csproj --logger trx --results-directory "playwright-test-results" +dotnet test ./e2e/SharpSite.E2E/SharpSite.E2E.csproj --logger trx --results-directory "playwright-test-results" -- xUnit.MaxParallelThreads=5 if ($LASTEXITCODE -ne 0) { Write-Host "Playwright tests failed!" -ForegroundColor Red diff --git a/e2e/SharpSite.E2E/AuthenticatedPageTests.cs b/e2e/SharpSite.E2E/AuthenticatedPageTests.cs index 7cdfa51..9b1345f 100644 --- a/e2e/SharpSite.E2E/AuthenticatedPageTests.cs +++ b/e2e/SharpSite.E2E/AuthenticatedPageTests.cs @@ -2,22 +2,70 @@ namespace SharpSite.E2E; +/// +/// This class is used to test pages where we are logged in as a user. +/// +[WithTestName] public abstract class AuthenticatedPageTests : SharpSitePageTest { - private const string URL_LOGIN = "/Account/Login"; private const string LOGIN_USERID = "admin@Localhost"; private const string LOGIN_PASSWORD = "Admin123!"; + public static readonly bool RunTrace = true; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + Context.SetDefaultNavigationTimeout(10000); + Context.SetDefaultTimeout(10000); + + if (RunTrace) + { + await Context.Tracing.StartAsync(new() + { + Title = $"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}", + Screenshots = true, + Snapshots = true, + Sources = true + }); + } + + } + + public override async Task DisposeAsync() + { + + if (RunTrace) + await Context.Tracing.StopAsync(new() + { + Path = Path.Combine( + Environment.CurrentDirectory, + "playwright-traces", + $"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}.zip" + ) + }); + await base.DisposeAsync().ConfigureAwait(false); + } + + protected async Task LoginAsDefaultAdmin() { + await Page.GotoAsync(URL_LOGIN); - await Page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); + //await Page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Email" }) .FillAsync(LOGIN_USERID); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Password" }) .FillAsync(LOGIN_PASSWORD); await Page.GetByRole(AriaRole.Button, new() { Name = "loginbutton" }).ClickAsync(); + //await Context.StorageStateAsync(new() + //{ + // Path = ".auth.json" + //}); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + Console.WriteLine("Logged in as " + LOGIN_USERID); + } protected async Task Logout() @@ -26,3 +74,5 @@ protected async Task Logout() } } + + diff --git a/e2e/SharpSite.E2E/CreatePostTests.cs b/e2e/SharpSite.E2E/CreatePostTests.cs new file mode 100644 index 0000000..91632f4 --- /dev/null +++ b/e2e/SharpSite.E2E/CreatePostTests.cs @@ -0,0 +1,71 @@ +using Microsoft.Playwright; + +namespace SharpSite.E2E; + + +[WithTestName] +public class CreatePostTests : AuthenticatedPageTests +{ + + // create a playwright test that logs in, navigates to the create post page, fills in the form and submits it + [Fact] + public async Task CreatePost() + { + const string PostTitle = "Test Post"; + + await LoginAsDefaultAdmin(); + await Page.NavigateToCreatePost(); + + await Page.GetByPlaceholder("Title").ClickAsync(); + await Page.GetByPlaceholder("Title").FillAsync(PostTitle); + await Page.GetByRole(AriaRole.Application).GetByRole(AriaRole.Textbox).FillAsync("This is a test"); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle, Exact = true })).ToBeVisibleAsync(); + + await Page.NavigateToPost(PostTitle); + + var title = await Page.Locator("h1").InnerTextAsync(); + Assert.Equal(PostTitle, title); + + } + + // create a new post with a date in the past + [Fact] + public async Task CreatePostWithDateInPast() + { + const string PostTitle = "Test Post in the past"; + + await LoginAsDefaultAdmin(); + await Page.NavigateToCreatePost(); + + await Page.GetByPlaceholder("Title").ClickAsync(); + + await Page.GetByPlaceholder("Title").FillAsync(PostTitle); + await Page.GetByRole(AriaRole.Application).GetByRole(AriaRole.Textbox).FillAsync("This is a test"); + + DateTime postDate = new DateTime(2020, 1, 1).Date; + await Page.GetByLabel("Publish Date").FillAsync(postDate.ToString("yyyy-MM-dd")); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle })).ToBeVisibleAsync(); + + await Page.NavigateToPost(PostTitle); + + var title = await Page.Locator("h1").InnerTextAsync(); + Assert.Equal(PostTitle, title); + + // check that the date in the h6 is in the past + var date = await Page.Locator("h6").InnerTextAsync(); + Assert.True(DateTime.TryParse(date, out var result)); + Assert.Equal(postDate, result.Date); + + + } + +} + diff --git a/e2e/SharpSite.E2E/FirstLoginTests.cs b/e2e/SharpSite.E2E/FirstLoginTests.cs index 98f28f0..4bd9780 100644 --- a/e2e/SharpSite.E2E/FirstLoginTests.cs +++ b/e2e/SharpSite.E2E/FirstLoginTests.cs @@ -1,43 +1,49 @@ using Microsoft.Playwright; +using Xunit; namespace SharpSite.E2E; + public class FirstLoginTests : AuthenticatedPageTests { + [Fact] + public async Task CanLogin() + { + await LoginAsDefaultAdmin(); + await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "loggedin.png" }); + + // check for the manage profile link with the text "Site Admin" + await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Site Admin" })).ToBeVisibleAsync(); + + } + + // add a test that logs in and then logs out + [Fact] + public Task CanLogout() + { + return Task.CompletedTask; + + // await LoginAsDefaultAdmin(); + // await Logout(); + // await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "loggedout.png" }); + // // check for the login link + // await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Login" })).ToBeVisibleAsync(); + } +} - [Fact] - public async Task HasLoginLink() - { - await Page.GotoAsync("/"); - // Click the get started link. - await Page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); - // take a screenshot - await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "login.png" }); - // Expects page to have a heading with the name of Installation. - await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Login" })).ToBeVisibleAsync(); - } +public class FirstVisitTests : SharpSitePageTest +{ - // add a test that clicks the login link and then logs in + // add a test that visits the home page and takes a screenshot [Fact] - public async Task CanLogin() + public async Task CanVisitHomePage() { - await LoginAsDefaultAdmin(); - await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "loggedin.png" }); - // check for the manage profile link with the text "Site Admin" - await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Site Admin" })).ToBeVisibleAsync(); - - } - - // add a test that logs in and then logs out - [Fact] - public async Task CanLogout() - { - await LoginAsDefaultAdmin(); - await Logout(); - await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "loggedout.png" }); + await Page.GotoAsync("/"); + await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "home.png" }); // check for the login link await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "Login" })).ToBeVisibleAsync(); } -} + +} \ No newline at end of file diff --git a/e2e/SharpSite.E2E/FirstWebsiteTests.cs b/e2e/SharpSite.E2E/FirstWebsiteTests.cs index 19e82f7..42f6ec2 100644 --- a/e2e/SharpSite.E2E/FirstWebsiteTests.cs +++ b/e2e/SharpSite.E2E/FirstWebsiteTests.cs @@ -11,6 +11,7 @@ public async Task HasAboutSharpSiteLink() await Page.GotoAsync("/"); // Click the get started link. await Page.GetByRole(AriaRole.Link, new() { Name = "About SharpSite" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); // take a screenshot await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "about-sharpsite.png" }); diff --git a/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs b/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs new file mode 100644 index 0000000..9c0af5b --- /dev/null +++ b/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs @@ -0,0 +1,55 @@ +using Microsoft.Playwright; + +namespace SharpSite.E2E; + +public class ManageXUnitTestSuite //: IAsyncLifetime +{ + + + private const string URL_LOGIN = "/Account/Login"; + private const string LOGIN_USERID = "admin@Localhost"; + private const string LOGIN_PASSWORD = "Admin123!"; + + public Task DisposeAsync() + { + if (File.Exists(".auth.json")) File.Delete(".auth.json"); + + return Task.CompletedTask; + } + + public async Task InitializeAsync() + { + if (File.Exists(".auth.json")) File.Delete(".auth.json"); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + var context = await browser.NewContextAsync(new BrowserNewContextOptions() + { + ColorScheme = ColorScheme.Light, + Locale = "en-US", + ViewportSize = new() + { + // set the viewport to 1024x768 + Width = 1024, + Height = 768, + }, + BaseURL = "http://localhost:5020" + }); + // create a new page + var page = await context.NewPageAsync(); + await page.GotoAsync(URL_LOGIN); + await page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); + await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Email" }) + .FillAsync(LOGIN_USERID); + await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Password" }) + .FillAsync(LOGIN_PASSWORD); + await page.GetByRole(AriaRole.Button, new() { Name = "loginbutton" }).ClickAsync(); + await context.StorageStateAsync(new() + { + Path = ".auth.json" + }); + + } +} + + diff --git a/e2e/SharpSite.E2E/PostNavigationExtensions.cs b/e2e/SharpSite.E2E/PostNavigationExtensions.cs new file mode 100644 index 0000000..21298bb --- /dev/null +++ b/e2e/SharpSite.E2E/PostNavigationExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Playwright; + +namespace SharpSite.E2E; + +internal static class PostNavigationExtensions +{ + public static async Task NavigateToPost(this IPage page, string postTitle) + { + await page.GotoAsync("/"); + await page.GetByRole(AriaRole.Link, new() { Name = postTitle, Exact = true }).ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Task.Delay(1000); + } + + // navigate to the create post page + public static async Task NavigateToCreatePost(this IPage page) + { + await page.GotoAsync("/admin/post"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + +} + diff --git a/e2e/SharpSite.E2E/ProfileTests.cs b/e2e/SharpSite.E2E/ProfileTests.cs index 39daa06..457c8e8 100644 --- a/e2e/SharpSite.E2E/ProfileTests.cs +++ b/e2e/SharpSite.E2E/ProfileTests.cs @@ -12,6 +12,8 @@ public async Task CanViewProfile() await LoginAsDefaultAdmin(); await Page.GotoAsync("/Account/Manage"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "profile.png" }); // check for the manage profile link with the text "Site Admin" await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Manage Profile" })).ToBeVisibleAsync(); @@ -27,9 +29,12 @@ public async Task CanChangePhoneNumber() var testPhoneNumber = Random.Shared.NextInt64(1000000000, 9999999999).ToString(); await Page.GetByLabel("Manage Profile").ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Page.GetByPlaceholder("Enter your phone number").ClickAsync(); await Page.GetByPlaceholder("Enter your phone number").FillAsync(testPhoneNumber); await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Page.ScreenshotAsync(new PageScreenshotOptions() { Path = "profile-changedphonenumber.png" }); diff --git a/e2e/SharpSite.E2E/SharpSitePageTest.cs b/e2e/SharpSite.E2E/SharpSitePageTest.cs index 81f9473..79ab778 100644 --- a/e2e/SharpSite.E2E/SharpSitePageTest.cs +++ b/e2e/SharpSite.E2E/SharpSitePageTest.cs @@ -18,7 +18,7 @@ public override BrowserNewContextOptions ContextOptions() Width = 1024, Height = 768, }, - BaseURL = "http://localhost:5020", + BaseURL = "http://localhost:5020" }; } diff --git a/e2e/SharpSite.E2E/WithTestNameAttribute.cs b/e2e/SharpSite.E2E/WithTestNameAttribute.cs new file mode 100644 index 0000000..5c21ef6 --- /dev/null +++ b/e2e/SharpSite.E2E/WithTestNameAttribute.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using Xunit.Sdk; + +namespace SharpSite.E2E; + +public class WithTestNameAttribute : BeforeAfterTestAttribute +{ + public static string CurrentTestName = string.Empty; + public static string CurrentClassName = string.Empty; + + public override void Before(MethodInfo methodInfo) + { + CurrentTestName = methodInfo.Name; + CurrentClassName = methodInfo.DeclaringType!.Name; + } + + public override void After(MethodInfo methodInfo) + { + } +} diff --git a/src/SharpSite.Data.Postgres/PgPostRepository.cs b/src/SharpSite.Data.Postgres/PgPostRepository.cs index a0fc475..2f9f63b 100644 --- a/src/SharpSite.Data.Postgres/PgPostRepository.cs +++ b/src/SharpSite.Data.Postgres/PgPostRepository.cs @@ -19,7 +19,7 @@ public PgPostRepository(IServiceProvider serviceProvider) public async Task AddPost(Post post) { // add a post to the database - post.PublishedDate = DateTimeOffset.Now; + //post.PublishedDate = DateTimeOffset.Now; post.LastUpdate = DateTimeOffset.Now; await Context.Posts.AddAsync((PgPost)post); await Context.SaveChangesAsync(); diff --git a/src/SharpSite.Web/Components/Admin/EditPost.razor b/src/SharpSite.Web/Components/Admin/EditPost.razor index 9b0afa9..f87f27a 100644 --- a/src/SharpSite.Web/Components/Admin/EditPost.razor +++ b/src/SharpSite.Web/Components/Admin/EditPost.razor @@ -41,7 +41,7 @@
- +
} @@ -77,13 +77,13 @@ // flush the outputcache for the sitemap and rss await FlushCache(); - NavManager.NavigateTo("/"); + NavManager.NavigateTo("/admin/posts"); } else { await PostService.UpdatePost(Post); await FlushCache(); - NavManager.NavigateTo("/"); + NavManager.NavigateTo("/admin/posts"); } } diff --git a/src/SharpSite.Web/Components/PostView.razor b/src/SharpSite.Web/Components/PostView.razor index 9e3d2fe..8831206 100644 --- a/src/SharpSite.Web/Components/PostView.razor +++ b/src/SharpSite.Web/Components/PostView.razor @@ -1,8 +1,9 @@ +

@item.Title

@item.PublishedDate.LocalDateTime.ToShortDateString()

@item.Description

- +
@code { [Parameter, EditorRequired] public required Post item { get; set; } From 6d84840e98839d2e20a83f50ec8a641a7fd41e62 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 20 Feb 2025 11:03:34 -0500 Subject: [PATCH 04/15] Added test and delete button on post list (#308) --- .../AuthenticatedPageTests.cs | 3 +- .../{ => Abstractions}/SharpSitePageTest.cs | 2 +- .../{ => Fixtures}/CreatePostTests.cs | 8 ++-- e2e/SharpSite.E2E/Fixtures/DeletePostTests.cs | 40 +++++++++++++++++++ .../{ => Fixtures}/FirstLoginTests.cs | 3 +- .../{ => Fixtures}/FirstWebsiteTests.cs | 3 +- .../{ => Fixtures}/ProfileTests.cs | 3 +- .../Posts.cs} | 4 +- e2e/SharpSite.E2E/README.md | 11 +++++ e2e/SharpSite.E2E/SharpSite.E2E.csproj | 4 ++ .../Components/Admin/PostList.razor | 14 +++++++ 11 files changed, 83 insertions(+), 12 deletions(-) rename e2e/SharpSite.E2E/{ => Abstractions}/AuthenticatedPageTests.cs (96%) rename e2e/SharpSite.E2E/{ => Abstractions}/SharpSitePageTest.cs (92%) rename e2e/SharpSite.E2E/{ => Fixtures}/CreatePostTests.cs (93%) create mode 100644 e2e/SharpSite.E2E/Fixtures/DeletePostTests.cs rename e2e/SharpSite.E2E/{ => Fixtures}/FirstLoginTests.cs (95%) rename e2e/SharpSite.E2E/{ => Fixtures}/FirstWebsiteTests.cs (90%) rename e2e/SharpSite.E2E/{ => Fixtures}/ProfileTests.cs (95%) rename e2e/SharpSite.E2E/{PostNavigationExtensions.cs => Navigation/Posts.cs} (88%) create mode 100644 e2e/SharpSite.E2E/README.md diff --git a/e2e/SharpSite.E2E/AuthenticatedPageTests.cs b/e2e/SharpSite.E2E/Abstractions/AuthenticatedPageTests.cs similarity index 96% rename from e2e/SharpSite.E2E/AuthenticatedPageTests.cs rename to e2e/SharpSite.E2E/Abstractions/AuthenticatedPageTests.cs index 9b1345f..eb53005 100644 --- a/e2e/SharpSite.E2E/AuthenticatedPageTests.cs +++ b/e2e/SharpSite.E2E/Abstractions/AuthenticatedPageTests.cs @@ -1,6 +1,6 @@ using Microsoft.Playwright; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Abstractions; /// /// This class is used to test pages where we are logged in as a user. @@ -64,7 +64,6 @@ protected async Task LoginAsDefaultAdmin() // Path = ".auth.json" //}); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - Console.WriteLine("Logged in as " + LOGIN_USERID); } diff --git a/e2e/SharpSite.E2E/SharpSitePageTest.cs b/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs similarity index 92% rename from e2e/SharpSite.E2E/SharpSitePageTest.cs rename to e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs index 79ab778..5c2165b 100644 --- a/e2e/SharpSite.E2E/SharpSitePageTest.cs +++ b/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs @@ -1,7 +1,7 @@ using Microsoft.Playwright; using Microsoft.Playwright.Xunit; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Abstractions; public abstract class SharpSitePageTest : PageTest { diff --git a/e2e/SharpSite.E2E/CreatePostTests.cs b/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs similarity index 93% rename from e2e/SharpSite.E2E/CreatePostTests.cs rename to e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs index 91632f4..9ece800 100644 --- a/e2e/SharpSite.E2E/CreatePostTests.cs +++ b/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs @@ -1,9 +1,10 @@ using Microsoft.Playwright; +using SharpSite.E2E.Abstractions; +using SharpSite.E2E.Navigation; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Fixtures; -[WithTestName] public class CreatePostTests : AuthenticatedPageTests { @@ -52,7 +53,7 @@ public async Task CreatePostWithDateInPast() await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle, Exact = true })).ToBeVisibleAsync(); await Page.NavigateToPost(PostTitle); @@ -68,4 +69,3 @@ public async Task CreatePostWithDateInPast() } } - diff --git a/e2e/SharpSite.E2E/Fixtures/DeletePostTests.cs b/e2e/SharpSite.E2E/Fixtures/DeletePostTests.cs new file mode 100644 index 0000000..330a578 --- /dev/null +++ b/e2e/SharpSite.E2E/Fixtures/DeletePostTests.cs @@ -0,0 +1,40 @@ +using Microsoft.Playwright; +using SharpSite.Abstractions; +using SharpSite.E2E.Abstractions; +using SharpSite.E2E.Navigation; + +namespace SharpSite.E2E.Fixtures; + +public class DeletePostTests : AuthenticatedPageTests +{ + // create a playwright test that logs in, navigates to the create post page, fills in the form and submits it + [Fact] + public async Task DeletePost() + { + + // ARRANGE - create a post to delets + const string PostTitle = "Test Post to delete"; + await LoginAsDefaultAdmin(); + + await Page.NavigateToCreatePost(); + + await Page.GetByPlaceholder("Title").ClickAsync(); + await Page.GetByPlaceholder("Title").FillAsync(PostTitle); + + await Page.GetByRole(AriaRole.Application).GetByRole(AriaRole.Textbox).FillAsync("This is a test"); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // ACT - now on the posts page, delete the post + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle, Exact = true })).ToBeVisibleAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = $"delete-{Post.GetSlug(PostTitle)}" }).ClickAsync(); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // ASSERT + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle, Exact = true })).Not.ToBeVisibleAsync(); + + } + + +} \ No newline at end of file diff --git a/e2e/SharpSite.E2E/FirstLoginTests.cs b/e2e/SharpSite.E2E/Fixtures/FirstLoginTests.cs similarity index 95% rename from e2e/SharpSite.E2E/FirstLoginTests.cs rename to e2e/SharpSite.E2E/Fixtures/FirstLoginTests.cs index 4bd9780..fa05341 100644 --- a/e2e/SharpSite.E2E/FirstLoginTests.cs +++ b/e2e/SharpSite.E2E/Fixtures/FirstLoginTests.cs @@ -1,7 +1,8 @@ using Microsoft.Playwright; +using SharpSite.E2E.Abstractions; using Xunit; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Fixtures; public class FirstLoginTests : AuthenticatedPageTests diff --git a/e2e/SharpSite.E2E/FirstWebsiteTests.cs b/e2e/SharpSite.E2E/Fixtures/FirstWebsiteTests.cs similarity index 90% rename from e2e/SharpSite.E2E/FirstWebsiteTests.cs rename to e2e/SharpSite.E2E/Fixtures/FirstWebsiteTests.cs index 42f6ec2..fd6f40a 100644 --- a/e2e/SharpSite.E2E/FirstWebsiteTests.cs +++ b/e2e/SharpSite.E2E/Fixtures/FirstWebsiteTests.cs @@ -1,6 +1,7 @@ using Microsoft.Playwright; +using SharpSite.E2E.Abstractions; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Fixtures; public class FirstWebsiteTests : SharpSitePageTest { diff --git a/e2e/SharpSite.E2E/ProfileTests.cs b/e2e/SharpSite.E2E/Fixtures/ProfileTests.cs similarity index 95% rename from e2e/SharpSite.E2E/ProfileTests.cs rename to e2e/SharpSite.E2E/Fixtures/ProfileTests.cs index 457c8e8..5791120 100644 --- a/e2e/SharpSite.E2E/ProfileTests.cs +++ b/e2e/SharpSite.E2E/Fixtures/ProfileTests.cs @@ -1,6 +1,7 @@ using Microsoft.Playwright; +using SharpSite.E2E.Abstractions; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Fixtures; public class ProfileTests : AuthenticatedPageTests { diff --git a/e2e/SharpSite.E2E/PostNavigationExtensions.cs b/e2e/SharpSite.E2E/Navigation/Posts.cs similarity index 88% rename from e2e/SharpSite.E2E/PostNavigationExtensions.cs rename to e2e/SharpSite.E2E/Navigation/Posts.cs index 21298bb..d55ddef 100644 --- a/e2e/SharpSite.E2E/PostNavigationExtensions.cs +++ b/e2e/SharpSite.E2E/Navigation/Posts.cs @@ -1,8 +1,8 @@ using Microsoft.Playwright; -namespace SharpSite.E2E; +namespace SharpSite.E2E.Navigation; -internal static class PostNavigationExtensions +internal static class Posts { public static async Task NavigateToPost(this IPage page, string postTitle) { diff --git a/e2e/SharpSite.E2E/README.md b/e2e/SharpSite.E2E/README.md new file mode 100644 index 0000000..6cdfdb3 --- /dev/null +++ b/e2e/SharpSite.E2E/README.md @@ -0,0 +1,11 @@ +# SharpSite.E2E + +This is the first end-to-end testing project for SharpSite. It uses xUnit and Playwright to exercise the application and ensure things are working properly. + +## Folder Structure + +There are three main folders in use inside this project to enable different C# capabilities that we need in order to execute tests using playwright the folders are + +- Abstractions containsthe resources that we reuse across multiple tests +- Fixtures contains the test classes +- Navigation contains extra classes that help with navigating the website diff --git a/e2e/SharpSite.E2E/SharpSite.E2E.csproj b/e2e/SharpSite.E2E/SharpSite.E2E.csproj index 82597c7..a40275c 100644 --- a/e2e/SharpSite.E2E/SharpSite.E2E.csproj +++ b/e2e/SharpSite.E2E/SharpSite.E2E.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/SharpSite.Web/Components/Admin/PostList.razor b/src/SharpSite.Web/Components/Admin/PostList.razor index 5ceef2a..bbaf7b0 100644 --- a/src/SharpSite.Web/Components/Admin/PostList.razor +++ b/src/SharpSite.Web/Components/Admin/PostList.razor @@ -1,6 +1,7 @@ @attribute [Route(RouteValues.AdminPostList)] @attribute [Authorize()] @using Microsoft.AspNetCore.Components.QuickGrid +@rendermode InteractiveServer @inject IPostRepository PostService @@ -27,6 +28,11 @@ else + + + } @@ -39,4 +45,12 @@ else { Posts = await PostService.GetPosts(); } + + async Task DeletePost(Post post) + { + await PostService.DeletePost(post.Slug); + Posts = await PostService.GetPosts(); + } + + } \ No newline at end of file From afa040962c63c8ae7e062292f5d71c948bb10bb5 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 20 Feb 2025 12:45:15 -0500 Subject: [PATCH 05/15] Update README.md - spelling & formatting --- e2e/SharpSite.E2E/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/SharpSite.E2E/README.md b/e2e/SharpSite.E2E/README.md index 6cdfdb3..b84e715 100644 --- a/e2e/SharpSite.E2E/README.md +++ b/e2e/SharpSite.E2E/README.md @@ -6,6 +6,6 @@ This is the first end-to-end testing project for SharpSite. It uses xUnit and P There are three main folders in use inside this project to enable different C# capabilities that we need in order to execute tests using playwright the folders are -- Abstractions containsthe resources that we reuse across multiple tests -- Fixtures contains the test classes -- Navigation contains extra classes that help with navigating the website +- **Abstractions** contains the resources that we reuse across multiple tests +- **Fixtures** contains the test classes +- **Navigation** contains extra classes that help with navigating the website From 534d753579d7b9cd9c9f993d29c94ad91efa6bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tero=20Kilpel=C3=A4inen?= <48437506+degenone@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:44:47 +0200 Subject: [PATCH 06/15] Block duplicate plugin uploads (#310) Fix FritzAndFriends/SharpSite#141 --- .../Components/Admin/AddPlugin.razor | 1 + .../Locales/SharedResource.Designer.cs | 9 ++++++++ .../Locales/SharedResource.bg.resx | 21 ++++++++++-------- .../Locales/SharedResource.ca.resx | 21 ++++++++++-------- .../Locales/SharedResource.de.resx | 21 ++++++++++-------- .../Locales/SharedResource.en.resx | 17 ++++++++------ .../Locales/SharedResource.es.resx | 21 ++++++++++-------- .../Locales/SharedResource.fi.resx | 11 ++++++---- .../Locales/SharedResource.fr.resx | 21 ++++++++++-------- .../Locales/SharedResource.it.resx | 21 ++++++++++-------- .../Locales/SharedResource.nl.resx | 21 ++++++++++-------- .../Locales/SharedResource.pt.resx | 21 ++++++++++-------- src/SharpSite.Web/Locales/SharedResource.resx | 4 ++++ .../Locales/SharedResource.sv.resx | 21 ++++++++++-------- .../Locales/SharedResource.sw.resx | 21 ++++++++++-------- src/SharpSite.Web/PluginManager.cs | 22 +++++++++++++++---- 16 files changed, 169 insertions(+), 105 deletions(-) diff --git a/src/SharpSite.Web/Components/Admin/AddPlugin.razor b/src/SharpSite.Web/Components/Admin/AddPlugin.razor index f4c0e66..1b9638a 100644 --- a/src/SharpSite.Web/Components/Admin/AddPlugin.razor +++ b/src/SharpSite.Web/Components/Admin/AddPlugin.razor @@ -66,6 +66,7 @@ catch (Exception ex) { Logger.LogError($"{ex.Message}"); + PluginManager.CleanupCurrentUploadedPlugin(); ErrorMessage = ex.Message; } diff --git a/src/SharpSite.Web/Locales/SharedResource.Designer.cs b/src/SharpSite.Web/Locales/SharedResource.Designer.cs index 071aca9..8c9bd7e 100644 --- a/src/SharpSite.Web/Locales/SharedResource.Designer.cs +++ b/src/SharpSite.Web/Locales/SharedResource.Designer.cs @@ -492,6 +492,15 @@ internal static string sharpsite_plugin_description { } } + /// + /// Looks up a localized string similar to Plugin '{0}' is already installed.. + /// + internal static string sharpsite_plugin_exists { + get { + return ResourceManager.GetString("sharpsite_plugin_exists", resourceCulture); + } + } + /// /// Looks up a localized string similar to Plugin File. /// diff --git a/src/SharpSite.Web/Locales/SharedResource.bg.resx b/src/SharpSite.Web/Locales/SharedResource.bg.resx index c4d8c4e..be1bf39 100644 --- a/src/SharpSite.Web/Locales/SharedResource.bg.resx +++ b/src/SharpSite.Web/Locales/SharedResource.bg.resx @@ -351,15 +351,6 @@ Файлът вече съдържа следното: AI generated translation - - Персонализиране на съдържанието на страницата "Не е намерена" - - - Персонализирайте съдържанието за страницата "страницата не е намерена" - - - Промени Темата - Език AI generated translation @@ -368,4 +359,16 @@ Това гарантира, че помощните технологии използват правилния език за съдържанието. AI generated translation + + Персонализиране на съдържанието на страницата "Не е намерена" + + + Персонализирайте съдържанието за страницата "страницата не е намерена" + + + Промени Темата + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.ca.resx b/src/SharpSite.Web/Locales/SharedResource.ca.resx index 6843489..3f22483 100644 --- a/src/SharpSite.Web/Locales/SharedResource.ca.resx +++ b/src/SharpSite.Web/Locales/SharedResource.ca.resx @@ -347,15 +347,6 @@ El fitxer ja conté el següent: AI generated translation - - Personalitza el contingut de Pàgina no trobada. - - - Personalitzeu el contingut per a la pàgina "pàgina no trobada". - - - Canvia el tema - Idioma AI generated translation @@ -364,4 +355,16 @@ Això garanteix que les tecnologies d'assistència utilitzin el llenguatge correcte per al contingut. AI generated translation + + Personalitza el contingut de Pàgina no trobada. + + + Personalitzeu el contingut per a la pàgina "pàgina no trobada". + + + Canvia el tema + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.de.resx b/src/SharpSite.Web/Locales/SharedResource.de.resx index 197b7ba..350268e 100644 --- a/src/SharpSite.Web/Locales/SharedResource.de.resx +++ b/src/SharpSite.Web/Locales/SharedResource.de.resx @@ -347,15 +347,6 @@ Die Datei enthält bereits Folgendes: AI generated translation - - Individualisiere Inhalte für die Seite "Nicht gefunden" - - - Passen Sie den Inhalt für die Seite "Seite nicht gefunden" an. - - - Thema ändern - Sprache AI generated translation @@ -364,4 +355,16 @@ Dies gewährleistet, dass assistive Technologien die richtige Sprache für den Inhalt verwenden. AI generated translation + + Individualisiere Inhalte für die Seite "Nicht gefunden" + + + Passen Sie den Inhalt für die Seite "Seite nicht gefunden" an. + + + Thema ändern + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.en.resx b/src/SharpSite.Web/Locales/SharedResource.en.resx index 13b64d8..257e618 100644 --- a/src/SharpSite.Web/Locales/SharedResource.en.resx +++ b/src/SharpSite.Web/Locales/SharedResource.en.resx @@ -323,6 +323,14 @@ The file already contains the following: + + Language + AI generated translation + + + This ensures assistive technologies use the correct language for the content. + AI generated translation + Customize Page Not Found content @@ -333,12 +341,7 @@ Change Theme Text of the button used to change the theme of the website - - Language - AI generated translation - - - This ensures assistive technologies use the correct language for the content. - AI generated translation + + Plugin '{0}' is already installed. \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.es.resx b/src/SharpSite.Web/Locales/SharedResource.es.resx index 5841512..b89e866 100644 --- a/src/SharpSite.Web/Locales/SharedResource.es.resx +++ b/src/SharpSite.Web/Locales/SharedResource.es.resx @@ -347,15 +347,6 @@ El archivo ya contiene lo siguiente: AI generated translation - - Personalizar el contenido de la página no encontrada. - - - Personalizar el contenido para la página de "página no encontrada". - - - Cambiar Tema - Idioma AI generated translation @@ -364,4 +355,16 @@ Esto asegura que las tecnologías de asistencia utilicen el idioma correcto para el contenido. AI generated translation + + Personalizar el contenido de la página no encontrada. + + + Personalizar el contenido para la página de "página no encontrada". + + + Cambiar Tema + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.fi.resx b/src/SharpSite.Web/Locales/SharedResource.fi.resx index 7b9d001..a7feeef 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fi.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fi.resx @@ -329,12 +329,15 @@ Tämä varmistaa, että avustavat teknologiat käyttävät sisällölle oikeaa kieltä. - Mukauta Sivua ei löytynyt -sisältöä + Mukauta Sivua ei löytynyt -sisältöä - Mukauta sisältö "sivua ei löydy" -sivulle. + Mukauta sisältö "sivua ei löydy" -sivulle. - - Vaihda teemaa + + Vaihda teemaa + + + Laajennus '{0}' on jo asennettu. \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.fr.resx b/src/SharpSite.Web/Locales/SharedResource.fr.resx index 5550702..6535b33 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fr.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fr.resx @@ -347,15 +347,6 @@ Le fichier contient déjà ce qui suit: AI generated translation - - Personnaliser le contenu de la page introuvable - - - Personnaliser le contenu de la page "page non trouvée". - - - Changer de thème - Langue AI generated translation @@ -364,4 +355,16 @@ Cela garantit que les technologies d'assistance utilisent la langue correcte pour le contenu. AI generated translation + + Personnaliser le contenu de la page introuvable + + + Personnaliser le contenu de la page "page non trouvée". + + + Changer de thème + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.it.resx b/src/SharpSite.Web/Locales/SharedResource.it.resx index a2ffa8c..a56a9ef 100644 --- a/src/SharpSite.Web/Locales/SharedResource.it.resx +++ b/src/SharpSite.Web/Locales/SharedResource.it.resx @@ -378,15 +378,6 @@ Il file contiene già quanto segue: AI generated translation - - Personalizza il contenuto della pagina non trovata. - - - Personalizza il contenuto per la pagina "pagina non trovata". - - - Cambia tema - Lingua AI generated translation @@ -395,4 +386,16 @@ Questo garantisce che le tecnologie assistive utilizzino la lingua corretta per il contenuto. AI generated translation + + Personalizza il contenuto della pagina non trovata. + + + Personalizza il contenuto per la pagina "pagina non trovata". + + + Cambia tema + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.nl.resx b/src/SharpSite.Web/Locales/SharedResource.nl.resx index e6e5a9b..d2a5667 100644 --- a/src/SharpSite.Web/Locales/SharedResource.nl.resx +++ b/src/SharpSite.Web/Locales/SharedResource.nl.resx @@ -347,15 +347,6 @@ Het bestand bevat al het volgende: AI generated translation - - Aanpassen van Pagina Niet Gevonden inhoud. - - - Pas de inhoud aan voor de "pagina niet gevonden" pagina. - - - Verander thema - Taal AI generated translation @@ -364,4 +355,16 @@ Dit zorgt ervoor dat hulpmiddelen voor toegankelijkheid de juiste taal gebruiken voor de inhoud. AI generated translation + + Aanpassen van Pagina Niet Gevonden inhoud. + + + Pas de inhoud aan voor de "pagina niet gevonden" pagina. + + + Verander thema + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.pt.resx b/src/SharpSite.Web/Locales/SharedResource.pt.resx index d83bc57..0c78e9a 100644 --- a/src/SharpSite.Web/Locales/SharedResource.pt.resx +++ b/src/SharpSite.Web/Locales/SharedResource.pt.resx @@ -347,15 +347,6 @@ O arquivo já contém o seguinte: AI generated translation - - Personalizar o conteúdo da página não encontrada. - - - Personalize o conteúdo para a página "página não encontrada" - - - Alterar Tema - Idioma AI generated translation @@ -364,4 +355,16 @@ Isso garante que as tecnologias assistivas usem o idioma correto para o conteúdo. AI generated translation + + Personalizar o conteúdo da página não encontrada. + + + Personalize o conteúdo para a página "página não encontrada" + + + Alterar Tema + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.resx b/src/SharpSite.Web/Locales/SharedResource.resx index 53b5013..7dd792f 100644 --- a/src/SharpSite.Web/Locales/SharedResource.resx +++ b/src/SharpSite.Web/Locales/SharedResource.resx @@ -359,4 +359,8 @@ Change Theme Text of the button used to change the theme of the website + + Plugin '{0}' is already installed. + Error message to be desplayed when a plugin that already exists, is attempted to be uploaded. + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.sv.resx b/src/SharpSite.Web/Locales/SharedResource.sv.resx index abf062a..4d6776b 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sv.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sv.resx @@ -381,6 +381,14 @@ Filen innehåller redan följande: AI generated translation + + Språk + AI generated translation + + + Detta säkerställer att hjälpmedelstekniker använder rätt språk för innehållet. + AI generated translation + Anpassa innehållet för 'Sidan kan inte hittas' redigeringskomponenten AI generated translation @@ -389,15 +397,10 @@ Anpassa innehållet för "sida hittades inte"-sidan. AI generated translation - - Byt tema + + Byt tema - - Språk - AI generated translation - - - Detta säkerställer att hjälpmedelstekniker använder rätt språk för innehållet. - AI generated translation + + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.sw.resx b/src/SharpSite.Web/Locales/SharedResource.sw.resx index 9669529..b091809 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sw.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sw.resx @@ -347,15 +347,6 @@ Faili tayari lina yafuatayo: AI generated translation - - Sawazisha Yaliyopatikana Ukurasa wa Yaliyopatikana maudhui kwa SharpSite ni mfumo wa usimamizi wa yaliyomo wa chanzo wazi uliojengwa na C# na Blazor. - - - Mbadilishe maudhui ya ukurasa wa "ukurasa haujapatikana" kulingana na mahitaji yako. - - - Badili Mandhari - Lugha AI generated translation @@ -364,4 +355,16 @@ Hii hufanya teknolojia za msaada kutumia lugha sahihi kwa maudhui. AI generated translation + + Sawazisha Yaliyopatikana Ukurasa wa Yaliyopatikana maudhui kwa SharpSite ni mfumo wa usimamizi wa yaliyomo wa chanzo wazi uliojengwa na C# na Blazor. + + + Mbadilishe maudhui ya ukurasa wa "ukurasa haujapatikana" kulingana na mahitaji yako. + + + Badili Mandhari + + + + \ No newline at end of file diff --git a/src/SharpSite.Web/PluginManager.cs b/src/SharpSite.Web/PluginManager.cs index 044d8cd..2ee68c3 100644 --- a/src/SharpSite.Web/PluginManager.cs +++ b/src/SharpSite.Web/PluginManager.cs @@ -50,6 +50,7 @@ public void HandleUploadedPlugin(Plugin plugin) Manifest = ReadManifest(manifestStream); Manifest.ValidateManifest(logger, plugin); + EnsurePluginNotInstalled(Manifest, logger); // Add your logic to process the manifest content here logger.LogInformation("Plugin {PluginName} uploaded and manifest processed.", Manifest); @@ -242,14 +243,14 @@ private async Task RegisterWithServiceLocator(PluginAssembly pluginAssembly) ZipArchive archive; var pluginFolder = Directory.CreateDirectory(Path.Combine("plugins", "_uploaded")); - var filePath = Path.Combine(pluginFolder.FullName, $"{pluginManifest!.Id}@{pluginManifest.Version}.sspkg"); + var filePath = Path.Combine(pluginFolder.FullName, $"{pluginManifest.IdVersionToString()}.sspkg"); using var pluginAssemblyFileStream = File.OpenWrite(filePath); await pluginAssemblyFileStream.WriteAsync(plugin.Bytes); logger.LogInformation("Plugin saved to {FilePath}", filePath); // Create a folder named after the plugin name under /plugins - pluginLibFolder = Directory.CreateDirectory(Path.Combine("plugins", $"{pluginManifest!.Id}@{pluginManifest.Version}")); + pluginLibFolder = Directory.CreateDirectory(Path.Combine("plugins", pluginManifest.IdVersionToString())); using var pluginMemoryStream = new MemoryStream(plugin.Bytes); archive = new ZipArchive(pluginMemoryStream, ZipArchiveMode.Read, true); @@ -260,7 +261,7 @@ private async Task RegisterWithServiceLocator(PluginAssembly pluginAssembly) if (hasWebContent) { - pluginWwwRootFolder = Directory.CreateDirectory(Path.Combine("plugins", "_wwwroot", $"{pluginManifest!.Id}@{pluginManifest.Version}")); + pluginWwwRootFolder = Directory.CreateDirectory(Path.Combine("plugins", "_wwwroot", pluginManifest.IdVersionToString())); } foreach (var entry in archive.Entries) @@ -371,7 +372,7 @@ public DirectoryInfo GetDirectoryInPluginsFolder(string name) private static readonly char[] _InvalidChars = Path.GetInvalidPathChars(); private static readonly string[] _InvalidPathSegments = ["~", "..", "/", "\\"]; - private static readonly string[] _ReservedNames = [ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" ]; + private static readonly string[] _ReservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"]; private static bool IsValidDirectory(string name) { @@ -415,4 +416,17 @@ private static bool IsValidDirectory(string name) return true; } + + private static void EnsurePluginNotInstalled(PluginManifest? manifest, ILogger logger) + { + + if (manifest is not null && Directory.Exists(Path.Combine("plugins", manifest.IdVersionToString()))) + { + var errMsg = string.Format(Locales.SharedResource.sharpsite_plugin_exists, manifest.IdVersionToString()); + PluginException ex = new(errMsg); + logger.LogError(ex, "Plugin '{Plugin}' is already installed.", manifest.IdVersionToString()); + throw ex; + } + + } } From d3f73edc56f57b7452a0d6f461c0f5c46a794f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tero=20Kilpel=C3=A4inen?= <48437506+degenone@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:56:58 +0200 Subject: [PATCH 07/15] Add a confirmation if script tag is in post/page (#311) Co-authored-by: Jeffrey T. Fritz fix FritzAndFriends/SharpSite#219 --- .../Components/Admin/ConfirmSaveButton.razor | 26 ++++++++++ .../Components/Admin/EditPage.razor | 17 ++++++- .../Components/Admin/EditPost.razor | 27 ++++++----- .../Locales/SharedResource.Designer.cs | 36 ++++++++++++++ .../Locales/SharedResource.bg.resx | 12 +++++ .../Locales/SharedResource.ca.resx | 12 +++++ .../Locales/SharedResource.de.resx | 12 +++++ .../Locales/SharedResource.en.resx | 12 +++++ .../Locales/SharedResource.es.resx | 12 +++++ .../Locales/SharedResource.fi.resx | 12 +++++ .../Locales/SharedResource.fr.resx | 12 +++++ .../Locales/SharedResource.it.resx | 12 +++++ .../Locales/SharedResource.nl.resx | 12 +++++ .../Locales/SharedResource.pt.resx | 12 +++++ src/SharpSite.Web/Locales/SharedResource.resx | 16 +++++++ .../Locales/SharedResource.sv.resx | 12 +++++ .../Locales/SharedResource.sw.resx | 12 +++++ src/SharpSite.Web/MarkdownHelper.cs | 33 +++++++++++++ .../MarkdownHelper/ContainsScriptTag.cs | 48 +++++++++++++++++++ 19 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 src/SharpSite.Web/Components/Admin/ConfirmSaveButton.razor create mode 100644 src/SharpSite.Web/MarkdownHelper.cs create mode 100644 tests/SharpSite.Tests.Web/MarkdownHelper/ContainsScriptTag.cs diff --git a/src/SharpSite.Web/Components/Admin/ConfirmSaveButton.razor b/src/SharpSite.Web/Components/Admin/ConfirmSaveButton.razor new file mode 100644 index 0000000..f850a2c --- /dev/null +++ b/src/SharpSite.Web/Components/Admin/ConfirmSaveButton.razor @@ -0,0 +1,26 @@ +
+ @if (!ConfirmationRequired) + { + + } + else + { + + + + } +
+ +@code { + [Parameter, EditorRequired] + public required string AlertMessage { get; set; } + + [Parameter, EditorRequired] + public required bool ConfirmationRequired { get; set; } + + [Parameter, EditorRequired] + public required EventCallback SaveCallback { get; set; } + + [Parameter, EditorRequired] + public required EventCallback CancelCallback { get; set; } +} diff --git a/src/SharpSite.Web/Components/Admin/EditPage.razor b/src/SharpSite.Web/Components/Admin/EditPage.razor index 55ecc0d..408312f 100644 --- a/src/SharpSite.Web/Components/Admin/EditPage.razor +++ b/src/SharpSite.Web/Components/Admin/EditPage.razor @@ -17,10 +17,14 @@

- +
- + } @code { @@ -31,6 +35,8 @@ private string ThisPageTitle = string.Empty; + private bool _ConfirmationRequired = false; + protected override async Task OnInitializedAsync() { if (Id != 0) @@ -54,6 +60,12 @@ private async Task SavePage() { + if (!_ConfirmationRequired && MarkdownHelper.ContainsScriptTag(Page!.Content)) + { + _ConfirmationRequired = true; + return; + } + if (Id == 0) { // format and set the slug based on the title @@ -67,5 +79,6 @@ await PageRepository.UpdatePage(Page!); } NavManager.NavigateTo("/admin/Pages"); + } } diff --git a/src/SharpSite.Web/Components/Admin/EditPost.razor b/src/SharpSite.Web/Components/Admin/EditPost.razor index f87f27a..8045f1d 100644 --- a/src/SharpSite.Web/Components/Admin/EditPost.razor +++ b/src/SharpSite.Web/Components/Admin/EditPost.razor @@ -35,14 +35,16 @@
- +
-
- -
+ } @code { @@ -50,6 +52,7 @@ [Parameter] public int? UrlDate { get; set; } private Post? Post { get; set; } + private bool _ConfirmationRequired = false; protected override async Task OnInitializedAsync() { @@ -66,25 +69,23 @@ private async Task SavePost() { - Console.WriteLine("Save Post"); + if (!_ConfirmationRequired && MarkdownHelper.ContainsScriptTag(Post!.Content)) + { + _ConfirmationRequired = true; + return; + } if (string.IsNullOrEmpty(Post!.Slug)) { Post.Slug = Post.GetSlug(Post.Title); - Console.WriteLine(Post.Slug); await PostService.AddPost(Post); - - // flush the outputcache for the sitemap and rss - await FlushCache(); - - NavManager.NavigateTo("/admin/posts"); } else { await PostService.UpdatePost(Post); - await FlushCache(); - NavManager.NavigateTo("/admin/posts"); } + await FlushCache(); + NavManager.NavigateTo("/admin/posts"); } diff --git a/src/SharpSite.Web/Locales/SharedResource.Designer.cs b/src/SharpSite.Web/Locales/SharedResource.Designer.cs index 8c9bd7e..e33f31a 100644 --- a/src/SharpSite.Web/Locales/SharedResource.Designer.cs +++ b/src/SharpSite.Web/Locales/SharedResource.Designer.cs @@ -150,6 +150,15 @@ internal static string sharpsite_backtohome { } } + /// + /// Looks up a localized string similar to Cancel. + /// + internal static string sharpsite_cancel { + get { + return ResourceManager.GetString("sharpsite_cancel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change Theme. /// @@ -159,6 +168,15 @@ internal static string sharpsite_ChangeTheme { } } + /// + /// Looks up a localized string similar to Confirm. + /// + internal static string sharpsite_confirm { + get { + return ResourceManager.GetString("sharpsite_confirm", resourceCulture); + } + } + /// /// Looks up a localized string similar to Customize the content for the "page not found" page. /// @@ -636,6 +654,24 @@ internal static string sharpsite_save { } } + /// + /// Looks up a localized string similar to The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed?. + /// + internal static string sharpsite_script_alert_page { + get { + return ResourceManager.GetString("sharpsite_script_alert_page", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The markdown contains a script tag which will be executed once users load the post. Are you sure you want to proceed?. + /// + internal static string sharpsite_script_alert_post { + get { + return ResourceManager.GetString("sharpsite_script_alert_post", resourceCulture); + } + } + /// /// Looks up a localized string similar to Site appearance. /// diff --git a/src/SharpSite.Web/Locales/SharedResource.bg.resx b/src/SharpSite.Web/Locales/SharedResource.bg.resx index be1bf39..ad0f4a0 100644 --- a/src/SharpSite.Web/Locales/SharedResource.bg.resx +++ b/src/SharpSite.Web/Locales/SharedResource.bg.resx @@ -368,6 +368,18 @@ Промени Темата + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.ca.resx b/src/SharpSite.Web/Locales/SharedResource.ca.resx index 3f22483..8dc5692 100644 --- a/src/SharpSite.Web/Locales/SharedResource.ca.resx +++ b/src/SharpSite.Web/Locales/SharedResource.ca.resx @@ -364,6 +364,18 @@ Canvia el tema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.de.resx b/src/SharpSite.Web/Locales/SharedResource.de.resx index 350268e..3d284d5 100644 --- a/src/SharpSite.Web/Locales/SharedResource.de.resx +++ b/src/SharpSite.Web/Locales/SharedResource.de.resx @@ -364,6 +364,18 @@ Thema ändern + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.en.resx b/src/SharpSite.Web/Locales/SharedResource.en.resx index 257e618..2de6cec 100644 --- a/src/SharpSite.Web/Locales/SharedResource.en.resx +++ b/src/SharpSite.Web/Locales/SharedResource.en.resx @@ -341,6 +341,18 @@ Change Theme Text of the button used to change the theme of the website
+ + The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed? + + + The markdown contains a script tag which will be executed once users load the post. Are you sure you want to proceed? + + + Confirm + + + Cancel + Plugin '{0}' is already installed. diff --git a/src/SharpSite.Web/Locales/SharedResource.es.resx b/src/SharpSite.Web/Locales/SharedResource.es.resx index b89e866..1129339 100644 --- a/src/SharpSite.Web/Locales/SharedResource.es.resx +++ b/src/SharpSite.Web/Locales/SharedResource.es.resx @@ -364,6 +364,18 @@ Cambiar Tema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.fi.resx b/src/SharpSite.Web/Locales/SharedResource.fi.resx index a7feeef..c4c67f2 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fi.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fi.resx @@ -337,6 +337,18 @@ Vaihda teemaa + + Markdown sisältää script -tagin, joka ajetaan, kun käyttäjät lataavat sivun. Oletko varma, että haluat jatkaa? + + + Markdown sisältää script -tagin, joka ajetaan, kun käyttäjät lataavat postauksen. Oletko varma, että haluat jatkaa? + + + Vahvista + + + Peruuta + Laajennus '{0}' on jo asennettu. diff --git a/src/SharpSite.Web/Locales/SharedResource.fr.resx b/src/SharpSite.Web/Locales/SharedResource.fr.resx index 6535b33..6d587ee 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fr.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fr.resx @@ -364,6 +364,18 @@ Changer de thème + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.it.resx b/src/SharpSite.Web/Locales/SharedResource.it.resx index a56a9ef..60898f6 100644 --- a/src/SharpSite.Web/Locales/SharedResource.it.resx +++ b/src/SharpSite.Web/Locales/SharedResource.it.resx @@ -395,6 +395,18 @@ Cambia tema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.nl.resx b/src/SharpSite.Web/Locales/SharedResource.nl.resx index d2a5667..2865d10 100644 --- a/src/SharpSite.Web/Locales/SharedResource.nl.resx +++ b/src/SharpSite.Web/Locales/SharedResource.nl.resx @@ -364,6 +364,18 @@ Verander thema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.pt.resx b/src/SharpSite.Web/Locales/SharedResource.pt.resx index 0c78e9a..0681114 100644 --- a/src/SharpSite.Web/Locales/SharedResource.pt.resx +++ b/src/SharpSite.Web/Locales/SharedResource.pt.resx @@ -364,6 +364,18 @@ Alterar Tema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.resx b/src/SharpSite.Web/Locales/SharedResource.resx index 7dd792f..7071d03 100644 --- a/src/SharpSite.Web/Locales/SharedResource.resx +++ b/src/SharpSite.Web/Locales/SharedResource.resx @@ -359,6 +359,22 @@ Change Theme Text of the button used to change the theme of the website + + The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed? + Alert message that is showed when the markdown content for a page contains a script tag + + + The markdown contains a script tag which will be executed once users load the post. Are you sure you want to proceed? + Alert message that is showed when the markdown content for a post contains a script tag + + + Confirm + To confirm an action + + + Cancel + To cancel an action + Plugin '{0}' is already installed. Error message to be desplayed when a plugin that already exists, is attempted to be uploaded. diff --git a/src/SharpSite.Web/Locales/SharedResource.sv.resx b/src/SharpSite.Web/Locales/SharedResource.sv.resx index 4d6776b..12c7761 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sv.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sv.resx @@ -400,6 +400,18 @@ Byt tema + + + + + + + + + + + + diff --git a/src/SharpSite.Web/Locales/SharedResource.sw.resx b/src/SharpSite.Web/Locales/SharedResource.sw.resx index b091809..5d7d750 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sw.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sw.resx @@ -364,6 +364,18 @@ Badili Mandhari + + + + + + + + + + + + diff --git a/src/SharpSite.Web/MarkdownHelper.cs b/src/SharpSite.Web/MarkdownHelper.cs new file mode 100644 index 0000000..4d5d596 --- /dev/null +++ b/src/SharpSite.Web/MarkdownHelper.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace SharpSite.Web; + +public static partial class MarkdownHelper +{ + /// + /// Checks if the markdown contains script tags outside of inline code or code blocks. + /// + /// The markdown to check. + /// + public static bool ContainsScriptTag(string markdown) + { + + markdown = CodeBlockRegex().Replace(markdown, string.Empty); + markdown = InlineCodeRegex().Replace(markdown, string.Empty); + + bool containsOpeningScriptTag = ScriptTagOpeningRegex().IsMatch(markdown); + bool containsClosingScriptTag = markdown.Contains("", StringComparison.OrdinalIgnoreCase); + return containsOpeningScriptTag && containsClosingScriptTag; + + } + + [GeneratedRegex(@"]*>", RegexOptions.IgnoreCase)] + private static partial Regex ScriptTagOpeningRegex(); + + [GeneratedRegex(@"```[\s\S]*?```")] + private static partial Regex CodeBlockRegex(); + + [GeneratedRegex(@"`[^`]*`")] + private static partial Regex InlineCodeRegex(); +} + diff --git a/tests/SharpSite.Tests.Web/MarkdownHelper/ContainsScriptTag.cs b/tests/SharpSite.Tests.Web/MarkdownHelper/ContainsScriptTag.cs new file mode 100644 index 0000000..026e0f0 --- /dev/null +++ b/tests/SharpSite.Tests.Web/MarkdownHelper/ContainsScriptTag.cs @@ -0,0 +1,48 @@ +using Xunit; + +namespace SharpSite.Tests.Web.MarkdownHelper; + +public class ContainsScriptTag +{ + + [Theory] + [InlineData("")] + [InlineData("")] + [InlineData("")] + [InlineData("")] + [InlineData("Text before text after")] + [InlineData("
")] + [InlineData("")] + [InlineData("")] + [InlineData("Text\n\nMore text")] + public void WithValidScriptTagsReturnsTrue(string markdown) + { + Assert.True(SharpSite.Web.MarkdownHelper.ContainsScriptTag(markdown)); + } + + [Theory] + [InlineData("```html\n\n```")] + [InlineData("`const script = ''`")] + [InlineData("not a script tag")] + [InlineData("")] + [InlineData("```js\nlet script = document.createElement('script');\n```")] + [InlineData("`\n\n```")] + [InlineData("not a real script tag")] + public void WithInvalidOrEscapedScriptTagsReturnsFalse(string markdown) + { + Assert.False(SharpSite.Web.MarkdownHelper.ContainsScriptTag(markdown)); + } + + [Theory] + [InlineData("# Just Markdown")] + [InlineData("Just text")] + [InlineData("## Script Documentation\nThis is about scripts")] + [InlineData("*italic* **bold** [link](https://test.com)")] + public void WithJustValidMarkdownReturnsFalse(string markdown) + { + Assert.False(SharpSite.Web.MarkdownHelper.ContainsScriptTag(markdown)); + } + +} From 2a05eb5184668c405c456e75da6f648a87794352 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 27 Feb 2025 10:43:30 -0500 Subject: [PATCH 08/15] Updated to latest .NET stuff (#315) --- Directory.Packages.props | 56 +++++++++++++------------- e2e/SharpSite.E2E/SharpSite.E2E.csproj | 10 ++++- global.json | 2 +- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 82d0d84..f1373d3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,38 +5,38 @@ true - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - + + + + + - + - + \ No newline at end of file diff --git a/e2e/SharpSite.E2E/SharpSite.E2E.csproj b/e2e/SharpSite.E2E/SharpSite.E2E.csproj index a40275c..1618665 100644 --- a/e2e/SharpSite.E2E/SharpSite.E2E.csproj +++ b/e2e/SharpSite.E2E/SharpSite.E2E.csproj @@ -8,11 +8,17 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/global.json b/global.json index c26c7d8..0eb36b2 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "9.0.200", "allowPrerelease": true, "rollForward": "minor" } From 28c2eb6b0ee24503a5b962c1e176bea2015793ac Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 27 Feb 2025 10:43:46 -0500 Subject: [PATCH 09/15] Added a BlazorClick navigation tool for Playwright testing (#314) --- e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs | 14 ++++++++++++-- src/SharpSite.Web/ApplicatonState.cs | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs b/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs index 9ece800..c5e4e73 100644 --- a/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs +++ b/e2e/SharpSite.E2E/Fixtures/CreatePostTests.cs @@ -21,8 +21,8 @@ public async Task CreatePost() await Page.GetByPlaceholder("Title").FillAsync(PostTitle); await Page.GetByRole(AriaRole.Application).GetByRole(AriaRole.Textbox).FillAsync("This is a test"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync(); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).BlazorClickAsync(); + // await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = PostTitle, Exact = true })).ToBeVisibleAsync(); @@ -69,3 +69,13 @@ public async Task CreatePostWithDateInPast() } } + + +public static class Extensions { + + public static async Task BlazorClickAsync(this ILocator locator) { + await locator.ClickAsync(); + await locator.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + } + +} \ No newline at end of file diff --git a/src/SharpSite.Web/ApplicatonState.cs b/src/SharpSite.Web/ApplicatonState.cs index 74294e5..170ead0 100644 --- a/src/SharpSite.Web/ApplicatonState.cs +++ b/src/SharpSite.Web/ApplicatonState.cs @@ -18,6 +18,8 @@ public class ApplicationState public record CurrentThemeRecord(string IdVersion); + + public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCultures); [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] From 4ec8909269a8d03ffac45634a11f2ab297e5f867 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 6 Mar 2025 12:19:22 -0500 Subject: [PATCH 10/15] Admin theme and initial out-of-the-box experience (#305) * Placed public website files in CSS, img, and JS folders * Initial coloring and theme for site admin * Added the ability to set the site name * First steps in creating startup wizard * Added ability to inject AppState at first config time * Adding website config collection fixure * Completed initial page * Completed initial upload of site logo * Added more typesafe routes --- .github/copilot-instructions.md | 1 + .github/workflows/dotnet-build.yml | 2 - SharpSite.sln | 1 + .../Abstractions/SharpSitePageTest.cs | 1 + e2e/SharpSite.E2E/ManageXUnitTestSuite.cs | 55 ---------- .../WebsiteConfigurationFixture.cs | 98 ++++++++++++++++++ .../ApplicationStateModel.cs | 31 ++++++ .../SharpSite.Abstractions.csproj | 1 + src/SharpSite.AppHost/PostgresExtensions.cs | 1 + src/SharpSite.Web/ApplicatonState.cs | 30 ++---- .../Components/Admin/AdminLayout.razor | 17 --- .../Components/Admin/AdminSiteSettings.razor | 16 ++- .../Components/Admin/ManageNavMenu.razor | 21 +++- .../Components/Admin/PageList.razor | 2 +- .../Components/Admin/PluginCard.razor | 2 +- .../Components/Admin/_Imports.razor | 2 +- src/SharpSite.Web/Components/App.razor | 4 +- .../Components/Layout/AdminLayout.razor | 22 ++++ .../Components/Layout/NavMenu.razor | 5 +- .../Components/Layout/StartupLayout.razor | 11 ++ .../Components/Layout/StartupLayout.razor.css | 14 +++ .../Components/Pages/About.razor | 2 +- .../Components/SeoHeaderTags.razor | 3 +- .../Components/Startup/Step1.razor | 51 +++++++++ .../Components/Startup/Step2.razor | 72 +++++++++++++ .../Components/Startup/_Imports.razor | 1 + src/SharpSite.Web/FileApi.cs | 6 +- .../Locales/SharedResource.Designer.cs | 18 ++++ .../Locales/SharedResource.bg.resx | 8 ++ .../Locales/SharedResource.ca.resx | 8 ++ .../Locales/SharedResource.de.resx | 8 ++ .../Locales/SharedResource.en.resx | 8 ++ .../Locales/SharedResource.es.resx | 8 ++ .../Locales/SharedResource.fi.resx | 8 ++ .../Locales/SharedResource.fr.resx | 8 ++ .../Locales/SharedResource.it.resx | 8 ++ .../Locales/SharedResource.nl.resx | 8 ++ .../Locales/SharedResource.pt.resx | 8 ++ src/SharpSite.Web/Locales/SharedResource.resx | 8 ++ .../Locales/SharedResource.sv.resx | 8 ++ .../Locales/SharedResource.sw.resx | 8 ++ src/SharpSite.Web/Program.cs | 2 + src/SharpSite.Web/RouteValues.cs | 3 + src/SharpSite.Web/StartupConfigMiddleware.cs | 59 +++++++++++ src/SharpSite.Web/wwwroot/css/admin.css | 33 ++++++ src/SharpSite.Web/wwwroot/{ => css}/app.css | 0 src/SharpSite.Web/wwwroot/img/logo-500.webp | Bin 0 -> 6080 bytes src/SharpSite.Web/wwwroot/{ => img}/logo.webp | Bin .../wwwroot/{ => img}/plugin-icon.svg | 0 src/SharpSite.Web/wwwroot/{ => js}/app.js | 0 50 files changed, 583 insertions(+), 108 deletions(-) create mode 100644 .github/copilot-instructions.md delete mode 100644 e2e/SharpSite.E2E/ManageXUnitTestSuite.cs create mode 100644 e2e/SharpSite.E2E/WebsiteConfigurationFixture.cs create mode 100644 src/SharpSite.Abstractions/ApplicationStateModel.cs delete mode 100644 src/SharpSite.Web/Components/Admin/AdminLayout.razor create mode 100644 src/SharpSite.Web/Components/Layout/AdminLayout.razor create mode 100644 src/SharpSite.Web/Components/Layout/StartupLayout.razor create mode 100644 src/SharpSite.Web/Components/Layout/StartupLayout.razor.css create mode 100644 src/SharpSite.Web/Components/Startup/Step1.razor create mode 100644 src/SharpSite.Web/Components/Startup/Step2.razor create mode 100644 src/SharpSite.Web/Components/Startup/_Imports.razor create mode 100644 src/SharpSite.Web/StartupConfigMiddleware.cs create mode 100644 src/SharpSite.Web/wwwroot/css/admin.css rename src/SharpSite.Web/wwwroot/{ => css}/app.css (100%) create mode 100644 src/SharpSite.Web/wwwroot/img/logo-500.webp rename src/SharpSite.Web/wwwroot/{ => img}/logo.webp (100%) rename src/SharpSite.Web/wwwroot/{ => img}/plugin-icon.svg (100%) rename src/SharpSite.Web/wwwroot/{ => js}/app.js (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f7fc68e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +Always use System.text.json for working with JSON markup \ No newline at end of file diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 8be1113..99c6b9c 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -50,7 +50,6 @@ jobs: - name: Set badge color shell: bash - if: always() run: | case ${{ fromJSON( steps.test-results.outputs.json ).conclusion }} in success) @@ -64,7 +63,6 @@ jobs: ;; esac - name: Create badge - if: always() uses: emibcn/badge-action@808173dd03e2f30c980d03ee49e181626088eee8 with: label: Unit Tests diff --git a/SharpSite.sln b/SharpSite.sln index 51726a9..7ed7ff9 100644 --- a/SharpSite.sln +++ b/SharpSite.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0. Solution Items", "0. Sol ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore + .github\copilot-instructions.md = .github\copilot-instructions.md Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props nuget.config = nuget.config diff --git a/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs b/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs index 5c2165b..75785de 100644 --- a/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs +++ b/e2e/SharpSite.E2E/Abstractions/SharpSitePageTest.cs @@ -3,6 +3,7 @@ namespace SharpSite.E2E.Abstractions; +[Collection(WebsiteConfigurationFixtureCollection.TEST_COLLECTION_NAME)] public abstract class SharpSitePageTest : PageTest { diff --git a/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs b/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs deleted file mode 100644 index 9c0af5b..0000000 --- a/e2e/SharpSite.E2E/ManageXUnitTestSuite.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.Playwright; - -namespace SharpSite.E2E; - -public class ManageXUnitTestSuite //: IAsyncLifetime -{ - - - private const string URL_LOGIN = "/Account/Login"; - private const string LOGIN_USERID = "admin@Localhost"; - private const string LOGIN_PASSWORD = "Admin123!"; - - public Task DisposeAsync() - { - if (File.Exists(".auth.json")) File.Delete(".auth.json"); - - return Task.CompletedTask; - } - - public async Task InitializeAsync() - { - if (File.Exists(".auth.json")) File.Delete(".auth.json"); - - using var playwright = await Playwright.CreateAsync(); - await using var browser = await playwright.Chromium.LaunchAsync(); - var context = await browser.NewContextAsync(new BrowserNewContextOptions() - { - ColorScheme = ColorScheme.Light, - Locale = "en-US", - ViewportSize = new() - { - // set the viewport to 1024x768 - Width = 1024, - Height = 768, - }, - BaseURL = "http://localhost:5020" - }); - // create a new page - var page = await context.NewPageAsync(); - await page.GotoAsync(URL_LOGIN); - await page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); - await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Email" }) - .FillAsync(LOGIN_USERID); - await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Password" }) - .FillAsync(LOGIN_PASSWORD); - await page.GetByRole(AriaRole.Button, new() { Name = "loginbutton" }).ClickAsync(); - await context.StorageStateAsync(new() - { - Path = ".auth.json" - }); - - } -} - - diff --git a/e2e/SharpSite.E2E/WebsiteConfigurationFixture.cs b/e2e/SharpSite.E2E/WebsiteConfigurationFixture.cs new file mode 100644 index 0000000..d587542 --- /dev/null +++ b/e2e/SharpSite.E2E/WebsiteConfigurationFixture.cs @@ -0,0 +1,98 @@ +using Microsoft.Playwright; +using SharpSite.Abstractions; +using System.Net.Http.Json; + +namespace SharpSite.E2E; + +[CollectionDefinition(TEST_COLLECTION_NAME)] +public class WebsiteConfigurationFixtureCollection : ICollectionFixture +{ + public const string TEST_COLLECTION_NAME = "Website collection"; + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} + + +public class WebsiteConfigurationFixture +{ + + + private const string URL_LOGIN = "/Account/Login"; + private const string LOGIN_USERID = "admin@Localhost"; + private const string LOGIN_PASSWORD = "Admin123!"; + + + public WebsiteConfigurationFixture() + { + + //using var playwright = await Playwright.CreateAsync(); + //await using var browser = await playwright.Chromium.LaunchAsync(); + //var context = await browser.NewContextAsync(new BrowserNewContextOptions() + //{ + // ColorScheme = ColorScheme.Light, + // Locale = "en-US", + // ViewportSize = new() + // { + // // set the viewport to 1024x768 + // Width = 1024, + // Height = 768, + // }, + // BaseURL = "http://localhost:5020" + //}); + + //await CreateAuthTicket(context); + + ConfigureSharpsiteAsExistingWebsite().GetAwaiter().GetResult(); + + } + + private async Task ConfigureSharpsiteAsExistingWebsite() + { + + // create an applicationState object and POST it to ./startapi + var appState = new ApplicationStateModel() + { + SiteName = "My Playwright Test Site", + MaximumUploadSizeMB = 10, + //CurrentTheme = "SharpSite.Web.DefaultTheme", + RobotsTxtCustomContent = "User-agent: *\nDisallow: /", + PageNotFoundContent = "

Page not found

", + StartupCompleted = true, + + }; + + // post AppState to the /startapi endpoint using an http client + var client = new HttpClient(); + client.BaseAddress = new Uri("http://localhost:5020"); + var response = await client.PostAsJsonAsync("/startapi", appState); + response.EnsureSuccessStatusCode(); + + + } + + private static async Task CreateAuthTicket(IBrowserContext context) + { + if (File.Exists(".auth.json")) File.Delete(".auth.json"); + // create a new page + var page = await context.NewPageAsync(); + await page.GotoAsync(URL_LOGIN); + await page.GetByRole(AriaRole.Link, new() { Name = "Login" }).ClickAsync(); + await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Email" }) + .FillAsync(LOGIN_USERID); + await page.GetByRole(AriaRole.Textbox, new() { Name = "Input.Password" }) + .FillAsync(LOGIN_PASSWORD); + await page.GetByRole(AriaRole.Button, new() { Name = "loginbutton" }).ClickAsync(); + await context.StorageStateAsync(new() + { + Path = ".auth.json" + }); + } + + public void Dispose() + { + throw new NotImplementedException(); + } +} + + diff --git a/src/SharpSite.Abstractions/ApplicationStateModel.cs b/src/SharpSite.Abstractions/ApplicationStateModel.cs new file mode 100644 index 0000000..7d33bb6 --- /dev/null +++ b/src/SharpSite.Abstractions/ApplicationStateModel.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace SharpSite.Abstractions; + +public class ApplicationStateModel +{ + + /// + /// Indicates whether the application state has been initialized from the applicationState.json file. + /// + [JsonIgnore] + public bool Initialized { get; protected set; } = false; + + public bool StartupCompleted { get; set; } = false; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? RobotsTxtCustomContent { get; set; } + + public string SiteName { get; set; } = "SharpSite"; + + + /// + /// Maximum file upload size in megabytes. + /// + public long MaximumUploadSizeMB { get; set; } = 10; // 10MB + + public string PageNotFoundContent { get; set; } = string.Empty; + + + +} diff --git a/src/SharpSite.Abstractions/SharpSite.Abstractions.csproj b/src/SharpSite.Abstractions/SharpSite.Abstractions.csproj index b8d268e..0935168 100644 --- a/src/SharpSite.Abstractions/SharpSite.Abstractions.csproj +++ b/src/SharpSite.Abstractions/SharpSite.Abstractions.csproj @@ -9,6 +9,7 @@ + diff --git a/src/SharpSite.AppHost/PostgresExtensions.cs b/src/SharpSite.AppHost/PostgresExtensions.cs index f2d34f8..4e48fc3 100644 --- a/src/SharpSite.AppHost/PostgresExtensions.cs +++ b/src/SharpSite.AppHost/PostgresExtensions.cs @@ -29,6 +29,7 @@ public static { config.WithImageTag(VERSIONS.PGADMIN); config.WithLifetime(ContainerLifetime.Persistent); + config.WithParentRelationship(dbServer); }); } diff --git a/src/SharpSite.Web/ApplicatonState.cs b/src/SharpSite.Web/ApplicatonState.cs index 170ead0..c4f0a0f 100644 --- a/src/SharpSite.Web/ApplicatonState.cs +++ b/src/SharpSite.Web/ApplicatonState.cs @@ -1,21 +1,15 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using SharpSite.Abstractions; using SharpSite.Abstractions.Base; using SharpSite.Abstractions.Theme; using SharpSite.Plugins; namespace SharpSite.Web; -public class ApplicationState +public class ApplicationState : ApplicationStateModel { - - /// - /// Indicates whether the application state has been initialized from the applicationState.json file. - /// - [JsonIgnore] - public bool Initialized { get; private set; } = false; - public record CurrentThemeRecord(string IdVersion); @@ -25,22 +19,17 @@ public record LocalizationRecord(string? DefaultCulture, string[]? SupportedCult [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CurrentThemeRecord? CurrentTheme { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public LocalizationRecord? Localization { get; set; } + public string HasCustomLogo { get; set; } = string.Empty; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string? RobotsTxtCustomContent { get; set; } + public LocalizationRecord? Localization { get; set; } public Dictionary ConfigurationSections { get; private set; } = new(); public event Func? ConfigurationSectionChanged; - /// - /// Maximum file upload size in megabytes. - /// - public long MaximumUploadSizeMB { get; set; } = 10; // 10MB - - public string PageNotFoundContent { get; set; } = string.Empty; + [JsonIgnore] + public int StartupStep { get; set; } = 0; [JsonIgnore] public Type? ThemeType @@ -118,10 +107,13 @@ public async Task Load(IServiceProvider services, Func? getApplicationSt { ConfigurationSections = state.ConfigurationSections; CurrentTheme = state.CurrentTheme; - MaximumUploadSizeMB = state.MaximumUploadSizeMB; + HasCustomLogo = state.HasCustomLogo; Localization = state.Localization; - RobotsTxtCustomContent = state.RobotsTxtCustomContent; + MaximumUploadSizeMB = state.MaximumUploadSizeMB; PageNotFoundContent = state.PageNotFoundContent; + RobotsTxtCustomContent = state.RobotsTxtCustomContent; + SiteName = state.SiteName; + StartupCompleted = state.StartupCompleted; } Initialized = true; diff --git a/src/SharpSite.Web/Components/Admin/AdminLayout.razor b/src/SharpSite.Web/Components/Admin/AdminLayout.razor deleted file mode 100644 index b246acf..0000000 --- a/src/SharpSite.Web/Components/Admin/AdminLayout.razor +++ /dev/null @@ -1,17 +0,0 @@ -@inherits LayoutComponentBase -@layout SharpSite.Web.Components.Layout.MainLayout - -

@Localizer[SharedResource.sharpsite_admin_layout_h1]

- -
-

@Localizer[SharedResource.sharpsite_admin_layout_h2]

-
-
- -
- @Body -
-
-
\ No newline at end of file diff --git a/src/SharpSite.Web/Components/Admin/AdminSiteSettings.razor b/src/SharpSite.Web/Components/Admin/AdminSiteSettings.razor index d50d5af..d101e42 100644 --- a/src/SharpSite.Web/Components/Admin/AdminSiteSettings.razor +++ b/src/SharpSite.Web/Components/Admin/AdminSiteSettings.razor @@ -17,6 +17,13 @@
+ + +

+ +

+ + @@ -82,11 +89,12 @@ @code { - private ViewModel Model = new(); + private ViewModel Model = new() { SiteName = "Sharpsite" }; protected override void OnInitialized() { + Model.SiteName = ApplicationState.SiteName; Model.MaxSizeMB = ApplicationState.MaximumUploadSizeMB; Model.DefaultCulture = ApplicationState.Localization?.DefaultCulture ?? "en"; Model.SupportedCultures = ApplicationState.Localization?.SupportedCultures; @@ -103,6 +111,8 @@ .MaximumReceiveMessageSize = 1024 * 1024 * Model.MaxSizeMB; ApplicationState.MaximumUploadSizeMB = Model.MaxSizeMB; + ApplicationState.SiteName = Model.SiteName; + await ApplicationState.Save(); } @@ -145,6 +155,10 @@ public class ViewModel { + + [Required, MaxLength(50)] + public required string SiteName { get; set; } + [Range(1, 100), Required] public long MaxSizeMB { get; set; } diff --git a/src/SharpSite.Web/Components/Admin/ManageNavMenu.razor b/src/SharpSite.Web/Components/Admin/ManageNavMenu.razor index 73bc05c..4255692 100644 --- a/src/SharpSite.Web/Components/Admin/ManageNavMenu.razor +++ b/src/SharpSite.Web/Components/Admin/ManageNavMenu.razor @@ -1,4 +1,4 @@ -@inject ApplicationState AppState + @inject ApplicationState AppState diff --git a/src/SharpSite.Web/Components/Admin/PageList.razor b/src/SharpSite.Web/Components/Admin/PageList.razor index 9718926..25933e3 100644 --- a/src/SharpSite.Web/Components/Admin/PageList.razor +++ b/src/SharpSite.Web/Components/Admin/PageList.razor @@ -1,4 +1,4 @@ -@page "/admin/Pages" +@attribute [Route(RouteValues.AdminPageList)] @attribute [Authorize(Roles = Constants.Roles.AdminUsers)] @inject IPageRepository PageRepository @inject NavigationManager NavManager diff --git a/src/SharpSite.Web/Components/Admin/PluginCard.razor b/src/SharpSite.Web/Components/Admin/PluginCard.razor index 033b8f0..eb55b51 100644 --- a/src/SharpSite.Web/Components/Admin/PluginCard.razor +++ b/src/SharpSite.Web/Components/Admin/PluginCard.razor @@ -19,7 +19,7 @@ @code { - private const string DefaultPluginIcon = "plugin-icon.svg"; + private const string DefaultPluginIcon = "/img/plugin-icon.svg"; [Parameter, EditorRequired] public required PluginManifest Plugin { get; set; } } diff --git a/src/SharpSite.Web/Components/Admin/_Imports.razor b/src/SharpSite.Web/Components/Admin/_Imports.razor index f6c6b4b..33adb45 100644 --- a/src/SharpSite.Web/Components/Admin/_Imports.razor +++ b/src/SharpSite.Web/Components/Admin/_Imports.razor @@ -1 +1 @@ -@layout AdminLayout \ No newline at end of file +@layout Layout.AdminLayout \ No newline at end of file diff --git a/src/SharpSite.Web/Components/App.razor b/src/SharpSite.Web/Components/App.razor index 9034471..969ea77 100644 --- a/src/SharpSite.Web/Components/App.razor +++ b/src/SharpSite.Web/Components/App.razor @@ -7,7 +7,7 @@ - + @@ -24,7 +24,7 @@ - + diff --git a/src/SharpSite.Web/Components/Layout/AdminLayout.razor b/src/SharpSite.Web/Components/Layout/AdminLayout.razor new file mode 100644 index 0000000..7408543 --- /dev/null +++ b/src/SharpSite.Web/Components/Layout/AdminLayout.razor @@ -0,0 +1,22 @@ +@using SharpSite.Web.Components.Layout +@inherits LayoutComponentBase +@* @layout SharpSite.Web.Components.Layout.MainLayout *@ + + + + + +
+

@Localizer[SharedResource.sharpsite_admin_layout_h2]

+
+
+ +
+ @Body +
+
+
\ No newline at end of file diff --git a/src/SharpSite.Web/Components/Layout/NavMenu.razor b/src/SharpSite.Web/Components/Layout/NavMenu.razor index 5dc4faa..1f66d79 100644 --- a/src/SharpSite.Web/Components/Layout/NavMenu.razor +++ b/src/SharpSite.Web/Components/Layout/NavMenu.razor @@ -1,5 +1,6 @@ @using System.Security.Claims @implements IDisposable +@inject ApplicationState AppState @inject NavigationManager NavigationManager @inject IPageRepository PageRepository @inject AuthenticationStateProvider AuthZ @@ -7,7 +8,7 @@ @@ -89,6 +90,8 @@ private HttpContext HttpContext { get; set; } = default!; private SharpSiteUser user = default!; + private string Logo => string.IsNullOrEmpty(AppState.HasCustomLogo) ? "/img/logo.webp" : Path.Combine(RouteValues.BaseFileApi,"/",AppState.HasCustomLogo); + protected override async Task OnInitializedAsync() { Pages = await PageRepository.GetPages(); diff --git a/src/SharpSite.Web/Components/Layout/StartupLayout.razor b/src/SharpSite.Web/Components/Layout/StartupLayout.razor new file mode 100644 index 0000000..991ad39 --- /dev/null +++ b/src/SharpSite.Web/Components/Layout/StartupLayout.razor @@ -0,0 +1,11 @@ +@inherits LayoutComponentBase + + + +
+
+ @Body +
+
diff --git a/src/SharpSite.Web/Components/Layout/StartupLayout.razor.css b/src/SharpSite.Web/Components/Layout/StartupLayout.razor.css new file mode 100644 index 0000000..2393576 --- /dev/null +++ b/src/SharpSite.Web/Components/Layout/StartupLayout.razor.css @@ -0,0 +1,14 @@ +.centered-content { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; +} + + .centered-content > div { + max-width: 700px; + align-items: center; + display: flex; + flex-direction: column; + } \ No newline at end of file diff --git a/src/SharpSite.Web/Components/Pages/About.razor b/src/SharpSite.Web/Components/Pages/About.razor index 95c3e69..ca84841 100644 --- a/src/SharpSite.Web/Components/Pages/About.razor +++ b/src/SharpSite.Web/Components/Pages/About.razor @@ -1,4 +1,4 @@ -@page "/aboutSharpSite" +@attribute [Route(RouteValues.AboutSharpSite)] @inject IStringLocalizer Localizer @Localizer[SharedResource.sharpsite_about] diff --git a/src/SharpSite.Web/Components/SeoHeaderTags.razor b/src/SharpSite.Web/Components/SeoHeaderTags.razor index a92cbd0..e7860d4 100644 --- a/src/SharpSite.Web/Components/SeoHeaderTags.razor +++ b/src/SharpSite.Web/Components/SeoHeaderTags.razor @@ -1,4 +1,5 @@ @inject NavigationManager NavigationManager +@inject ApplicationState State @* add typical og and social media meta tags for discovery *@ @@ -8,7 +9,7 @@ @* TODO: This should be replaced with a name the Site Admin gives to this site *@ - + @* diff --git a/src/SharpSite.Web/Components/Startup/Step1.razor b/src/SharpSite.Web/Components/Startup/Step1.razor new file mode 100644 index 0000000..f698b17 --- /dev/null +++ b/src/SharpSite.Web/Components/Startup/Step1.razor @@ -0,0 +1,51 @@ +@page "/start/step1" +@inject ApplicationState AppState +@inject NavigationManager NavManager +@rendermode InteractiveServer + +@* add the sharpsite logo *@ +SharpSite + +

Welcome to your new website!

+ +

+ This is SharpSite, a fun and friendly website management tool. + It is designed to be easy to use and to help you create a website that you can be proud of. +

+ +

+ Let's start with some basics: what is the name for your cool new website? +

+ +@* add a simple textbox to collect the website name *@ + +

+ You can change this later, so don't worry if you don't have a name yet. +

+

+ Once you have a name, click the Next button to continue. +

+@* add a button to continue to the next step *@ + + +@code { + private string WebsiteName { get; set; } = string.Empty; + + protected override async Task OnInitializedAsync() + { + if (AppState.StartupCompleted) NavManager.NavigateTo("/", true); + if (AppState.StartupStep > 1) NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false); + + await base.OnInitializedAsync(); + } + + private void SaveAndContinue(MouseEventArgs args) + { + + AppState.SiteName = WebsiteName; + AppState.StartupStep = 2; + + NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false); + + } +} diff --git a/src/SharpSite.Web/Components/Startup/Step2.razor b/src/SharpSite.Web/Components/Startup/Step2.razor new file mode 100644 index 0000000..4582255 --- /dev/null +++ b/src/SharpSite.Web/Components/Startup/Step2.razor @@ -0,0 +1,72 @@ +@page "/start/step2" +@using SharpSite.Abstractions.FileStorage +@inject ApplicationState AppState +@inject NavigationManager NavManager +@inject IHandleFileStorage FileStorage +@rendermode InteractiveServer + +@* add the sharpsite logo *@ +SharpSite + +

Step 2 - Initial Appearance of @AppState.SiteName

+ +

+ Let's next configure some things that are going to help with the initial appearance of your website @AppState.SiteName +

+ +

+ You can change these later, so don't worry if you want to make changes later. +

+ +

+ Let's select a logo for your website. This will be used in the header of your website. +

+ + + +@* Add a div to show the logo that was uploaded *@ +@if (Logo != Stream.Null) +{ +
+ Logo +
+} + +

+ Once you have uploaded a logo, click the Next button to continue. +

+ +@* add a button to continue to the next step *@ + + + +@code { + + Stream Logo { get; set; } = Stream.Null; + + protected override async Task OnInitializedAsync() + { + if (AppState.StartupCompleted) NavManager.NavigateTo("/", true); + if (AppState.StartupStep < 2) NavManager.NavigateTo("/start/step1", false); + if (AppState.StartupStep != 2) NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false); + + await base.OnInitializedAsync(); + } + + private async Task Finish(MouseEventArgs args) + { + + await FileStorage.AddFile(new FileData(Logo, new FileMetaData("logo.png", "image/png", DateTimeOffset.Now))); + AppState.StartupCompleted = true; + AppState.StartupStep = 0; + AppState.HasCustomLogo = "logo.png"; + await AppState.Save(); + NavManager.NavigateTo($"/", false); + } + +} diff --git a/src/SharpSite.Web/Components/Startup/_Imports.razor b/src/SharpSite.Web/Components/Startup/_Imports.razor new file mode 100644 index 0000000..113be90 --- /dev/null +++ b/src/SharpSite.Web/Components/Startup/_Imports.razor @@ -0,0 +1 @@ +@layout Layout.StartupLayout \ No newline at end of file diff --git a/src/SharpSite.Web/FileApi.cs b/src/SharpSite.Web/FileApi.cs index 01c1314..3e4300f 100644 --- a/src/SharpSite.Web/FileApi.cs +++ b/src/SharpSite.Web/FileApi.cs @@ -18,7 +18,7 @@ public static WebApplication MapFileApi(this WebApplication app, PluginManager p // throw new InvalidOperationException("No file storage plugin found"); //} - var filesGroup = app.MapGroup("/api/files"); + var filesGroup = app.MapGroup(RouteValues.BaseFileApi); filesGroup.MapGet("/", async (int page, int filesOnPage) => { @@ -55,7 +55,7 @@ public static WebApplication MapFileApi(this WebApplication app, PluginManager p await fileProvider!.AddFile(file); // generate the base of the URL using HttpContextAccessor to get the host and port - var path = $"{context.Request.Scheme}://{context.Request.Host}/api/files/{file.Metadata.FileName}"; + var path = $"{context.Request.Scheme}://{context.Request.Host}{Path.Combine(RouteValues.BaseFileApi, "/", file.Metadata.FileName)}"; return Results.Ok(path); }).RequireAuthorization(Constants.Roles.AllUsers); @@ -65,7 +65,7 @@ public static WebApplication MapFileApi(this WebApplication app, PluginManager p var fileProvider = pluginManager.GetPluginProvidedService(); await fileProvider!.RemoveFile(path); await fileProvider.AddFile(file); - return Results.Created($"/api/files/{file.Metadata.FileName}", file.Metadata); + return Results.Created($"{Path.Combine(RouteValues.BaseFileApi, "/", file.Metadata.FileName)}", file.Metadata); }).RequireAuthorization(Constants.Roles.AdminUsers); // need to add a DELETE endpoint to remove files that is limited to members of the "Admin" role diff --git a/src/SharpSite.Web/Locales/SharedResource.Designer.cs b/src/SharpSite.Web/Locales/SharedResource.Designer.cs index e33f31a..301c6db 100644 --- a/src/SharpSite.Web/Locales/SharedResource.Designer.cs +++ b/src/SharpSite.Web/Locales/SharedResource.Designer.cs @@ -627,6 +627,15 @@ internal static string sharpsite_remove { } } + /// + /// Looks up a localized string similar to Return to website. + /// + internal static string sharpsite_returntowebsite { + get { + return ResourceManager.GetString("sharpsite_returntowebsite", resourceCulture); + } + } + /// /// Looks up a localized string similar to The file already contains the following:. /// @@ -681,6 +690,15 @@ internal static string sharpsite_site_appearance_admin { } } + /// + /// Looks up a localized string similar to Site Name:. + /// + internal static string sharpsite_sitenamelabel { + get { + return ResourceManager.GetString("sharpsite_sitenamelabel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Site Settings. /// diff --git a/src/SharpSite.Web/Locales/SharedResource.bg.resx b/src/SharpSite.Web/Locales/SharedResource.bg.resx index ad0f4a0..5aa2f80 100644 --- a/src/SharpSite.Web/Locales/SharedResource.bg.resx +++ b/src/SharpSite.Web/Locales/SharedResource.bg.resx @@ -359,6 +359,14 @@ Това гарантира, че помощните технологии използват правилния език за съдържанието. AI generated translation + + Име на сайта: + AI generated translation + + + Върни се на уебсайта + AI generated translation + Персонализиране на съдържанието на страницата "Не е намерена" diff --git a/src/SharpSite.Web/Locales/SharedResource.ca.resx b/src/SharpSite.Web/Locales/SharedResource.ca.resx index 8dc5692..b8f87ff 100644 --- a/src/SharpSite.Web/Locales/SharedResource.ca.resx +++ b/src/SharpSite.Web/Locales/SharedResource.ca.resx @@ -355,6 +355,14 @@ Això garanteix que les tecnologies d'assistència utilitzin el llenguatge correcte per al contingut. AI generated translation + + Nom del lloc: + AI generated translation + + + Tornar al lloc web + AI generated translation + Personalitza el contingut de Pàgina no trobada. diff --git a/src/SharpSite.Web/Locales/SharedResource.de.resx b/src/SharpSite.Web/Locales/SharedResource.de.resx index 3d284d5..2948974 100644 --- a/src/SharpSite.Web/Locales/SharedResource.de.resx +++ b/src/SharpSite.Web/Locales/SharedResource.de.resx @@ -355,6 +355,14 @@ Dies gewährleistet, dass assistive Technologien die richtige Sprache für den Inhalt verwenden. AI generated translation + + Seitenname: + AI generated translation + + + Zurück zur Website + AI generated translation + Individualisiere Inhalte für die Seite "Nicht gefunden" diff --git a/src/SharpSite.Web/Locales/SharedResource.en.resx b/src/SharpSite.Web/Locales/SharedResource.en.resx index 2de6cec..b982929 100644 --- a/src/SharpSite.Web/Locales/SharedResource.en.resx +++ b/src/SharpSite.Web/Locales/SharedResource.en.resx @@ -355,5 +355,13 @@ Plugin '{0}' is already installed. + + + Site Name: + Label on admin pages that allows customization of the website name + + + Return to website + Link text on admin portal that returns the user to the public website \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.es.resx b/src/SharpSite.Web/Locales/SharedResource.es.resx index 1129339..9d01100 100644 --- a/src/SharpSite.Web/Locales/SharedResource.es.resx +++ b/src/SharpSite.Web/Locales/SharedResource.es.resx @@ -355,6 +355,14 @@ Esto asegura que las tecnologías de asistencia utilicen el idioma correcto para el contenido. AI generated translation + + Nombre del sitio: + AI generated translation + + + Volver al sitio web. + AI generated translation + Personalizar el contenido de la página no encontrada. diff --git a/src/SharpSite.Web/Locales/SharedResource.fi.resx b/src/SharpSite.Web/Locales/SharedResource.fi.resx index c4c67f2..faca17f 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fi.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fi.resx @@ -352,4 +352,12 @@ Laajennus '{0}' on jo asennettu. + + Sivuston nimi: + AI generated translation + + + Palaa verkkosivustolle + AI generated translation + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.fr.resx b/src/SharpSite.Web/Locales/SharedResource.fr.resx index 6d587ee..a458fc4 100644 --- a/src/SharpSite.Web/Locales/SharedResource.fr.resx +++ b/src/SharpSite.Web/Locales/SharedResource.fr.resx @@ -355,6 +355,14 @@ Cela garantit que les technologies d'assistance utilisent la langue correcte pour le contenu. AI generated translation + + Nom du site : + AI generated translation + + + Retour au site web + AI generated translation + Personnaliser le contenu de la page introuvable diff --git a/src/SharpSite.Web/Locales/SharedResource.it.resx b/src/SharpSite.Web/Locales/SharedResource.it.resx index 60898f6..4330a5d 100644 --- a/src/SharpSite.Web/Locales/SharedResource.it.resx +++ b/src/SharpSite.Web/Locales/SharedResource.it.resx @@ -386,6 +386,14 @@ Questo garantisce che le tecnologie assistive utilizzino la lingua corretta per il contenuto. AI generated translation + + Nome del sito: + AI generated translation + + + Torna al sito web. + AI generated translation + Personalizza il contenuto della pagina non trovata. diff --git a/src/SharpSite.Web/Locales/SharedResource.nl.resx b/src/SharpSite.Web/Locales/SharedResource.nl.resx index 2865d10..81b16ce 100644 --- a/src/SharpSite.Web/Locales/SharedResource.nl.resx +++ b/src/SharpSite.Web/Locales/SharedResource.nl.resx @@ -355,6 +355,14 @@ Dit zorgt ervoor dat hulpmiddelen voor toegankelijkheid de juiste taal gebruiken voor de inhoud. AI generated translation + + Website Naam: + AI generated translation + + + Terug naar website + AI generated translation + Aanpassen van Pagina Niet Gevonden inhoud. diff --git a/src/SharpSite.Web/Locales/SharedResource.pt.resx b/src/SharpSite.Web/Locales/SharedResource.pt.resx index 0681114..8301ad4 100644 --- a/src/SharpSite.Web/Locales/SharedResource.pt.resx +++ b/src/SharpSite.Web/Locales/SharedResource.pt.resx @@ -355,6 +355,14 @@ Isso garante que as tecnologias assistivas usem o idioma correto para o conteúdo. AI generated translation + + Nome do Site: + AI generated translation + + + Voltar ao site + AI generated translation + Personalizar o conteúdo da página não encontrada. diff --git a/src/SharpSite.Web/Locales/SharedResource.resx b/src/SharpSite.Web/Locales/SharedResource.resx index 7071d03..0e299a7 100644 --- a/src/SharpSite.Web/Locales/SharedResource.resx +++ b/src/SharpSite.Web/Locales/SharedResource.resx @@ -359,6 +359,14 @@ Change Theme Text of the button used to change the theme of the website + + Site Name: + Label on admin pages that allows customization of the website name + + + Return to website + Link text on admin portal that returns the user to the public website + The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed? Alert message that is showed when the markdown content for a page contains a script tag diff --git a/src/SharpSite.Web/Locales/SharedResource.sv.resx b/src/SharpSite.Web/Locales/SharedResource.sv.resx index 12c7761..49aa8fb 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sv.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sv.resx @@ -415,4 +415,12 @@ + + Webbplatsnamn: + AI generated translation + + + Återgå till webbplatsen + AI generated translation + \ No newline at end of file diff --git a/src/SharpSite.Web/Locales/SharedResource.sw.resx b/src/SharpSite.Web/Locales/SharedResource.sw.resx index 5d7d750..8dd3acf 100644 --- a/src/SharpSite.Web/Locales/SharedResource.sw.resx +++ b/src/SharpSite.Web/Locales/SharedResource.sw.resx @@ -355,6 +355,14 @@ Hii hufanya teknolojia za msaada kutumia lugha sahihi kwa maudhui. AI generated translation + + Jina la Tovuti: + AI generated translation + + + Rudi kwenye tovuti + AI generated translation + Sawazisha Yaliyopatikana Ukurasa wa Yaliyopatikana maudhui kwa SharpSite ni mfumo wa usimamizi wa yaliyomo wa chanzo wazi uliojengwa na C# na Blazor. diff --git a/src/SharpSite.Web/Program.cs b/src/SharpSite.Web/Program.cs index 6ca6366..2ec21d0 100644 --- a/src/SharpSite.Web/Program.cs +++ b/src/SharpSite.Web/Program.cs @@ -90,4 +90,6 @@ app.MapFileApi(pluginManager); +app.UseMiddleware(); + app.Run(); diff --git a/src/SharpSite.Web/RouteValues.cs b/src/SharpSite.Web/RouteValues.cs index 79dbe9b..492b9a9 100644 --- a/src/SharpSite.Web/RouteValues.cs +++ b/src/SharpSite.Web/RouteValues.cs @@ -1,6 +1,9 @@ public static class RouteValues { + public const string AboutSharpSite = "/aboutSharpSite"; public const string AdminPostList = "/admin/posts"; + public const string AdminPageList = "/admin/pages"; + public const string BaseFileApi = "/api/files"; } public record struct RouteValue(string Value, Func? Formatter) diff --git a/src/SharpSite.Web/StartupConfigMiddleware.cs b/src/SharpSite.Web/StartupConfigMiddleware.cs new file mode 100644 index 0000000..b849061 --- /dev/null +++ b/src/SharpSite.Web/StartupConfigMiddleware.cs @@ -0,0 +1,59 @@ +using SharpSite.Abstractions; +using SharpSite.Web; +using System.Text.Json; + +public class StartupConfigMiddleware(RequestDelegate next, ApplicationState AppState) +{ + + public async Task Invoke(HttpContext context) + { + + // Exit now if the app is already configured + if (!AppState.StartupCompleted && + !context.Request.Path.Value!.StartsWith("/start") && + !context.Request.Path.Value!.StartsWith("/_blazor") && + !context.Request.Path.Value.EndsWith(".js") && + !context.Request.Path.Value.EndsWith(".css") && + !context.Request.Path.Value.Contains("/img/")) + { + Console.WriteLine("Redirecting for first start"); + context.Response.Redirect("/start/step1"); + } + else if (context.Request.Path.Value!.StartsWith("/startapi") && context.Request.Method == "POST") + { + + if (AppState.StartupCompleted) + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return; + } + + var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); + var state = JsonSerializer.Deserialize(body, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + if (state is not null) + { + //AppState.ConfigurationSections = state.ConfigurationSections; + //AppState.CurrentTheme = state.CurrentTheme; + AppState.MaximumUploadSizeMB = state.MaximumUploadSizeMB; + //AppState.Localization = state.Localization; + AppState.PageNotFoundContent = state.PageNotFoundContent; + AppState.RobotsTxtCustomContent = state.RobotsTxtCustomContent; + AppState.SiteName = state.SiteName; + AppState.StartupCompleted = true; + //await AppState.Save(); + + } + + context.Response.StatusCode = StatusCodes.Status200OK; + return; + + } + + await next(context); + + } + +} diff --git a/src/SharpSite.Web/wwwroot/css/admin.css b/src/SharpSite.Web/wwwroot/css/admin.css new file mode 100644 index 0000000..c20589f --- /dev/null +++ b/src/SharpSite.Web/wwwroot/css/admin.css @@ -0,0 +1,33 @@ +:root { + --primary: steelblue; +} + +.navbar { + background-color: var(--primary); +} + +.nav-pills .nav-link.active { + background-color: var(--primary) !important; +} + + +.jumbotron { + background-color: var(--primary); + color: white; +} + +.btn-primary { + background-color: var(--primary); + border-color: var(--primary); +} + + .btn-primary:hover { + background-color: darkblue; + border-color: darkblue; + } + +.footer { + background-color: var(--primary); + color: white; + padding: 20px 0; +} diff --git a/src/SharpSite.Web/wwwroot/app.css b/src/SharpSite.Web/wwwroot/css/app.css similarity index 100% rename from src/SharpSite.Web/wwwroot/app.css rename to src/SharpSite.Web/wwwroot/css/app.css diff --git a/src/SharpSite.Web/wwwroot/img/logo-500.webp b/src/SharpSite.Web/wwwroot/img/logo-500.webp new file mode 100644 index 0000000000000000000000000000000000000000..0610e21f6e9e49f0e2d57cbe53fb04db11631513 GIT binary patch literal 6080 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 cL!SAcKzi Date: Tue, 11 Mar 2025 10:10:12 -0400 Subject: [PATCH 11/15] updated git describe to get most recent tag --- .github/workflows/dotnet-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 99c6b9c..b366e03 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -90,10 +90,10 @@ jobs: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Get current tag + - name: Get most recent tag id: get-tag run: | - TAG=$(git describe --tags --exact-match 2>/dev/null || echo "") + TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") echo "::set-output name=tag::$TAG" - name: Build Docker image run: | From 272bd713bc613261a840d43049b1b1f86ba57926 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 1 Apr 2025 10:46:41 -0400 Subject: [PATCH 12/15] Now loading defaultplugins on install --- .../Components/Startup/Step1.razor | 5 +++- .../Components/Startup/Step2.razor | 26 ++++++++++++---- src/SharpSite.Web/Components/_Imports.razor | 1 + src/SharpSite.Web/PluginManager.cs | 28 ++++++++++++++++++ src/SharpSite.Web/SharpSite.Web.csproj | 1 + src/SharpSite.Web/StartupConfigMiddleware.cs | 15 +++++++--- .../SharpSite.FileSystemPlugin@0.1.4.sspkg | Bin 0 -> 6674 bytes 7 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 src/SharpSite.Web/defaultplugins/SharpSite.FileSystemPlugin@0.1.4.sspkg diff --git a/src/SharpSite.Web/Components/Startup/Step1.razor b/src/SharpSite.Web/Components/Startup/Step1.razor index f698b17..eb34413 100644 --- a/src/SharpSite.Web/Components/Startup/Step1.razor +++ b/src/SharpSite.Web/Components/Startup/Step1.razor @@ -1,6 +1,7 @@ @page "/start/step1" @inject ApplicationState AppState @inject NavigationManager NavManager +@inject PluginManager PluginManager @rendermode InteractiveServer @* add the sharpsite logo *@ @@ -39,12 +40,14 @@ await base.OnInitializedAsync(); } - private void SaveAndContinue(MouseEventArgs args) + private async Task SaveAndContinue(MouseEventArgs args) { AppState.SiteName = WebsiteName; AppState.StartupStep = 2; + await PluginManager.InstallDefaultPlugins(); + NavManager.NavigateTo($"/start/step{AppState.StartupStep}", false); } diff --git a/src/SharpSite.Web/Components/Startup/Step2.razor b/src/SharpSite.Web/Components/Startup/Step2.razor index 4582255..40ef164 100644 --- a/src/SharpSite.Web/Components/Startup/Step2.razor +++ b/src/SharpSite.Web/Components/Startup/Step2.razor @@ -2,7 +2,7 @@ @using SharpSite.Abstractions.FileStorage @inject ApplicationState AppState @inject NavigationManager NavManager -@inject IHandleFileStorage FileStorage +@inject PluginManager PluginManager @rendermode InteractiveServer @* add the sharpsite logo *@ @@ -38,12 +38,13 @@ Placeholder="Upload a logo for your website" }

- Once you have uploaded a logo, click the Next button to continue. + Once you have uploaded a logo, click the Finish button to continue. If you don't want to upload a logo, just click the Skip and Finish button to continue.

-@* add a button to continue to the next step *@ - - +
+ + +
@code { @@ -61,6 +62,13 @@ Placeholder="Upload a logo for your website" private async Task Finish(MouseEventArgs args) { + var FileStorage = PluginManager.GetPluginProvidedService(); + + if (FileStorage == null) + { + throw new Exception("FileStorage is not available. Please contact support."); + } + await FileStorage.AddFile(new FileData(Logo, new FileMetaData("logo.png", "image/png", DateTimeOffset.Now))); AppState.StartupCompleted = true; AppState.StartupStep = 0; @@ -69,4 +77,12 @@ Placeholder="Upload a logo for your website" NavManager.NavigateTo($"/", false); } + private async Task SkipAndFinish(MouseEventArgs args) + { + AppState.StartupCompleted = true; + AppState.StartupStep = 0; + await AppState.Save(); + NavManager.NavigateTo($"/", false); + } + } diff --git a/src/SharpSite.Web/Components/_Imports.razor b/src/SharpSite.Web/Components/_Imports.razor index ee3cc7d..88cec59 100644 --- a/src/SharpSite.Web/Components/_Imports.razor +++ b/src/SharpSite.Web/Components/_Imports.razor @@ -13,6 +13,7 @@ @using SharpSite.Web @using SharpSite.Web.Components @using SharpSite.Abstractions +@using SharpSite.Abstractions.Base @using System.Globalization @using Microsoft.Extensions.Localization @using SharpSite.Plugins diff --git a/src/SharpSite.Web/PluginManager.cs b/src/SharpSite.Web/PluginManager.cs index 2ee68c3..0b294df 100644 --- a/src/SharpSite.Web/PluginManager.cs +++ b/src/SharpSite.Web/PluginManager.cs @@ -429,4 +429,32 @@ private static void EnsurePluginNotInstalled(PluginManifest? manifest, ILogger l } } + + public async Task InstallDefaultPlugins() + { + + var defaultPluginFolder = new DirectoryInfo("defaultplugins"); + if (!defaultPluginFolder.Exists) return; + + foreach (var file in defaultPluginFolder.GetFiles("*.sspkg")) + { + + using var stream = File.OpenRead(file.FullName); + var plugin = await Plugin.LoadFromStream(stream, file.Name); + + try { + HandleUploadedPlugin(plugin); + logger.LogInformation("Plugin {0} loaded from default plugins.", file.Name); + await SavePlugin(); + } catch (PluginException ex) { + logger.LogError(ex, "Plugin {0} failed to load from default plugins.", file.Name); + } finally { + // Cleanup the plugin after processing + CleanupCurrentUploadedPlugin(); + } + + } + + } + } diff --git a/src/SharpSite.Web/SharpSite.Web.csproj b/src/SharpSite.Web/SharpSite.Web.csproj index e96786b..484c774 100644 --- a/src/SharpSite.Web/SharpSite.Web.csproj +++ b/src/SharpSite.Web/SharpSite.Web.csproj @@ -11,6 +11,7 @@ + diff --git a/src/SharpSite.Web/StartupConfigMiddleware.cs b/src/SharpSite.Web/StartupConfigMiddleware.cs index b849061..4e3235c 100644 --- a/src/SharpSite.Web/StartupConfigMiddleware.cs +++ b/src/SharpSite.Web/StartupConfigMiddleware.cs @@ -8,10 +8,17 @@ public class StartupConfigMiddleware(RequestDelegate next, ApplicationState AppS public async Task Invoke(HttpContext context) { - // Exit now if the app is already configured - if (!AppState.StartupCompleted && - !context.Request.Path.Value!.StartsWith("/start") && - !context.Request.Path.Value!.StartsWith("/_blazor") && + // Check if the application is started and skip the middleware if it is. + if (AppState.StartupCompleted) + { + await next(context); + return; + } + + // Redirect to the start page if the application is not started yet. + if (context.Request.Path.Value is not null && + !context.Request.Path.Value.StartsWith("/start") && + !context.Request.Path.Value.StartsWith("/_blazor") && !context.Request.Path.Value.EndsWith(".js") && !context.Request.Path.Value.EndsWith(".css") && !context.Request.Path.Value.Contains("/img/")) diff --git a/src/SharpSite.Web/defaultplugins/SharpSite.FileSystemPlugin@0.1.4.sspkg b/src/SharpSite.Web/defaultplugins/SharpSite.FileSystemPlugin@0.1.4.sspkg new file mode 100644 index 0000000000000000000000000000000000000000..4ad00a08675dc492c1ec413d4fd1b174247423e9 GIT binary patch literal 6674 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 Wm8L2X Date: Tue, 1 Jul 2025 18:10:37 +0300 Subject: [PATCH 13/15] Feature: Plugin Packer Tool (#338) --- SharpSite.sln | 9 ++ src/SharpSite.PluginPacker/ArgumentParser.cs | 25 +++ src/SharpSite.PluginPacker/ManifestHandler.cs | 40 +++++ .../ManifestPrompter.cs | 69 +++++++++ src/SharpSite.PluginPacker/PluginPackager.cs | 142 ++++++++++++++++++ src/SharpSite.PluginPacker/Program.cs | 45 ++++++ .../SharpSite.PluginPacker.csproj | 14 ++ 7 files changed, 344 insertions(+) create mode 100644 src/SharpSite.PluginPacker/ArgumentParser.cs create mode 100644 src/SharpSite.PluginPacker/ManifestHandler.cs create mode 100644 src/SharpSite.PluginPacker/ManifestPrompter.cs create mode 100644 src/SharpSite.PluginPacker/PluginPackager.cs create mode 100644 src/SharpSite.PluginPacker/Program.cs create mode 100644 src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj diff --git a/SharpSite.sln b/SharpSite.sln index 7ed7ff9..9782609 100644 --- a/SharpSite.sln +++ b/SharpSite.sln @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.E2E", "e2e\SharpS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.Tests.Plugins", "tests\SharpSite.Tests.Plugins\SharpSite.Tests.Plugins.csproj", "{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.PluginPacker", "src\SharpSite.PluginPacker\SharpSite.PluginPacker.csproj", "{677B59E7-C4BA-4024-84D7-78CE6985F3F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Tools", "2. Tools", "{78F974E0-8074-0543-93D5-DC2AAC8BF3DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,6 +130,10 @@ Global {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.Build.0 = Release|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -147,6 +155,7 @@ Global {BA24379C-40D5-5EDF-63BE-CE5BC727E45D} = {3266CA51-9816-4037-9715-701EB6C2928A} {EFCFB571-6B0C-35CD-6664-160CA5B39244} = {8779454A-1F9C-4705-8EE0-5980C6B9C2A5} {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C} = {3266CA51-9816-4037-9715-701EB6C2928A} + {677B59E7-C4BA-4024-84D7-78CE6985F3F5} = {78F974E0-8074-0543-93D5-DC2AAC8BF3DF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {62A15C13-360B-4791-89E9-1FDDFE483970} diff --git a/src/SharpSite.PluginPacker/ArgumentParser.cs b/src/SharpSite.PluginPacker/ArgumentParser.cs new file mode 100644 index 0000000..545f6c3 --- /dev/null +++ b/src/SharpSite.PluginPacker/ArgumentParser.cs @@ -0,0 +1,25 @@ +namespace SharpSite.PluginPacker; + +public static class ArgumentParser +{ + public static (string? inputPath, string? outputPath) ParseArguments(string[] args) + { + string? inputPath = null; + string? outputPath = null; + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-i": + case "--input": + if (i + 1 < args.Length) inputPath = args[++i]; + break; + case "-o": + case "--output": + if (i + 1 < args.Length) outputPath = args[++i]; + break; + } + } + return (inputPath, outputPath); + } +} diff --git a/src/SharpSite.PluginPacker/ManifestHandler.cs b/src/SharpSite.PluginPacker/ManifestHandler.cs new file mode 100644 index 0000000..597dc8a --- /dev/null +++ b/src/SharpSite.PluginPacker/ManifestHandler.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class ManifestHandler +{ + private static readonly JsonSerializerOptions _Opts = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }; + + public static PluginManifest? LoadOrCreateManifest(string inputPath) + { + string manifestPath = Path.Combine(inputPath, "manifest.json"); + PluginManifest? manifest; + if (!File.Exists(manifestPath)) + { + Console.WriteLine($"manifest.json not found in {inputPath}."); + Console.WriteLine("Let's create one interactively."); + manifest = ManifestPrompter.PromptForManifest(); + var json = JsonSerializer.Serialize(manifest, _Opts); + File.WriteAllText(manifestPath, json); + Console.WriteLine($"Created manifest.json at {manifestPath}"); + } + else + { + var json = File.ReadAllText(manifestPath); + manifest = JsonSerializer.Deserialize(json, _Opts); + if (manifest is null) + { + Console.WriteLine("Failed to parse manifest.json"); + return null; + } + } + return manifest; + } +} diff --git a/src/SharpSite.PluginPacker/ManifestPrompter.cs b/src/SharpSite.PluginPacker/ManifestPrompter.cs new file mode 100644 index 0000000..6ae49b4 --- /dev/null +++ b/src/SharpSite.PluginPacker/ManifestPrompter.cs @@ -0,0 +1,69 @@ +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class ManifestPrompter +{ + private static string PromptRequired(string label) + { + string? value; + do + { + Console.Write($"{label}: "); + value = Console.ReadLine()?.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + Console.WriteLine($"{label} is required."); + } + } while (string.IsNullOrWhiteSpace(value)); + return value; + } + + public static PluginManifest PromptForManifest() + { + var id = PromptRequired("Id"); + var displayName = PromptRequired("DisplayName"); + var description = PromptRequired("Description"); + var version = PromptRequired("Version"); + var published = PromptRequired("Published (yyyy-MM-dd)"); + var supportedVersions = PromptRequired("SupportedVersions"); + var author = PromptRequired("Author"); + var contact = PromptRequired("Contact"); + var contactEmail = PromptRequired("ContactEmail"); + var authorWebsite = PromptRequired("AuthorWebsite"); + + // Optional fields + Console.Write("Icon (URL): "); + var icon = (Console.ReadLine() ?? "").Trim(); + Console.Write("Source (repository URL): "); + var source = (Console.ReadLine() ?? "").Trim(); + Console.Write("KnownLicense (e.g. MIT, Apache, LGPL): "); + var knownLicense = (Console.ReadLine() ?? "").Trim(); + Console.Write("Tags (comma separated): "); + var tagsStr = (Console.ReadLine() ?? "").Trim(); + var tags = tagsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + Console.Write("Features (comma separated, e.g. Theme,FileStorage): "); + var featuresStr = (Console.ReadLine() ?? "").Trim(); + var features = featuresStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var featureEnums = features.Length > 0 ? Array.ConvertAll(features, f => Enum.Parse(f, true)) : []; + return new PluginManifest + { + Id = id, + DisplayName = displayName, + Description = description, + Version = version, + Icon = string.IsNullOrWhiteSpace(icon) ? null : icon, + Published = published, + SupportedVersions = supportedVersions, + Author = author, + Contact = contact, + ContactEmail = contactEmail, + AuthorWebsite = authorWebsite, + Source = string.IsNullOrWhiteSpace(source) ? null : source, + KnownLicense = string.IsNullOrWhiteSpace(knownLicense) ? null : knownLicense, + Tags = tags.Length > 0 ? tags : null, + Features = featureEnums + }; + } +} diff --git a/src/SharpSite.PluginPacker/PluginPackager.cs b/src/SharpSite.PluginPacker/PluginPackager.cs new file mode 100644 index 0000000..b08ad36 --- /dev/null +++ b/src/SharpSite.PluginPacker/PluginPackager.cs @@ -0,0 +1,142 @@ +using System.Diagnostics; +using System.IO.Compression; +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class PluginPackager +{ + public static bool PackagePlugin(string inputPath, string outputPath) + { + // Load manifest + var manifest = ManifestHandler.LoadOrCreateManifest(inputPath); + if (manifest is null) + { + Console.WriteLine("Manifest not found or invalid."); + return false; + } + + // Create temp build output folder + string tempBuildDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginBuild_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempBuildDir); + + // Build the project in Release mode to temp build folder + if (!BuildProject(inputPath, tempBuildDir)) + { + Console.WriteLine("Build failed."); + try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { } + return false; + } + + // Create temp folder for packaging + string tempDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginPack_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + // Copy DLL to lib/ and rename + CopyAndRenameDll(inputPath, tempBuildDir, tempDir, manifest); + + // If Theme, copy .css from wwwroot/ to web/ + if (manifest.Features.Contains(PluginFeatures.Theme)) + { + CopyThemeCssFiles(inputPath, tempDir); + } + // Copy manifest.json and other required files + CopyRequiredFiles(inputPath, tempDir); + // Zip tempDir to outputPath - use proper naming convention ID@VERSION.sspkg + // outputPath is always a directory, generate the filename from manifest + string outFile = Path.Combine(outputPath, $"{manifest.IdVersionToString()}.sspkg"); + + // Ensure the output directory exists + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + if (File.Exists(outFile)) File.Delete(outFile); + ZipFile.CreateFromDirectory(tempDir, outFile); + Console.WriteLine($"Plugin packaged successfully: {outFile}"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Packaging failed: {ex.Message}"); + return false; + } + finally + { + // Clean up temp folder + try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } + try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { } + } + } + + private static void CopyAndRenameDll(string inputPath, string tempBuildDir, string tempDir, PluginManifest manifest) + { + string libDir = Path.Combine(tempDir, "lib"); + Directory.CreateDirectory(libDir); + string projectName = new DirectoryInfo(inputPath).Name; + string dllSource = Path.Combine(tempBuildDir, projectName + ".dll"); + string dllTarget = Path.Combine(libDir, manifest.Id + ".dll"); + if (!File.Exists(dllSource)) + { + throw new FileNotFoundException($"DLL not found: {dllSource}"); + } + File.Copy(dllSource, dllTarget, overwrite: true); + } + + private static void CopyThemeCssFiles(string inputPath, string tempDir) + { + string webSrc = Path.Combine(inputPath, "wwwroot"); + string webDst = Path.Combine(tempDir, "web"); + if (Directory.Exists(webSrc)) + { + Directory.CreateDirectory(webDst); + foreach (var css in Directory.GetFiles(webSrc, "*.css", SearchOption.AllDirectories)) + { + string dest = Path.Combine(webDst, Path.GetFileName(css)); + File.Copy(css, dest, overwrite: true); + } + } + } + + private static void CopyRequiredFiles(string inputPath, string tempDir) + { + string[] requiredFiles = ["manifest.json", "LICENSE", "README.md", "Changelog.txt"]; + foreach (var file in requiredFiles) + { + string src = Path.Combine(inputPath, file); + if (File.Exists(src)) + { + File.Copy(src, Path.Combine(tempDir, file), overwrite: true); + } + } + } + + private static bool BuildProject(string inputPath, string outputPath) + { + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build --configuration Release --output \"{outputPath}\"", + WorkingDirectory = inputPath, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using var proc = Process.Start(psi); + if (proc is null) + { + Console.WriteLine("Failed to start build process."); + return false; + } + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + Console.WriteLine(proc.StandardError.ReadToEnd()); + return false; + } + return true; + } +} diff --git a/src/SharpSite.PluginPacker/Program.cs b/src/SharpSite.PluginPacker/Program.cs new file mode 100644 index 0000000..6e6d78d --- /dev/null +++ b/src/SharpSite.PluginPacker/Program.cs @@ -0,0 +1,45 @@ +using SharpSite.PluginPacker; + +(string? inputPath, string? outputPath) = ArgumentParser.ParseArguments(args); + +if (string.IsNullOrWhiteSpace(inputPath)) +{ + Console.WriteLine("Usage: SharpSite.PluginPacker -i [-o ]"); + Console.WriteLine(" -i, --input Input folder containing the plugin project"); + Console.WriteLine(" -o, --output Output directory (optional, defaults to current directory)"); + Console.WriteLine(); + Console.WriteLine("The output filename will be automatically generated as: ID@VERSION.sspkg"); + return 1; +} + +// Default to current directory if no output path specified +outputPath = string.IsNullOrWhiteSpace(outputPath) ? Directory.GetCurrentDirectory() : outputPath; + +if (!Directory.Exists(inputPath)) +{ + Console.WriteLine($"Input directory '{inputPath}' does not exist."); + return 1; +} + +// Validate that output path is a directory, not a file +if (File.Exists(outputPath)) +{ + Console.WriteLine($"Error: Output path '{outputPath}' points to a file. Please specify a directory."); + return 1; +} + +var manifest = ManifestHandler.LoadOrCreateManifest(inputPath); +if (manifest is null) +{ + Console.WriteLine("Failed to load or create manifest."); + return 1; +} +Console.WriteLine($"Loaded manifest for {manifest.DisplayName} ({manifest.Id})"); + +if (!PluginPackager.PackagePlugin(inputPath, outputPath)) +{ + Console.WriteLine("Packaging failed."); + return 1; +} + +return 0; diff --git a/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj b/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj new file mode 100644 index 0000000..6d9085a --- /dev/null +++ b/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj @@ -0,0 +1,14 @@ + + + + + + + + Exe + net9.0 + enable + enable + + + From 03c9aa1d5a1d1f2f3053c6440a6cb7f784b5a3ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:11:58 +0000 Subject: [PATCH 14/15] Initial plan From 41d21ac78df31ae77ad80226c1b7a32e13565f9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:29:58 +0000 Subject: [PATCH 15/15] Add comprehensive plugin author documentation and packaging tools - Created detailed PluginAuthorGuide.md with step-by-step instructions for using SharpSite.PluginPacker - Added QuickStart.md for rapid plugin development - Created package-plugin.sh and package-plugin.ps1 helper scripts - Merged v0.7 branch to access PluginPacker utility - Documentation covers project structure, manifest creation, build process, and examples - Addresses feedback to work off v0.7 branch and document PluginPacker usage Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --- doc/PluginAuthorGuide.md | 392 +++++++++++++++++++++++++++++++++++++ doc/QuickStart.md | 160 +++++++++++++++ scripts/package-plugin.ps1 | 167 ++++++++++++++++ scripts/package-plugin.sh | 171 ++++++++++++++++ 4 files changed, 890 insertions(+) create mode 100644 doc/PluginAuthorGuide.md create mode 100644 doc/QuickStart.md create mode 100644 scripts/package-plugin.ps1 create mode 100755 scripts/package-plugin.sh diff --git a/doc/PluginAuthorGuide.md b/doc/PluginAuthorGuide.md new file mode 100644 index 0000000..81880f8 --- /dev/null +++ b/doc/PluginAuthorGuide.md @@ -0,0 +1,392 @@ +# Plugin Author Guide + +This guide provides comprehensive instructions for creating, building, and packaging plugins for SharpSite using the SharpSite.PluginPacker utility. + +## Table of Contents + +1. [Overview](#overview) +2. [Prerequisites](#prerequisites) +3. [Plugin Project Structure](#plugin-project-structure) +4. [Creating a New Plugin](#creating-a-new-plugin) +5. [Using SharpSite.PluginPacker](#using-sharpsite-pluginpacker) +6. [Manifest.json Configuration](#manifest-json-configuration) +7. [Building and Packaging](#building-and-packaging) +8. [Plugin Types and Examples](#plugin-types-and-examples) +9. [Testing Your Plugin](#testing-your-plugin) +10. [Best Practices](#best-practices) +11. [Troubleshooting](#troubleshooting) + +## Overview + +SharpSite supports a rich ecosystem of plugins that allow administrators to extend the look, feel, and capabilities of a SharpSite application. Plugins are distributed as `.sspkg` files (renamed ZIP files) that contain the compiled plugin library, manifest information, and any required assets. + +The **SharpSite.PluginPacker** utility automates the entire plugin packaging process, from building your project to creating the final `.sspkg` file. + +## Prerequisites + +- .NET 9.0 SDK or later (minimum .NET 8.0 SDK) +- Visual Studio, VS Code, or your preferred C# development environment +- SharpSite source code (to reference abstractions) +- Access to the SharpSite.PluginPacker utility + +**Note**: The v0.7 branch targets .NET 9.0, but plugins can be developed for .NET 8.0 if needed for compatibility. + +## Plugin Project Structure + +A typical plugin project should follow this structure: + +``` +MyPlugin/ +├── MyPlugin.csproj # Project file +├── manifest.json # Plugin metadata (created by PluginPacker if missing) +├── README.md # Plugin documentation +├── LICENSE # License file +├── Changelog.txt # Version history +├── PluginClass.cs # Main plugin implementation +├── wwwroot/ # Web assets (for theme plugins) +│ └── theme.css # CSS files +└── _Imports.razor # Razor imports (if needed) +``` + +## Creating a New Plugin + +### Step 1: Create a New Project + +Create a new .NET class library or Razor class library: + +```bash +dotnet new classlib -n MyAwesomePlugin +cd MyAwesomePlugin +``` + +For theme plugins that include Razor components: +```bash +dotnet new razorclasslib -n MyAwesomeTheme +cd MyAwesomeTheme +``` + +### Step 2: Add Required References + +Add references to the appropriate SharpSite abstractions: + +```xml + + + net9.0 + enable + enable + + + + + + + +``` + +### Step 3: Implement Plugin Interface + +Implement the appropriate interface for your plugin type. For example, a theme plugin: + +```csharp +using SharpSite.Abstractions.Theme; + +namespace MyAwesomeTheme; + +public class MyTheme : IHasStylesheets +{ + public string[] Stylesheets => [ + "theme.css" + ]; +} +``` + +### Step 4: Add Required Files + +Create the following files in your project root: + +- **README.md**: Document your plugin's features and usage +- **LICENSE**: Include your plugin's license +- **Changelog.txt**: Track version changes + +## Using SharpSite.PluginPacker + +The SharpSite.PluginPacker is a command-line utility that automates the plugin packaging process. + +### Building the PluginPacker + +First, build the PluginPacker utility: + +```bash +cd path/to/SharpSite/src/SharpSite.PluginPacker +dotnet build --configuration Release +``` + +### Basic Usage + +```bash +dotnet run --project path/to/SharpSite/src/SharpSite.PluginPacker -- -i [-o ] +``` + +**Parameters:** +- `-i, --input`: Input folder containing the plugin project (required) +- `-o, --output`: Output directory for the .sspkg file (optional, defaults to current directory) + +### Example Usage + +```bash +# Package a plugin project +dotnet run --project ../SharpSite/src/SharpSite.PluginPacker -- -i ./MyAwesomePlugin -o ./dist + +# This will create: ./dist/my.awesome.plugin@1.0.0.sspkg +``` + +### What the PluginPacker Does + +1. **Validates the input directory** exists +2. **Loads or creates manifest.json** interactively if missing +3. **Builds the project** in Release configuration +4. **Creates the package structure**: + - Copies the compiled DLL to `lib/` folder and renames it to match the plugin ID + - For theme plugins: copies CSS files from `wwwroot/` to `web/` folder + - Includes required files: `manifest.json`, `LICENSE`, `README.md`, `Changelog.txt` +5. **Creates the .sspkg file** with naming format: `ID@VERSION.sspkg` + +## Manifest.json Configuration + +The manifest.json file contains metadata about your plugin. If it doesn't exist, the PluginPacker will create one interactively. + +### Required Fields + +```json +{ + "id": "my.awesome.plugin", + "DisplayName": "My Awesome Plugin", + "Description": "A fantastic plugin that does amazing things", + "Version": "1.0.0", + "Published": "2024-12-12", + "SupportedVersions": "0.7.0-0.8.0", + "Author": "Your Name", + "Contact": "Your Name", + "ContactEmail": "you@example.com", + "AuthorWebsite": "https://yourwebsite.com", + "Source": "https://github.com/yourusername/your-plugin", + "KnownLicense": "MIT", + "Tags": ["theme", "blue", "modern"], + "Features": ["Theme"] +} +``` + +### Optional Fields + +- `Icon`: URL to plugin icon +- `Source`: Repository URL +- `KnownLicense`: Standard license identifier (MIT, Apache, LGPL, etc.) +- `Tags`: Array of descriptive tags +- `Features`: Array of plugin features (Theme, FileStorage, etc.) + +### Interactive Manifest Creation + +If no manifest.json exists, the PluginPacker will prompt you for required information: + +``` +Id: my.awesome.plugin +DisplayName: My Awesome Plugin +Description: A fantastic plugin that does amazing things +Version: 1.0.0 +Published (yyyy-MM-dd): 2024-12-12 +SupportedVersions: 0.7.0-0.8.0 +Author: Your Name +Contact: Your Name +ContactEmail: you@example.com +AuthorWebsite: https://yourwebsite.com +Icon (URL): +Source (repository URL): https://github.com/yourusername/your-plugin +KnownLicense (e.g. MIT, Apache, LGPL): MIT +Tags (comma separated): theme, blue, modern +Features (comma separated, e.g. Theme,FileStorage): Theme +``` + +## Building and Packaging + +### Step-by-Step Process + +1. **Prepare your project**: Ensure all required files are present +2. **Run the PluginPacker**: + ```bash + dotnet run --project path/to/SharpSite/src/SharpSite.PluginPacker -- -i ./MyPlugin -o ./dist + ``` +3. **Review the output**: The packager will show progress and create `MyPlugin@1.0.0.sspkg` + +### Package Contents + +The generated .sspkg file contains: + +``` +MyPlugin@1.0.0.sspkg +├── manifest.json # Plugin metadata +├── README.md # Plugin documentation +├── LICENSE # License file +├── Changelog.txt # Version history +├── lib/ +│ └── my.plugin.id.dll # Renamed plugin DLL +└── web/ # Web assets (theme plugins only) + └── theme.css # CSS files +``` + +## Plugin Types and Examples + +### Theme Plugin Example + +**Project Structure:** +``` +MyTheme/ +├── MyTheme.csproj +├── MyTheme.cs +├── _Imports.razor +├── wwwroot/ +│ └── theme.css +├── README.md +├── LICENSE +└── Changelog.txt +``` + +**MyTheme.cs:** +```csharp +using SharpSite.Abstractions.Theme; + +namespace MyTheme; + +public class MyTheme : IHasStylesheets +{ + public string[] Stylesheets => [ + "theme.css" + ]; +} +``` + +**wwwroot/theme.css:** +```css +h1 { + color: #0066cc; + font-family: 'Segoe UI', sans-serif; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} +``` + +### File Storage Plugin Example + +**Project Structure:** +``` +MyFileStorage/ +├── MyFileStorage.csproj +├── FileSystemStorage.cs +├── README.md +├── LICENSE +└── Changelog.txt +``` + +**FileSystemStorage.cs:** +```csharp +using SharpSite.Abstractions.FileStorage; + +namespace MyFileStorage; + +public class FileSystemStorage : IHandleFileStorage +{ + // Implement IHandleFileStorage interface + // ... implementation details +} +``` + +## Testing Your Plugin + +### Local Testing + +1. **Package your plugin** using PluginPacker +2. **Copy the .sspkg file** to your SharpSite instance +3. **Upload via Admin UI** to test installation +4. **Verify functionality** in the SharpSite application + +### Validation Checklist + +- [ ] Plugin compiles successfully +- [ ] Manifest.json is valid and complete +- [ ] Required files are included +- [ ] CSS files are properly copied (theme plugins) +- [ ] Plugin loads without errors +- [ ] Functionality works as expected + +## Best Practices + +### Project Organization + +- **Use meaningful namespaces** that match your plugin ID +- **Follow C# naming conventions** +- **Include comprehensive documentation** +- **Maintain a clear changelog** + +### Manifest Guidelines + +- **Use semantic versioning** (e.g., 1.0.0, 1.2.3-beta1) +- **Specify accurate version ranges** for SharpSite compatibility +- **Include descriptive tags** for discoverability +- **Provide contact information** for support + +### Code Quality + +- **Enable nullable reference types** +- **Use dependency injection** where appropriate +- **Handle errors gracefully** +- **Follow async/await patterns** for I/O operations +- **Include unit tests** for your plugin logic + +### Packaging + +- **Test packaging locally** before distribution +- **Verify file structure** in the generated .sspkg +- **Include all necessary dependencies** +- **Keep packages small** by excluding unnecessary files + +## Troubleshooting + +### Common Issues + +**"DLL not found" Error:** +- Ensure your project builds successfully +- Check that the project name matches the expected DLL name +- Verify the build output directory + +**"Failed to parse manifest.json":** +- Validate JSON syntax using a JSON validator +- Ensure all required fields are present +- Check that feature names match expected values + +**Missing CSS Files:** +- Verify CSS files are in the `wwwroot/` directory +- Ensure your plugin implements `IHasStylesheets` +- Check that the Features array includes "Theme" + +### Debug Mode + +Run the PluginPacker with detailed output to troubleshoot: + +```bash +dotnet run --project path/to/SharpSite/src/SharpSite.PluginPacker -- -i ./MyPlugin -o ./dist --verbose +``` + +### Getting Help + +- Check the [SharpSite documentation](../README.md) +- Review [sample plugins](../../plugins/) for reference +- Report issues on the [SharpSite GitHub repository](https://github.com/FritzAndFriends/SharpSite) + +## Conclusion + +The SharpSite.PluginPacker utility streamlines the plugin development workflow by automating the build, packaging, and validation process. By following this guide, you can create professional, distributable plugins that extend SharpSite's capabilities. + +For more information about plugin architecture, see [PluginArchitecture.md](./PluginArchitecture.md). \ No newline at end of file diff --git a/doc/QuickStart.md b/doc/QuickStart.md new file mode 100644 index 0000000..8140dec --- /dev/null +++ b/doc/QuickStart.md @@ -0,0 +1,160 @@ +# Quick Start: Creating Your First Plugin + +This guide shows you how to create a simple theme plugin from scratch using the SharpSite.PluginPacker. + +## Step 1: Create a New Plugin Project + +```bash +# Create a new directory for your plugin +mkdir MyFirstPlugin +cd MyFirstPlugin + +# Create a new Razor class library project +dotnet new razorclasslib -n MyFirstPlugin --framework net9.0 +cd MyFirstPlugin +``` + +## Step 2: Configure Project References + +Edit `MyFirstPlugin.csproj`: + +```xml + + + net9.0 + enable + enable + + + + + + + + + + + + + + +``` + +## Step 3: Create Plugin Implementation + +Create `MyTheme.cs`: + +```csharp +using SharpSite.Abstractions.Theme; + +namespace MyFirstPlugin; + +public class MyTheme : IHasStylesheets +{ + public string[] Stylesheets => [ + "theme.css" + ]; +} +``` + +## Step 4: Add Theme Styles + +Create `wwwroot/theme.css`: + +```css +/* My First Plugin Theme */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #333; +} + +h1, h2, h3 { + color: #2c3e50; + text-shadow: 1px 1px 2px rgba(0,0,0,0.1); +} + +.container { + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + padding: 20px; + margin: 20px auto; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} +``` + +## Step 5: Add Required Files + +Create `README.md`: + +```markdown +# My First Plugin + +A beautiful gradient theme for SharpSite. + +## Features + +- Modern gradient background +- Clean typography +- Responsive design +- Professional styling + +## Installation + +Upload the .sspkg file through the SharpSite admin interface. +``` + +Create `LICENSE`: + +``` +MIT License + +Copyright (c) 2024 Your Name + +Permission is hereby granted, free of charge, to any person obtaining a copy... +``` + +Create `Changelog.txt`: + +``` +1.0.0 (2024-12-12) +- Initial release +- Beautiful gradient theme +- Responsive design +``` + +## Step 6: Package Your Plugin + +Use the provided packaging script: + +```bash +# From the SharpSite root directory +./scripts/package-plugin.sh -i ./MyFirstPlugin -o ./dist +``` + +Or use the PluginPacker directly: + +```bash +dotnet run --project src/SharpSite.PluginPacker -- -i ./MyFirstPlugin -o ./dist +``` + +## Step 7: Install and Test + +1. The packager will create `my.first.plugin@1.0.0.sspkg` in the dist folder +2. Upload this file through your SharpSite admin interface +3. Enable the plugin and see your theme in action! + +## What You've Learned + +- How to structure a plugin project +- How to implement the IHasStylesheets interface +- How to include CSS assets in your plugin +- How to use the PluginPacker to create distributable packages +- How to include proper documentation and metadata + +## Next Steps + +- Explore other plugin types (FileStorage, etc.) +- Add more complex styling and features +- Create Razor components for your theme +- Study the sample plugins for more advanced examples \ No newline at end of file diff --git a/scripts/package-plugin.ps1 b/scripts/package-plugin.ps1 new file mode 100644 index 0000000..0349d21 --- /dev/null +++ b/scripts/package-plugin.ps1 @@ -0,0 +1,167 @@ +# SharpSite Plugin Packaging Script (PowerShell) +# This script provides an easy way to package SharpSite plugins using the PluginPacker utility + +param( + [Parameter(Mandatory=$true, HelpMessage="Input directory containing the plugin project")] + [Alias("i")] + [string]$InputDir, + + [Parameter(HelpMessage="Output directory for the .sspkg file (default: ./dist)")] + [Alias("o")] + [string]$OutputDir = "./dist", + + [Parameter(HelpMessage="Path to SharpSite source directory (default: auto-detect)")] + [Alias("s")] + [string]$SharpSiteDir = "", + + [Parameter(HelpMessage="Show help message")] + [Alias("h")] + [switch]$Help +) + +# Function to show usage +function Show-Usage { + Write-Host @" +SharpSite Plugin Packaging Script + +Usage: .\package-plugin.ps1 [options] + +Options: + -InputDir, -i DIR Input directory containing the plugin project (required) + -OutputDir, -o DIR Output directory for the .sspkg file (default: ./dist) + -SharpSiteDir, -s DIR Path to SharpSite source directory (default: auto-detect) + -Help, -h Show this help message + +Examples: + .\package-plugin.ps1 -InputDir ./MyPlugin + .\package-plugin.ps1 -i ./MyPlugin -o ./output + .\package-plugin.ps1 -i ./MyPlugin -o ./output -s ../SharpSite + +"@ +} + +# Function to print colored output +function Write-Info { + param([string]$Message) + Write-Host "ℹ️ $Message" -ForegroundColor Blue +} + +function Write-Success { + param([string]$Message) + Write-Host "✅ $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "⚠️ $Message" -ForegroundColor Yellow +} + +function Write-Error { + param([string]$Message) + Write-Host "❌ $Message" -ForegroundColor Red +} + +# Show help if requested +if ($Help) { + Show-Usage + exit 0 +} + +# Validate required parameters +if (-not $InputDir) { + Write-Error "Input directory is required" + Show-Usage + exit 1 +} + +if (-not (Test-Path $InputDir -PathType Container)) { + Write-Error "Input directory '$InputDir' does not exist" + exit 1 +} + +# Auto-detect SharpSite directory if not provided +if (-not $SharpSiteDir) { + $CurrentDir = Get-Location + while ($CurrentDir -and $CurrentDir.Path -ne $CurrentDir.Root) { + $SolutionFile = Join-Path $CurrentDir.Path "SharpSite.sln" + if (Test-Path $SolutionFile) { + $SharpSiteDir = $CurrentDir.Path + break + } + $CurrentDir = $CurrentDir.Parent + } + + if (-not $SharpSiteDir) { + Write-Error "Could not auto-detect SharpSite directory. Please specify with -SharpSiteDir option." + exit 1 + } +} + +# Validate SharpSite directory +$PluginPackerDir = Join-Path $SharpSiteDir "src\SharpSite.PluginPacker" +if (-not (Test-Path $PluginPackerDir -PathType Container)) { + Write-Error "SharpSite.PluginPacker not found at '$PluginPackerDir'" + Write-Error "Please ensure you have the correct SharpSite source directory" + exit 1 +} + +# Create output directory if it doesn't exist +if (-not (Test-Path $OutputDir -PathType Container)) { + Write-Info "Creating output directory: $OutputDir" + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +# Get absolute paths +$InputDir = Resolve-Path $InputDir +$OutputDir = Resolve-Path $OutputDir +$PluginPackerDir = Resolve-Path $PluginPackerDir + +Write-Info "Starting plugin packaging..." +Write-Info "Input directory: $InputDir" +Write-Info "Output directory: $OutputDir" +Write-Info "PluginPacker: $PluginPackerDir" + +# Check if manifest.json exists +$ManifestPath = Join-Path $InputDir "manifest.json" +if (-not (Test-Path $ManifestPath)) { + Write-Warning "No manifest.json found in input directory" + Write-Warning "The PluginPacker will prompt you to create one interactively" +} + +# Build and run the PluginPacker +Write-Info "Building PluginPacker..." +Push-Location $PluginPackerDir +try { + $BuildResult = dotnet build --configuration Release 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to build PluginPacker" + Write-Host $BuildResult + exit 1 + } + + Write-Success "PluginPacker built successfully" + + Write-Info "Packaging plugin..." + $PackageResult = dotnet run --configuration Release --no-build -- -i "$InputDir" -o "$OutputDir" 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Success "Plugin packaged successfully!" + + # Find and display the generated package + $PackageFiles = Get-ChildItem -Path $OutputDir -Filter "*.sspkg" | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-1) } + if ($PackageFiles) { + $PackageFile = $PackageFiles[0] + $PackageSize = [math]::Round($PackageFile.Length / 1KB, 2) + Write-Success "Generated package: $($PackageFile.Name) ($PackageSize KB)" + Write-Info "Location: $($PackageFile.FullName)" + } + } else { + Write-Error "Plugin packaging failed" + Write-Host $PackageResult + exit 1 + } +} finally { + Pop-Location +} + +Write-Success "Plugin packaging completed successfully!" \ No newline at end of file diff --git a/scripts/package-plugin.sh b/scripts/package-plugin.sh new file mode 100755 index 0000000..431c525 --- /dev/null +++ b/scripts/package-plugin.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# SharpSite Plugin Packaging Script +# This script provides an easy way to package SharpSite plugins using the PluginPacker utility + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +# Function to show usage +show_usage() { + cat << EOF +SharpSite Plugin Packaging Script + +Usage: $0 [options] + +Options: + -i, --input DIR Input directory containing the plugin project (required) + -o, --output DIR Output directory for the .sspkg file (default: ./dist) + -s, --sharpsite DIR Path to SharpSite source directory (default: auto-detect) + -h, --help Show this help message + +Examples: + $0 -i ./MyPlugin + $0 -i ./MyPlugin -o ./output + $0 -i ./MyPlugin -o ./output -s ../SharpSite + +EOF +} + +# Default values +INPUT_DIR="" +OUTPUT_DIR="./dist" +SHARPSITE_DIR="" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -i|--input) + INPUT_DIR="$2" + shift 2 + ;; + -o|--output) + OUTPUT_DIR="$2" + shift 2 + ;; + -s|--sharpsite) + SHARPSITE_DIR="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Validate required parameters +if [[ -z "$INPUT_DIR" ]]; then + print_error "Input directory is required" + show_usage + exit 1 +fi + +if [[ ! -d "$INPUT_DIR" ]]; then + print_error "Input directory '$INPUT_DIR' does not exist" + exit 1 +fi + +# Auto-detect SharpSite directory if not provided +if [[ -z "$SHARPSITE_DIR" ]]; then + # Look for SharpSite.sln in current directory and parent directories + CURRENT_DIR="$(pwd)" + while [[ "$CURRENT_DIR" != "/" ]]; do + if [[ -f "$CURRENT_DIR/SharpSite.sln" ]]; then + SHARPSITE_DIR="$CURRENT_DIR" + break + fi + CURRENT_DIR="$(dirname "$CURRENT_DIR")" + done + + if [[ -z "$SHARPSITE_DIR" ]]; then + print_error "Could not auto-detect SharpSite directory. Please specify with -s option." + exit 1 + fi +fi + +# Validate SharpSite directory +PLUGINPACKER_DIR="$SHARPSITE_DIR/src/SharpSite.PluginPacker" +if [[ ! -d "$PLUGINPACKER_DIR" ]]; then + print_error "SharpSite.PluginPacker not found at '$PLUGINPACKER_DIR'" + print_error "Please ensure you have the correct SharpSite source directory" + exit 1 +fi + +# Create output directory if it doesn't exist +if [[ ! -d "$OUTPUT_DIR" ]]; then + print_info "Creating output directory: $OUTPUT_DIR" + mkdir -p "$OUTPUT_DIR" +fi + +# Get absolute paths +INPUT_DIR="$(cd "$INPUT_DIR" && pwd)" +OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" +PLUGINPACKER_DIR="$(cd "$PLUGINPACKER_DIR" && pwd)" + +print_info "Starting plugin packaging..." +print_info "Input directory: $INPUT_DIR" +print_info "Output directory: $OUTPUT_DIR" +print_info "PluginPacker: $PLUGINPACKER_DIR" + +# Check if manifest.json exists +if [[ ! -f "$INPUT_DIR/manifest.json" ]]; then + print_warning "No manifest.json found in input directory" + print_warning "The PluginPacker will prompt you to create one interactively" +fi + +# Build and run the PluginPacker +print_info "Building PluginPacker..." +cd "$PLUGINPACKER_DIR" +if ! dotnet build --configuration Release > /dev/null 2>&1; then + print_error "Failed to build PluginPacker" + exit 1 +fi + +print_success "PluginPacker built successfully" + +print_info "Packaging plugin..." +if dotnet run --configuration Release --no-build -- -i "$INPUT_DIR" -o "$OUTPUT_DIR"; then + print_success "Plugin packaged successfully!" + + # Find and display the generated package + PACKAGE_FILE=$(find "$OUTPUT_DIR" -name "*.sspkg" -type f -newer "$PLUGINPACKER_DIR" | head -n 1) + if [[ -n "$PACKAGE_FILE" ]]; then + PACKAGE_SIZE=$(du -h "$PACKAGE_FILE" | cut -f1) + print_success "Generated package: $(basename "$PACKAGE_FILE") ($PACKAGE_SIZE)" + print_info "Location: $PACKAGE_FILE" + fi +else + print_error "Plugin packaging failed" + exit 1 +fi + +print_success "Plugin packaging completed successfully!" \ No newline at end of file