Compare commits

...

688 Commits

Author SHA1 Message Date
6a43bcf458 Merge pull request #5685 from oqtane/master
6.2.1 Release
2025-09-29 16:43:35 -04:00
ecec06b616 Merge pull request #5684 from oqtane/dev
6.2.1 Release
2025-09-29 16:43:06 -04:00
368c9e60ea Update README.md 2025-09-29 16:41:58 -04:00
f5b4e52526 Merge pull request #5683 from sbwalker/dev
update azuredeploy.json
2025-09-29 16:39:53 -04:00
a939a286ae update azuredeploy.json 2025-09-29 16:39:36 -04:00
88acb2a665 Merge pull request #5682 from sbwalker/dev
remove logo-white.png from application template as it causes conflict
2025-09-29 15:12:35 -04:00
e7c2ad5965 remove logo-white.png from application template as it causes conflict 2025-09-29 15:11:33 -04:00
071cceb7f8 Merge pull request #5681 from sbwalker/dev
exclude module/theme template files from Oqtane.Server Nuget package
2025-09-29 14:53:27 -04:00
377465e361 exclude module/theme template files from Oqtane.Server Nuget package 2025-09-29 14:53:13 -04:00
0f738113af Merge pull request #5679 from sbwalker/dev
add back IsPackable to Oqtane.Server to ensure static web assets are included
2025-09-29 14:04:21 -04:00
534a6147a8 add back IsPackable to Oqtane.Server to ensure static web assets are included 2025-09-29 14:04:00 -04:00
87313c8082 Merge pull request #5678 from sbwalker/dev
update Swashbuckle, Mailkit to latest versions
2025-09-29 13:18:11 -04:00
b315f09640 update Swashbuckle, Mailkit to latest versions 2025-09-29 13:17:56 -04:00
3025f11ea8 Merge pull request #5677 from sbwalker/dev
update Radzen.Blazor to latest version
2025-09-29 13:14:11 -04:00
8fb391717f update Radzen.Blazor to latest version 2025-09-29 13:13:57 -04:00
583ccf9811 Merge pull request #5676 from sbwalker/dev
synchronize static assets with .NET MAUI
2025-09-29 11:32:50 -04:00
cd6ec49cc8 synchronize static assets with .NET MAUI 2025-09-29 11:32:29 -04:00
2b9c5b1728 Merge pull request #5675 from sbwalker/dev
add directory.build.props to centralize propertygroup settings
2025-09-29 09:56:48 -04:00
0e772974a6 add directory.build.props to centralize propertygroup settings 2025-09-29 09:56:35 -04:00
62e181cfc3 Merge pull request #5674 from sbwalker/dev
implement single logout for OIDC
2025-09-29 09:39:29 -04:00
68233951cb implement single logout for OIDC 2025-09-29 09:39:15 -04:00
d8531899b6 Merge pull request #5672 from sbwalker/dev
change term from existing to current
2025-09-29 07:41:02 -04:00
bc2e7915cc change term from existing to current 2025-09-29 07:40:42 -04:00
b18d47afa3 Merge pull request #5670 from zyhfish/task/insert-file-link
do not replace the selected text when insert file link.
2025-09-29 07:26:58 -04:00
Ben
297f91da00 do not replace the selected text when insert file link. 2025-09-28 18:05:17 +08:00
Ben
2bdc7e1bc3 do not replace the selected text when insert file link. 2025-09-28 14:51:55 +08:00
dc0a5c8bb0 Merge pull request #5668 from zyhfish/task/radzen-editor-insert-link
enable to insert file link in radzen editor.
2025-09-27 07:30:41 -04:00
Ben
2a302a187a enable to insert file link in radzen editor. 2025-09-27 09:31:16 +08:00
407a3a19b6 Clarify Installation Wizard instructions in README
Updated wording for clarity regarding the Installation Wizard.
2025-09-26 08:57:29 -04:00
d462fc7afd Update README.md 2025-09-26 08:54:43 -04:00
0b425e3bd9 Merge pull request #5667 from sbwalker/dev
include logo as it is a dependency of the default site template
2025-09-26 08:30:25 -04:00
3efc12fabc include logo as it is a dependency of the default site template 2025-09-26 08:30:05 -04:00
852385a192 Merge pull request #5665 from leigh-pointer/ThemeManagement
Update to Themes Editresx
2025-09-26 07:57:51 -04:00
8077025fe8 Merge pull request #5666 from sbwalker/dev
consolidate template packaging into release.cmd
2025-09-26 07:57:29 -04:00
6f0da0c002 consolidate template packaging into release.cmd 2025-09-26 07:57:13 -04:00
5dbea610c1 Update to Themes Editresx
Small typo fix
2025-09-26 12:41:56 +02:00
92496f4369 Merge pull request #5664 from sbwalker/dev
update README
2025-09-25 14:47:05 -04:00
ec00b1162f update README 2025-09-25 14:46:51 -04:00
e638aee1ac Merge pull request #5663 from sbwalker/dev
changes to template.json based on https://github.com/sayedihashimi/template-sample
2025-09-25 14:43:02 -04:00
5420f625b4 changes to template.json based on https://github.com/sayedihashimi/template-sample 2025-09-25 14:42:40 -04:00
77fa7f4a79 Merge pull request #5662 from sbwalker/dev
add all direct package dependencies to Application Template
2025-09-25 14:09:57 -04:00
2db1fe0890 add all direct package dependencies to Application Template 2025-09-25 14:09:41 -04:00
63bb70785a Merge pull request #5661 from sbwalker/dev
allow themes to define usage permissions similar to modules
2025-09-25 13:55:17 -04:00
8d23d9aba3 allow themes to define usage permissions similar to modules 2025-09-25 13:55:02 -04:00
bebe70f46b Merge pull request #5655 from Raceeend/template_package_reference
Update Oqtane.Application.Server.csproj
2025-09-24 08:18:07 -04:00
34f2db5985 Merge pull request #5654 from mdmontesinos/fix-5648
Fix #5648: Restore order of SetImage in FileManager
2025-09-24 08:17:51 -04:00
8e75c09e3f Merge pull request #5653 from tvatavuk/patch-3
RadzenTextEditor.placeholder.cs in RadzenTextEditor for docs
2025-09-24 08:17:37 -04:00
b5d4eaa36e Merge pull request #5652 from tvatavuk/patch-2
Fix XML comment in UnzipFileAsync method
2025-09-24 08:17:24 -04:00
116d163b9d Update Oqtane.Application.Server.csproj
Add Package reference that will create the BlazorDebugProxy folder.
2025-09-23 22:54:34 +02:00
2cb568773c Restore order of SetImage in FileManager
Fixes #5648
2025-09-23 16:21:20 +02:00
916019f015 RadzenTextEditor.placeholder.cs in RadzenTextEditor for docs
This is just a placeholder file
It is necessary for the documentation to successfully build this project.
Reason is that docfx will run the .net compiler and find references
to this class in the project.
But since the real class is just a .razor file, ATM docfx will fail.
2025-09-23 16:18:52 +02:00
e83d7e9d57 Fix XML comment in UnzipFileAsync method
Removed an unnecessary XML comment parameter closing tag.
2025-09-23 15:21:03 +02:00
151af30259 Merge pull request #5647 from sbwalker/dev
improve Profile ability to use Settings
2025-09-21 11:12:27 -04:00
7fed6bb93a improve Profile ability to use Settings 2025-09-21 11:12:07 -04:00
382a8eb8f3 Merge pull request #5644 from leigh-pointer/PkgUpdates
Package Updates
2025-09-21 08:39:26 -04:00
9508ff68db nuspec files updated 2025-09-21 14:29:05 +02:00
9684e6e1a8 Merge pull request #5645 from sbwalker/dev
use MailboxAddress approach sugested by @jstedfast
2025-09-21 08:09:37 -04:00
52745b1946 use MailboxAddress approach sugested by @jstedfast 2025-09-21 08:09:11 -04:00
3db2d03a37 Package Updates
Updated Client Radzen.Blazo to 7.3.5
Updated Server HtmlAgilityPack 1.12.3
2025-09-20 11:49:10 +02:00
9052d6abb6 Merge pull request #5643 from sbwalker/dev
add whitespace
2025-09-19 15:29:56 -04:00
3c528f0b93 add whitespace 2025-09-19 15:29:30 -04:00
3322297eaa Merge pull request #5642 from sbwalker/dev
improve migration history
2025-09-19 15:25:25 -04:00
3c1167d359 improve migration history 2025-09-19 15:25:11 -04:00
9e35a520cc Merge pull request #5641 from sbwalker/dev
add ability to view Migration History
2025-09-19 15:00:14 -04:00
beb4919d97 add ability to view Migration History 2025-09-19 14:59:58 -04:00
6895d16a20 Merge pull request #5640 from sbwalker/dev
improve NotificationJob validation logic
2025-09-19 12:46:09 -04:00
05b37080c1 improve NotificationJob validation logic 2025-09-19 12:45:55 -04:00
51894de708 Merge pull request #5638 from sbwalker/dev
optimizations to NotificationJob
2025-09-19 09:06:04 -04:00
442ec291a1 optimizations to NotificationJob 2025-09-19 09:05:35 -04:00
6ef106be31 Merge pull request #5637 from sbwalker/dev
synchronize Application Template project settings with Oqtane Framework
2025-09-19 08:55:54 -04:00
70551f9d27 synchronize Application Template project settings with Oqtane Framework 2025-09-19 08:55:36 -04:00
fe422ed5aa Merge pull request #5636 from sbwalker/dev
fix issues with NotificationJob related to MailKit behavior
2025-09-18 17:24:05 -04:00
1995a96a98 fix issues with NotificationJob related to MailKit behavior 2025-09-18 17:19:30 -04:00
4abcc6e58f Merge pull request #5635 from sbwalker/dev
changes to release.cmd
2025-09-18 13:46:33 -04:00
a6f4921055 changes to release.cmd 2025-09-18 13:46:18 -04:00
23e83a5e30 Merge pull request #5633 from leigh-pointer/patch-1
Update README.md
2025-09-18 08:48:01 -04:00
6abd2cf7fc Merge pull request #5634 from sbwalker/dev
use static form for close button in ModuleMessage to support all render modes
2025-09-18 07:55:48 -04:00
085f137942 use static form for close button in ModuleMessage to support all render modes 2025-09-18 07:55:26 -04:00
62d99d33bd Update README.md
Updated the SDK version number and the url to the microsoft default.
2025-09-18 13:23:07 +02:00
57375eaab9 Merge pull request #5632 from sbwalker/dev
update release.cmd
2025-09-17 17:27:06 -04:00
30b7e71cd8 update release.cmd 2025-09-17 17:26:50 -04:00
e26bb66405 Merge pull request #5629 from sbwalker/dev
add class selectors for control panel elements
2025-09-17 10:14:32 -04:00
6263bd3a60 add class selectors for control panel elements 2025-09-17 10:14:13 -04:00
8bee8d2f3f Revise README instructions for application setup 2025-09-16 14:45:33 -04:00
4ffe8fac3a Enhance README with run and debug instructions 2025-09-16 14:44:45 -04:00
09a7457c01 Update README.md 2025-09-16 14:41:36 -04:00
15a8f0a4ac Merge pull request #5627 from sbwalker/dev
use newer Resource overload methods
2025-09-16 12:50:19 -04:00
503134d38c use newer Resource overload methods 2025-09-16 12:50:01 -04:00
e19b8ffed9 Merge pull request #5626 from sbwalker/dev
fix #5616 - show all available modules/themes and indicate if they are installed
2025-09-16 11:36:50 -04:00
c38dc69d3b fix #5616 - show all available modules/themes and indicate if they are installed 2025-09-16 11:36:28 -04:00
ff16fd8b9c Merge pull request #5625 from sbwalker/dev
use enum for MessageStyle
2025-09-16 11:16:41 -04:00
880a6e43d1 use enum for MessageStyle 2025-09-16 11:16:23 -04:00
188d3b42d8 Merge pull request #5623 from zyhfish/task/fix-5612
Fix #5612: add paging function.
2025-09-16 10:24:30 -04:00
c526e01534 Merge pull request #5624 from sbwalker/dev
fix incorrect resourceType in Settings.razor in Application template
2025-09-16 10:23:41 -04:00
dae906d52f fix incorrect resourceType in Settings.razor in Application template 2025-09-16 10:23:26 -04:00
Ben
e620bba0da Fix #5612: add paging function. 2025-09-16 18:21:47 +08:00
fae22595aa Merge pull request #5622 from sbwalker/dev
add toast support to ModuleMessage
2025-09-15 16:42:54 -04:00
a528e5eab2 add toast support to ModuleMessage 2025-09-15 16:42:37 -04:00
0991925090 Merge pull request #5621 from sbwalker/dev
remove static assets from Application Template
2025-09-15 10:02:47 -04:00
f9741a82bd remove static assets from Application Template 2025-09-15 10:02:29 -04:00
f0067d86a6 Merge pull request #5620 from sbwalker/dev
Fix #5613 - add Theme.css for the external theme template
2025-09-15 08:22:10 -04:00
d7aa999f25 add Theme.css for the external theme template 2025-09-15 08:21:28 -04:00
ea87497e6d Merge pull request #5618 from zyhfish/task/move-radzen-editor-settings-dialog
Move radzen editor settings dialog  to correct folder.
2025-09-15 08:02:29 -04:00
2dc8cabc80 Merge pull request #5619 from sbwalker/dev
improve performance of UpdateSettingsAsync method
2025-09-15 08:00:04 -04:00
4e53dcd8d5 improve performance of UpdateSettingsAsync method 2025-09-15 07:59:46 -04:00
Ben
dd447e802e Move radzen editor settings dialog to correct folder. 2025-09-15 17:05:51 +08:00
c7a86aa49c Merge pull request #5611 from sbwalker/dev
remove content files from Oqtane.Server Nuget package
2025-09-12 15:41:34 -04:00
c6e7638e8b remove content files from Oqtane.Server Nuget package 2025-09-12 15:41:18 -04:00
166969bc35 Merge pull request #5610 from sbwalker/dev
adding static assets back to Application Template
2025-09-12 15:38:05 -04:00
61d231801a adding static assets back to Application Template 2025-09-12 15:37:47 -04:00
cc51f5bb0f Merge pull request #5608 from sbwalker/dev
bump version to 6.2.1
2025-09-12 12:32:06 -04:00
dcd99695e7 bump version to 6.2.1 2025-09-12 12:31:47 -04:00
1c78683f4c Merge pull request #5607 from sbwalker/dev
update to .NET SDK 9.0.9
2025-09-12 12:02:16 -04:00
f2124c5ae0 update to .NET SDK 9.0.9 2025-09-12 12:01:57 -04:00
868aca9fdb Merge pull request #5606 from sbwalker/dev
remove static assets from Application Template
2025-09-12 11:43:42 -04:00
d12f7b79d2 remove static assets from Application Template 2025-09-12 11:43:25 -04:00
600bbdfd0d Merge pull request #5605 from sbwalker/dev
Include content files in Oqtane.Server Nuget package
2025-09-12 11:39:23 -04:00
04bc68de55 Include content files in Oqtane.Server Nuget package 2025-09-12 11:39:03 -04:00
82b4f7b611 Merge pull request #5603 from sbwalker/dev
remove OQTANE3 constant
2025-09-12 07:32:12 -04:00
4adba1ab5f remove OQTANE3 constant 2025-09-12 07:31:53 -04:00
6d2ac670af Merge pull request #5602 from sbwalker/dev
prevent the creation of fingerprinted static web assets as Oqtane does not support them
2025-09-11 10:19:13 -04:00
64a03b6e91 prevent the creation of fingerprinted static web assets as Oqtane does not support them 2025-09-11 10:18:45 -04:00
c88958ae7e Merge pull request #5601 from sbwalker/dev
move and rename FileManagerDialog so that it is clear that it is a dependency of RadzenTextEditor
2025-09-11 08:24:40 -04:00
4278b9992b move and rename FileManagerDialog so that it is clear that it is a dependency of RadzenTextEditor 2025-09-11 08:24:14 -04:00
ebac6d51b0 Merge pull request #5598 from sbwalker/dev
improve sort order of user profile options using settings
2025-09-10 17:18:07 -04:00
fba4f23f71 improve sort order of user profile options using settings 2025-09-10 17:17:47 -04:00
7231d2f49e Merge pull request #5597 from sbwalker/dev
add upgrade logic to cleanup assets which were moved to a new location in 6.2.0
2025-09-10 17:02:26 -04:00
d871bffdd5 add upgrade logic to cleanup assets which were moved to a new location in 6.2.0 2025-09-10 17:01:59 -04:00
5630b4842c Merge pull request #5595 from sbwalker/dev
upate README for Application Template
2025-09-10 07:55:52 -04:00
b19141b361 upate README for Application Template 2025-09-10 07:55:31 -04:00
732e279605 Merge pull request #5594 from sbwalker/dev
fix issue with Application template
2025-09-09 16:47:57 -04:00
fa173d492c fix issue with Application template 2025-09-09 16:47:43 -04:00
3259494d45 Merge pull request #5593 from oqtane/master
6.2.0 Release
2025-09-09 16:24:37 -04:00
0a03eb620a Merge pull request #5592 from oqtane/dev
6.2.0 Release
2025-09-09 16:24:12 -04:00
701d8c9a57 Update README.md 2025-09-09 16:23:07 -04:00
fdca8a2890 Merge pull request #5591 from sbwalker/dev
ensure Radzen.Blazor static assets are included in publish output
2025-09-09 14:25:08 -04:00
22e2a4da1e ensure Radzen.Blazor static assets are included in publish output 2025-09-09 14:24:53 -04:00
409523912b Merge pull request #5590 from sbwalker/dev
use standard port #'s
2025-09-09 13:31:08 -04:00
d5c68444c3 use standard port #'s 2025-09-09 13:30:53 -04:00
ffa93e0ee7 Merge pull request #5589 from sbwalker/dev
profile improvements
2025-09-09 08:52:36 -04:00
3f4f1a8278 profile improvements 2025-09-09 08:52:20 -04:00
8e70949880 Merge pull request #5588 from sbwalker/dev
improve setting import
2025-09-08 12:55:58 -04:00
be8436d237 improve setting import 2025-09-08 12:55:45 -04:00
876f13be5e Merge pull request #5587 from sbwalker/dev
add setting import
2025-09-08 12:13:34 -04:00
dfca6640da add setting import 2025-09-08 12:13:17 -04:00
a2e57bc54c Merge pull request #5585 from sbwalker/dev
improve validation
2025-09-05 17:38:17 -04:00
dcc2e59e46 improve validation 2025-09-05 17:38:02 -04:00
90e721b172 Merge pull request #5584 from sbwalker/dev
added a Setting Management UI
2025-09-05 17:20:50 -04:00
94391875d5 added a Setting Management UI 2025-09-05 17:20:31 -04:00
43d06c042d Merge pull request #5583 from sbwalker/dev
allow installer logo to be overridden
2025-09-05 13:12:56 -04:00
3e12910fbd allow installer logo to be overridden 2025-09-05 13:12:39 -04:00
ba70ebe23c Merge pull request #5582 from sbwalker/dev
update application template
2025-09-05 12:36:18 -04:00
b739841495 update application template 2025-09-05 12:36:03 -04:00
acabc75aa6 Merge pull request #5581 from sbwalker/dev
restructure text editors and static assets
2025-09-05 12:33:00 -04:00
27041f464f restructure text editors and static assets 2025-09-05 12:32:43 -04:00
9f923ae968 Merge pull request #5577 from zyhfish/task/radzen-text-editor
implement radzen text editor.
2025-09-05 11:13:52 -04:00
9c7d832357 Merge pull request #5580 from sbwalker/dev
exception handling needs to encapsulate entire method
2025-09-05 11:13:37 -04:00
e913c10d5b exception handling needs to encapsulate entire method 2025-09-05 11:13:21 -04:00
c698188901 Merge pull request #5576 from W6HBR/dev
fix job status issue for disabled jobs
2025-09-05 11:07:52 -04:00
8fd67621ac Merge pull request #5579 from sbwalker/dev
update dependencies in Oqtane.Server.nusepc
2025-09-05 11:05:47 -04:00
0c60085e09 update dependencies in Oqtane.Server.nusepc 2025-09-05 11:05:18 -04:00
1826316c80 Merge pull request #5567 from thabaum/Update-v6.2.0-azure-deploy-and-dependencies
Update v6.2.0 azure deploy and Oqtane.Server project dependencies
2025-09-05 11:02:45 -04:00
07341aeebe Fix Azure deployment link in README
Updated Azure deployment link to use the master branch.
2025-09-05 11:02:00 -04:00
9f6945dda2 Merge pull request #5578 from oqtane/master
Update azuredeploy.json
2025-09-05 11:01:15 -04:00
b39b568b4c Update azuredeploy.json 2025-09-05 11:00:06 -04:00
Ben
e59d5fd339 implement radzen text editor. 2025-09-05 20:36:50 +08:00
b7bc527d6c Added resource message for Message.Job.Disabled 2025-09-04 21:52:24 -07:00
1ea76d06d1 Change StartJob to check if job is enabled 2025-09-04 21:47:51 -07:00
b049be9d83 Change IsStarted to follow IsEnabled value upon startup.
Changes behavior of IsStarted to follow the same value of IsEnabled.
2025-09-04 21:44:14 -07:00
966fc55594 Merge pull request #5574 from sbwalker/dev
fix #5570 - multi-database installation authentication issue
2025-09-04 14:02:24 -04:00
ca9ddbd90f fix #5570 - multi-database installation authentication issue 2025-09-04 14:01:42 -04:00
0d04926d9f Merge pull request #5569 from sbwalker/dev
fix issue in application template
2025-09-02 17:11:32 -04:00
2b500d41ca fix issue in application template 2025-09-02 17:11:23 -04:00
5c67eeea58 Merge pull request #5568 from sbwalker/dev
fix issue in default module template
2025-09-02 17:10:06 -04:00
09daf3f6cc fix issue in default module template 2025-09-02 17:09:55 -04:00
9a06a3311e Update azuredeploy.json to v6.2.0 2025-09-02 12:58:04 -07:00
304694fbf9 Update to latest SQLitePCLRaw.bundle_e_sqlite3 & Swashbuckle.AspNetCore Package Dependencies 2025-09-02 12:53:42 -07:00
96ba42df96 Merge pull request #5565 from sbwalker/dev
bump version to 6.2.0
2025-09-02 13:59:38 -04:00
e7bc11d026 bump version to 6.2.0 2025-09-02 13:59:27 -04:00
1272305355 Merge pull request #5564 from sbwalker/dev
fix help text related to module/theme upload
2025-09-02 08:50:45 -04:00
30c6da13c2 fix help text related to module/theme upload 2025-09-02 08:49:32 -04:00
5aacb2b877 Merge pull request #5563 from sbwalker/dev
allow modules to be able to specify the databases they support
2025-09-02 08:33:51 -04:00
b5fdf42c37 allow modules to be able to specify the databases they support 2025-09-02 08:32:46 -04:00
c81d677c5c Merge pull request #5559 from sbwalker/dev
added support for cookie domain option in User Management Settings
2025-08-30 08:01:56 -04:00
6daf675e52 added support for cookie domain option in User Management Settings 2025-08-30 08:01:18 -04:00
3f7a7f3340 Merge pull request #5558 from sbwalker/dev
added StaticAssetPath properties to base classes
2025-08-30 07:49:06 -04:00
1ebf3c4077 added StaticAssetPath properties to base classes 2025-08-30 07:48:26 -04:00
1f1173ae03 Merge pull request #5557 from sbwalker/dev
add comments
2025-08-30 07:27:23 -04:00
efa466e1d6 add comments 2025-08-30 07:26:37 -04:00
cefe349b4e Merge pull request #5555 from sbwalker/dev
remove hardcoded references to LocalDB
2025-08-29 17:17:27 -04:00
a9bc356f37 remove hardcoded references to LocalDB 2025-08-29 17:16:42 -04:00
6fc791020c Merge pull request #5554 from sbwalker/dev
move default template static assets
2025-08-29 16:34:35 -04:00
713ec1b373 move default template static assets 2025-08-29 16:33:51 -04:00
e3fa781122 Merge pull request #5553 from sbwalker/dev
improve default theme template to follow RCL/Nuget standards
2025-08-29 16:31:18 -04:00
e4b6d0ff29 improve default theme template to follow RCL/Nuget standards 2025-08-29 16:30:49 -04:00
cd2a328560 Merge pull request #5552 from sbwalker/dev
improve default module template to follow RCL/Nuget standards
2025-08-29 16:21:06 -04:00
d2d88d4b5e improve default module template to follow RCL/Nuget standards 2025-08-29 16:20:16 -04:00
0067cc4266 Added FixProps command line utility 2025-08-29 15:19:07 -04:00
da3afefa8d Merge pull request #5551 from sbwalker/dev
update default module/theme templates to use projectType rather than dependency in nuspec file
2025-08-29 15:15:12 -04:00
ab534d07f3 update default module/theme templates to use projectType rather than dependency in nuspec file 2025-08-29 15:14:48 -04:00
49c513ac9b Merge pull request #5550 from sbwalker/dev
add support for packageType in nuspec files for minimum Oqtane version
2025-08-29 14:31:07 -04:00
6f7a18674e add support for packageType in nuspec files for minimum Oqtane version 2025-08-29 14:30:49 -04:00
0f559ba42d Merge pull request #5546 from sbwalker/dev
install wizard should use RenderMode and Runtime values from appsettings.json when creating site
2025-08-27 14:28:43 -04:00
2af02fae95 install wizard should use RenderMode and Runtime values from appsettings.json when creating site 2025-08-27 14:28:23 -04:00
006423e32e Change RenderMode from Interactive to Static 2025-08-27 14:27:28 -04:00
23f29ca55d Change RenderMode from Interactive to Static 2025-08-27 14:27:01 -04:00
68a7571741 Change RenderMode from Interactive to Static 2025-08-27 14:26:47 -04:00
10e60e352a Merge pull request #5545 from sbwalker/dev
improve help text
2025-08-27 14:08:08 -04:00
3b16ae8cc0 improve help text 2025-08-27 14:07:51 -04:00
66c4737021 Merge pull request #5533 from zyhfish/task/fix-5532
Fix #5532: add require nonce setting.
2025-08-27 13:55:20 -04:00
8684e03af1 Merge pull request #5544 from sbwalker/dev
fix #5531 - external login single sign-on for multiple sites
2025-08-27 13:54:46 -04:00
edad9e6b3c fix #5531 - external login single sign-on for multiple sites 2025-08-27 13:54:30 -04:00
66b89752d3 Merge pull request #5543 from sbwalker/dev
fix resources in default theme template
2025-08-27 12:29:04 -04:00
9a6195edf1 fix resources in default theme template 2025-08-27 12:28:51 -04:00
2bd07b54b6 Merge pull request #5542 from sbwalker/dev
optimize client startup in templates
2025-08-27 12:21:16 -04:00
7cf9d9ad65 optimize client startup in templates 2025-08-27 12:20:59 -04:00
4dff30ec8c Merge pull request #5540 from sbwalker/dev
default index component to interactive
2025-08-27 09:21:34 -04:00
581f14e661 default index component to interactive 2025-08-27 09:21:19 -04:00
8ccdc37b64 Merge pull request #5538 from sbwalker/dev
fix naming
2025-08-26 17:22:23 -04:00
9e85b35498 fix naming 2025-08-26 17:22:10 -04:00
fff408a5bf Merge pull request #5537 from sbwalker/dev
application template changes
2025-08-26 17:16:01 -04:00
4d5168c998 application template changes 2025-08-26 17:15:46 -04:00
bf2c978f1d Merge pull request #5536 from sbwalker/dev
optimize startup
2025-08-26 15:27:51 -04:00
ec06c1cdf1 optimize startup 2025-08-26 15:27:35 -04:00
Ben
f451cfce09 Fix #5532: remove duplicated semi colon. 2025-08-26 20:27:41 +08:00
Ben
91e55aeb9b Fix #5532: change the default value to true. 2025-08-26 20:26:11 +08:00
Ben
919fb5012f Fix #5532: add require nonce setting. 2025-08-26 18:13:09 +08:00
2bb6226e78 Merge pull request #5530 from sbwalker/dev
remove unecessary content exclusion
2025-08-22 14:25:59 -04:00
6a0c47f7b1 remove unecessary content exclusion 2025-08-22 14:21:49 -04:00
31b688cbf6 Merge pull request #5529 from sbwalker/dev
make kestrel the default web server for the app template
2025-08-22 10:06:25 -04:00
7f1fed2fb1 make kestrel the default web server for the app template 2025-08-22 10:06:10 -04:00
aa6c876b12 Merge pull request #5528 from sbwalker/dev
more template optimizations
2025-08-22 09:17:40 -04:00
4e33aeef89 more template optimizations 2025-08-22 09:17:24 -04:00
e2601dcf05 Merge pull request #5527 from sbwalker/dev
increment template version
2025-08-22 08:55:37 -04:00
247baa375d increment template version 2025-08-22 08:55:20 -04:00
a4adba846e Merge pull request #5526 from sbwalker/dev
declare dependencies in nuspec files and optimize application template
2025-08-22 08:42:09 -04:00
52799c7cb0 declare dependencies in nuspec files and optimize application template 2025-08-22 08:41:52 -04:00
a8635dc555 Merge pull request #5523 from sbwalker/dev
fix #5520 - site soft delete should only be visible to Host users
2025-08-21 09:59:31 -04:00
cca0f2219e fix #5520 - site soft delete should only be visible to Host users 2025-08-21 09:59:15 -04:00
d2f8c3c2bb Merge pull request #5522 from sbwalker/dev
fix #5519 - Site hard delete exception
2025-08-21 09:57:44 -04:00
0f38df053f fix #5519 - Site hard delete exception 2025-08-21 09:57:29 -04:00
5c926a10a7 Merge pull request #5521 from sbwalker/dev
template updates
2025-08-21 09:56:18 -04:00
036bbb418e template updates 2025-08-21 09:56:03 -04:00
93d224fa37 Merge pull request #5518 from sbwalker/dev
support for staticwebassets folder in Nuget packages
2025-08-20 07:09:50 -04:00
5b45e3e417 support for staticwebassets folder in Nuget packages 2025-08-20 07:09:33 -04:00
c2f2dfd837 Merge pull request #5517 from sbwalker/dev
fix filename in template
2025-08-19 14:01:38 -04:00
2f2baf12fb fix filename in template 2025-08-19 14:01:25 -04:00
052c339d0d Merge pull request #5516 from sbwalker/dev
add additional SSL connection options for SMTP
2025-08-19 13:16:51 -04:00
96192e2e06 add additional SSL connection options for SMTP 2025-08-19 13:16:37 -04:00
ea9fa30358 Merge pull request #5515 from sbwalker/dev
template improvements
2025-08-19 12:27:10 -04:00
78f8e2f484 template improvements 2025-08-19 12:26:54 -04:00
0fe2a3fb80 Merge pull request #5514 from sbwalker/dev
add project reference in AppHost to Server so that dependencies will be automatically copied
2025-08-19 12:10:35 -04:00
a340f52973 add project reference in AppHost to Server so that dependencies will be automatically copied 2025-08-19 12:10:15 -04:00
bd94b715ba Merge pull request #5513 from sbwalker/dev
Resources for Interactive module components should be managed via JS Interop
2025-08-19 12:05:58 -04:00
b9a97ffa4c Resources for Interactive module components should be managed via JS Interop 2025-08-19 12:05:39 -04:00
5a37ab1b89 Merge pull request #5510 from sbwalker/dev
modify template content
2025-08-17 10:54:01 -04:00
67a6ac2240 Merge branch 'dev' into dev 2025-08-17 10:53:54 -04:00
7b42845ecc modify template content 2025-08-17 10:53:22 -04:00
3ef39896d1 Merge pull request #5509 from sbwalker/dev
update Azure ARM template to 6.1.5
2025-08-17 10:29:37 -04:00
b01f3b505d update Azure ARM template to 6.1.5 2025-08-17 10:29:25 -04:00
84c5e4c30b Update README.md 2025-08-17 10:10:56 -04:00
abc0f3943e Merge pull request #5507 from oqtane/master
6.1.5 Release
2025-08-17 09:59:14 -04:00
c7b71db015 Merge pull request #5506 from oqtane/dev
6.1.5 Release
2025-08-17 09:58:54 -04:00
f5a8a953bb Update README.md 2025-08-16 09:39:09 -04:00
8e965912aa Update README.md 2025-08-16 09:34:21 -04:00
6c3cfb0c7a Update README.md 2025-08-16 09:33:28 -04:00
85d162aa9d Update README.md 2025-08-16 09:32:50 -04:00
67c460dfa5 Merge pull request #5502 from thabaum/6.1.5-Maui-Version-9.0.100
Fixes #5501: Updates Maui Project Dependencies to version 9.0.100
2025-08-15 16:15:49 -04:00
83d35dbc65 Updates Maui Project Dependencies to version 9.0.100 2025-08-15 11:19:00 -07:00
86735a5afd Update README.md 2025-08-15 14:15:13 -04:00
6ecbb89469 Merge pull request #5500 from sbwalker/dev
consolidate packaging
2025-08-15 14:07:07 -04:00
2ca0508030 consolidate packaging 2025-08-15 14:06:28 -04:00
8fbd50dcef Merge pull request #5499 from sbwalker/dev
resolve issue related to moving database providers to Oqtane.Server
2025-08-15 13:57:16 -04:00
2143660345 resolve issue related tp moving database providers to Oqtane.Server 2025-08-15 13:56:57 -04:00
8c903fbfdd Merge pull request #5496 from sbwalker/dev
update Microsoft.Data.SqlClient in AppHost
2025-08-15 13:37:49 -04:00
33be372348 update Microsoft.Data.SqlClient in AppHost 2025-08-15 13:37:35 -04:00
447ec3f5e6 Merge pull request #5495 from leigh-pointer/Microsoft.Data.SqlClient
Microsoft.Data.SqlClient updated
2025-08-15 13:35:00 -04:00
a4aed69887 Microsoft.Data.SqlClient updated
Updated Microsoft.Data.SqlClient to 6.1.1
2025-08-15 19:01:03 +02:00
bbbd6e9e3e Merge pull request #5494 from sbwalker/dev
remove unecessary using
2025-08-15 12:46:49 -04:00
06712faee9 remove unecessary using 2025-08-15 12:46:35 -04:00
48a90072ee Update README.md 2025-08-15 12:44:50 -04:00
0344f4d60b Update README.md 2025-08-15 12:44:31 -04:00
6a4affd5a6 Merge pull request #5493 from sbwalker/dev
add a new Visual Studio Project Template
2025-08-15 12:43:53 -04:00
d73e2288bb add a new Visual Studio Project Template 2025-08-15 12:43:32 -04:00
7d7500ba05 Merge pull request #5492 from sbwalker/dev
remove content from readme,md
2025-08-15 10:44:07 -04:00
247fc5248b remove content from readme,md 2025-08-15 10:43:52 -04:00
85fcd1ed33 Merge pull request #5490 from sbwalker/dev
improve error handling for the scenario where a connection string does not exist in appsettings.json for a tenant
2025-08-14 15:58:13 -04:00
4ab8f8cc25 improve error handling for the scenario where a connection string does not exist in appsettings.json for a tenant 2025-08-14 15:57:50 -04:00
ccdfe9bc26 Update appsettings.release.json 2025-08-14 15:38:38 -04:00
dc47961cc2 Update appsettings.json 2025-08-14 15:36:36 -04:00
87394cd330 Merge pull request #5489 from sbwalker/dev
migrate database providers to core framework
2025-08-14 15:20:11 -04:00
b5a9c32c3e migrate database providers to core framework 2025-08-14 15:19:52 -04:00
d16521f037 Merge pull request #5487 from sbwalker/dev
fix #5462 add logic to check if database already exists before calling EnsureCreated
2025-08-13 16:13:59 -04:00
b553b16049 fix #5462 add logic to check if database already exists before calling EnsureCreated 2025-08-13 16:13:38 -04:00
784548be57 Merge pull request #5486 from sbwalker/dev
Include support for DateTime values in RewriteValue method
2025-08-13 15:13:13 -04:00
cf96a80ead Include support for DateTime values in RewriteValue method 2025-08-13 15:12:58 -04:00
ede6babeaf Merge pull request #5485 from sbwalker/dev
fix compatibility issue
2025-08-13 14:55:32 -04:00
9a57cae4bd fix compatibility issue 2025-08-13 14:55:18 -04:00
1a296bf58c Merge pull request #5484 from sbwalker/dev
consolidate Infrastructure interface and implementation classes
2025-08-13 14:45:01 -04:00
e900d2f35a consolidate Infrastructure interface and implementation classes 2025-08-13 14:44:42 -04:00
69d2d3d942 Merge pull request #5483 from sbwalker/dev
add authorization convenience methods to ModuleBase
2025-08-13 08:18:33 -04:00
b7ff49bdb2 add authorization convenience methods to ModuleBase 2025-08-13 08:18:16 -04:00
3284e0f60a Merge pull request #5482 from sbwalker/dev
upgrade SQLitePCLRaw.bundle_e_sqlite3 package and add logic to release.cmd to remove android and ios client runtimes
2025-08-12 16:37:03 -04:00
8cec847188 upgrade SQLitePCLRaw.bundle_e_sqlite3 package and add logic to release.cmd to remove android and ios client runtimes 2025-08-12 16:36:39 -04:00
2d44644a3d Merge pull request #5481 from sbwalker/dev
bump version to 6.1.5
2025-08-12 16:08:46 -04:00
e32f55e433 bump version to 6.1.5 2025-08-12 16:08:30 -04:00
362c4ae272 Merge pull request #5480 from sbwalker/dev
ensure all install config settings are populated
2025-08-12 15:33:33 -04:00
eb8ad04557 ensure all install config settings are populated 2025-08-12 15:33:17 -04:00
d1455596c6 Merge pull request #5479 from sbwalker/dev
add InsertData(), UpdateData(), DeleteData() migration methods and improve RewriteValue() abstraction
2025-08-12 15:00:12 -04:00
6142bfc5db add InsertData(), UpdateData(), DeleteData() migration methods and improve RewriteValue() abstraction 2025-08-12 14:59:51 -04:00
dbda0be53b Merge pull request #5478 from sbwalker/dev
follow same pattern as core framework
2025-08-11 17:11:15 -04:00
bf932719b2 follow same pattern as core framework 2025-08-11 17:10:58 -04:00
60e6e33805 Merge pull request #5477 from sbwalker/dev
consolidate Service interface and implementation classes
2025-08-11 16:53:50 -04:00
64b8b5d3c8 consolidate Service interface and implementation classes 2025-08-11 16:53:32 -04:00
8bce40c2b8 Merge pull request #5476 from sbwalker/dev
consolidate interface and implementation classes
2025-08-11 16:36:29 -04:00
b3f6194fda consolidate interface and implementation classes 2025-08-11 16:36:09 -04:00
fdbf2ab0a7 Merge pull request #5475 from sbwalker/dev
fix issue with Admin Site Template
2025-08-11 16:22:40 -04:00
d7eb0dc509 fix issue with Admin Site Template 2025-08-11 16:22:22 -04:00
1a34bf4460 Merge pull request #5469 from sbwalker/dev
add missing delete setting API method
2025-08-07 15:07:46 -04:00
4cf1b5c0e7 add missing delete setting API method 2025-08-07 15:07:33 -04:00
764b883579 Merge pull request #5468 from sbwalker/dev
only hosts should be allowed to view/edit SMTP settings
2025-08-07 14:42:40 -04:00
3bd6767138 only hosts should be allowed to view/edit SMTP settings 2025-08-07 14:42:24 -04:00
bef9025b6c Merge pull request #5467 from sbwalker/dev
fix malformed bold tag
2025-08-07 14:32:09 -04:00
a37f07d20b fix malformed bold tag 2025-08-07 14:31:57 -04:00
638946b1f5 Merge pull request #5466 from sbwalker/dev
performance improvement to filter settings in database
2025-08-07 14:30:27 -04:00
30c869ff2a performance improvement to filter settings in database 2025-08-07 14:30:13 -04:00
2c3fda9cb5 Merge pull request #5464 from sbwalker/dev
fix #5461 - handle MinDate and MaxDate
2025-08-07 10:58:54 -04:00
b11a7a678c fix #5461 - handle MinDate and MaxDate 2025-08-07 10:58:33 -04:00
02011f9ce5 Merge pull request #5463 from leigh-pointer/REFsUpdate908
Updated Project Refs 9.0.8
2025-08-07 10:38:02 -04:00
39ae6a76cd Updated Project Refs 9.0.8
# SQLitePCLRaw.bundle_e_sqlite3 was not updated.
Oqtane Framework
Project Templates Module and Theme
MAUI solution
2025-08-07 08:09:34 +02:00
31684bf7ca Merge pull request #5458 from sbwalker/dev
rolling back to SQLitePCLRaw.bundle_e_sqlite3 version 2.1.11
2025-08-04 17:18:42 -04:00
7b36f8d122 rolling back to SQLitePCLRaw.bundle_e_sqlite3 version 2.1.11 2025-08-04 17:18:25 -04:00
f2a0be4f57 Merge pull request #5457 from sbwalker/dev
resolve interactive page load
2025-08-04 13:09:52 -04:00
2cefab1c64 resolve interactive page load 2025-08-04 13:09:37 -04:00
5b4b96f065 Merge pull request #5456 from sbwalker/dev
improve FileManager performance
2025-08-04 13:06:34 -04:00
77949331e2 improve FileManager performance 2025-08-04 13:06:16 -04:00
4f8c4f47e2 Merge pull request #5454 from sbwalker/dev
improve FileManager performance when ShowFiles is disabled
2025-08-02 09:46:20 -04:00
334137454e improve FileManager performance when ShowFiles is disabled 2025-08-02 09:46:02 -04:00
af7ea3efa8 Merge pull request #5453 from sbwalker/dev
improve interactive rendering logic
2025-08-01 15:43:36 -04:00
6119417331 improve interactive rendering logic 2025-08-01 15:43:21 -04:00
580397a82d Merge pull request #5452 from sbwalker/dev
add active/deleted filter in User Management
2025-08-01 14:45:54 -04:00
23c3c47db4 add active/deleted filter in User Management 2025-08-01 14:45:40 -04:00
df3073fb12 Merge pull request #5451 from sbwalker/dev
improve broken link handling
2025-08-01 10:54:55 -04:00
aa9664e187 improve broken link handling 2025-08-01 10:54:40 -04:00
44f4aee55d Merge pull request #5450 from sbwalker/dev
fix AddModuleMessage not displaying messages in Interactive render mode
2025-08-01 09:15:12 -04:00
02861b8e01 fix AddModuleMessage not displaying messages in Interactive render mode 2025-08-01 09:14:58 -04:00
9607110381 Merge pull request #5449 from sbwalker/dev
Resolve issue where visitor cookie was not being added to HttpClient. This was because cookie values cannot contain spaces and therefore need to be Url encoded.
2025-08-01 07:52:25 -04:00
9ae12ff678 Resolve issue where visitor cookie was not being added to HttpClient. This was because cookie values cannot contain spaces and therefore need to be Url encoded. 2025-08-01 07:51:58 -04:00
2bcb8636ca Merge pull request #5448 from sbwalker/dev
log the logout event
2025-07-31 16:23:56 -04:00
4c2960eeae log the logout event 2025-07-31 16:23:40 -04:00
7e2c76e872 Merge pull request #5447 from sbwalker/dev
improve notification message when email is verified by administrator
2025-07-31 16:06:57 -04:00
30fcde7157 improve notification message when email is verified by administrator 2025-07-31 16:06:42 -04:00
4971d3317d Merge pull request #5446 from thabaum/6.1.5-oqtane.server-update-dependencies
Fixes #5445: Updates Oqtane.Server.csproj Package Dependencies
2025-07-31 15:54:55 -04:00
85ae7b01b8 Update Oqtane.Server.csproj Package Dependencies 2025-07-31 09:11:09 -07:00
9f566624fe Merge pull request #5444 from sbwalker/dev
resolve interactive rendering issue
2025-07-31 11:04:37 -04:00
50fa95dff9 resolve interactive rendering issue 2025-07-31 11:04:22 -04:00
752083e9eb Update README.md 2025-07-30 15:29:19 -04:00
582c7f83f7 Merge pull request #5440 from sbwalker/dev
update Azure ARM template to 6.1.4
2025-07-30 15:23:29 -04:00
d95104cb92 update Azure ARM template to 6.1.4 2025-07-30 15:23:16 -04:00
6c58ab4554 Merge pull request #5439 from oqtane/master
6.1.4 Release
2025-07-30 15:11:01 -04:00
085187cfac 6.1.4 Release
6.1.4 Release
2025-07-30 15:10:42 -04:00
3d0f0a5adc Merge pull request #5437 from sbwalker/dev
synchronize app.css with .NET MAUI
2025-07-30 13:40:41 -04:00
eae8b431ee synchronize app.css with .NET MAUI 2025-07-30 13:40:25 -04:00
e3a34446c0 Merge pull request #5436 from sbwalker/dev
synchronize interop,js with .NET MAUI
2025-07-30 13:35:56 -04:00
bfe57c3ac7 synchronize interop,js with .NET MAUI 2025-07-30 13:35:39 -04:00
d4001be716 Merge pull request #5435 from sbwalker/dev
fix #5364 - add ability to specify preferred Container per Pane
2025-07-30 10:43:51 -04:00
662a1817f2 fix #5364 - add ability to specify preferred Container per Pane 2025-07-30 10:43:36 -04:00
2c99ef412d Merge pull request #5434 from sbwalker/dev
use consistent terminology
2025-07-30 10:01:11 -04:00
f53ed5b13b use consistent terminology 2025-07-30 10:00:57 -04:00
b5d51838c6 Merge pull request #5433 from sbwalker/dev
allow specific time zones to be excluded
2025-07-30 09:29:56 -04:00
92fd70198a allow specific time zones to be excluded 2025-07-30 09:29:43 -04:00
7f1990f851 Merge pull request #5432 from sbwalker/dev
fix incorrect resource reference
2025-07-30 08:48:04 -04:00
797d7afc3e fix incorrect resource reference 2025-07-30 08:47:50 -04:00
c5a23cdfa0 Merge pull request #5431 from sbwalker/dev
update Oqtane theme to Bootstrap 5.3.7
2025-07-30 08:30:54 -04:00
906358f1f8 update Oqtane theme to Bootstrap 5.3.7 2025-07-30 08:30:40 -04:00
638f2a59c5 Merge pull request #5430 from sbwalker/dev
use margin rather than padding
2025-07-30 08:16:20 -04:00
cf9b4b869c use margin rather than padding 2025-07-30 08:16:07 -04:00
671c52fbbb Merge pull request #5429 from leigh-pointer/CDN-Bootstrap
Discussion #5426 updated and returned to https://cdnjs.com/
2025-07-30 08:07:52 -04:00
6c0e2a62e7 Discussion #5426 updated and returned to https://cdnjs.com/
Updated and styles tested - reload.js needs still testing?
2025-07-30 12:53:59 +02:00
1b78c9ad81 Merge pull request #5428 from sbwalker/dev
use consistent naming
2025-07-29 16:36:43 -04:00
7a4b98aec9 use consistent naming 2025-07-29 16:36:28 -04:00
9ef6c15014 Merge pull request #5427 from sbwalker/dev
fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site
2025-07-29 16:20:37 -04:00
f4cea3fe03 fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site 2025-07-29 16:20:07 -04:00
5dd9b1ec91 Merge pull request #5425 from sbwalker/dev
fix #5346 - deleting role should remove associated useroles
2025-07-29 09:05:54 -04:00
658059806b fix #5346 - deleting role should remove associated useroles 2025-07-29 09:05:37 -04:00
4f8a18451c Merge pull request #5424 from sbwalker/dev
fix #5346 - deleting role should remove associated permissions
2025-07-29 08:40:54 -04:00
b1770ebb76 fix #5346 - deleting role should remove associated permissions 2025-07-29 08:40:38 -04:00
6923065d86 Merge pull request #5423 from sbwalker/dev
fix #5348 - ensure time zones work consistently on all platforms
2025-07-29 08:11:56 -04:00
9f097521f6 fix #5348 - ensure time zones work consistently on all platforms 2025-07-29 08:11:42 -04:00
235e5c1d3a Merge pull request #5421 from sbwalker/dev
improve TimeZoneService
2025-07-28 17:00:47 -04:00
e179976fe8 improve TimeZoneService 2025-07-28 17:00:27 -04:00
082726b405 Merge pull request #5420 from sbwalker/dev
fix #5372 - add support for sending SMTP emails using OAuth
2025-07-28 10:26:34 -04:00
91c5309855 fix #5372 - add support for sending SMTP emails using OAuth 2025-07-28 10:26:18 -04:00
92be1e7a5c Merge pull request #5419 from sbwalker/dev
add OAuth support to Notification Job (#5372)
2025-07-28 09:06:55 -04:00
cceda1db1e add OAuth support to Notification Job (#5372) 2025-07-28 09:06:36 -04:00
a59191cea7 Merge pull request #5416 from sbwalker/dev
fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations
2025-07-25 15:22:54 -04:00
b0dee4a60c fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations 2025-07-25 15:22:26 -04:00
3f33f2b9df Merge pull request #5412 from sbwalker/dev
fix #5410 - allow duplicate email addresses
2025-07-23 16:40:27 -04:00
97116b4e0c fix #5410 - allow duplicate email addresses 2025-07-23 16:40:12 -04:00
a5f51ff9a1 Merge pull request #5411 from sbwalker/dev
localize time zone names
2025-07-23 14:52:34 -04:00
962488fd34 localize time zone names 2025-07-23 14:52:18 -04:00
190d973b77 Merge pull request #5406 from leigh-pointer/Refs
Solutions References update
2025-07-22 16:14:13 -04:00
397e0b3f71 Merge pull request #5408 from sbwalker/dev
improve user experience of permissions grid
2025-07-22 16:12:48 -04:00
83ba9ca73e improve user experience of permissions grid 2025-07-22 16:07:52 -04:00
3d08138686 Merge pull request #5407 from sbwalker/dev
improve documentation
2025-07-22 09:23:42 -04:00
262fa6b99b improve documentation 2025-07-22 09:23:26 -04:00
372db9dcfa Solutions References update
MySql.Data 9.4.0
HtmlAgilityPack 1.12.2
2025-07-22 07:45:03 +02:00
e9dc52919c Merge pull request #5405 from sbwalker/dev
fix Control Panel to initialize extended module permissions when module is added or copied
2025-07-21 16:34:56 -04:00
a981dd0e97 fix Control Panel to initialize extended module permissions when module is added or copied 2025-07-21 16:34:34 -04:00
7c2775119b Merge pull request #5404 from sbwalker/dev
add new option to FileManager component to anonymize filenames during upload
2025-07-21 09:14:30 -04:00
0be7f1bdb5 add new option to FileManager component to anonymize filenames during upload 2025-07-21 09:14:07 -04:00
8446b9e8d5 Merge pull request #5392 from thabaum/patch-15 2025-07-15 14:28:50 -04:00
ce404668d3 Merge pull request #5391 from thabaum/6.1.4-dependencies 2025-07-15 14:28:28 -04:00
948fab50ee [FIX] #5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:10:07 -07:00
9690f1df48 [FIX] oqtane#5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:09:24 -07:00
5a24f87293 [FIX] #5164 ‑ Set z‑index for .dropdown‑menu in .app‑moduleactions 2025-07-14 17:07:39 -07:00
d2ff49fe73 [FIX] #5164 - Set z-index for .dropdown-menu in .app-moduleactions 2025-07-14 16:10:50 -07:00
e9035df9d2 Update Package Dependencies 2025-07-14 13:40:52 -07:00
1ddf21f4fc Merge pull request #5387 from mdmontesinos/feat-nodatime 2025-07-11 07:18:10 -04:00
63d2ded038 Merge branch 'dev' into feat-nodatime 2025-07-11 09:07:12 +02:00
7b8e0e48c0 Merge pull request #5385 from leigh-pointer/907 2025-07-11 02:28:38 -04:00
bb52402a17 feat: handle timezones and conversions with NodaTime 2025-07-09 12:09:00 +02:00
13d9cb461b Update Oqtane Maui project to 9.0.7 2025-07-09 03:42:26 +02:00
0a994afd67 Update References .NetCore 9.0.7 2025-07-09 02:52:30 +02:00
57a1257750 Merge pull request #5384 from sbwalker/dev
update External Login default values for Facebook OAuth2
2025-07-08 16:27:58 -04:00
b0c1d36bab update External Login default values for Facebook OAuth2 2025-07-08 16:27:35 -04:00
0621751968 Merge pull request #5383 from sbwalker/dev
resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user
2025-07-08 16:04:37 -04:00
461330773a resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user 2025-07-08 16:04:19 -04:00
818a97cc2c Merge pull request #5382 from sbwalker/dev
bump version to 6.1.4
2025-07-08 13:20:43 -04:00
17045073c8 bump version to 6.1.4 2025-07-08 13:20:28 -04:00
7a818ee698 Merge pull request #5381 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:15:10 -04:00
668e0cb4eb update to .NET SDK 9.0.6 2025-07-08 13:14:53 -04:00
741b16ca4e Merge pull request #5380 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:12:06 -04:00
85a376b17d update to .NET SDK 9.0.6 2025-07-08 13:11:52 -04:00
df86cd909c Merge pull request #5379 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:09:24 -04:00
ac236607f5 update to .NET SDK 9.0.6 2025-07-08 13:09:10 -04:00
19813b7eb6 Merge pull request #5378 from sbwalker/dev
remove unused variable
2025-07-07 12:42:51 -04:00
cb5e4e076f remove unused variable 2025-07-07 12:42:35 -04:00
48fca77f59 Merge pull request #5276 from leigh-pointer/Bootstrap
Updated to Bootstrap 5.3.5
2025-07-07 12:40:39 -04:00
76372451aa Merge pull request #5370 from zyhfish/task/fix-5363
Fix #5363: update SettingService.MergeSettings.
2025-07-07 12:40:14 -04:00
34cd197122 Merge pull request #5376 from mdmontesinos/feat-mailkit
feat: replace System.Net.Mail with MailKit (#5372)
2025-07-07 12:40:05 -04:00
6b567364f9 feat: use appropriate UseSSL equivalent in MailKit 2025-07-04 14:55:02 +02:00
711de49571 feat: replace System.Net.Mail with MailKit (#5372) 2025-07-04 12:55:40 +02:00
9a0f7ad83f Merge pull request #5375 from sbwalker/dev
fix #5374 Visitor Settings not returned due to change in Visitor cookie format
2025-07-03 16:45:19 -04:00
0d3d693799 fix #5374 Visitor Settings not returned due to change in Visitor cookie format 2025-07-03 16:44:59 -04:00
Ben
b63590d6c7 Fix #5363: update SettingService.MergeSettings. 2025-07-03 15:42:11 +08:00
5f3a3d4d54 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-06-13 19:58:18 +02:00
b1a8c28283 Merge pull request #5356 from leigh-pointer/Schedular
Fix for Scheduled Jobs UI #5354
2025-06-13 08:36:47 -04:00
1412737036 Date / Time validations
This PR ensures time fields are required when dates are set, using Oqtane validation and dynamically toggles the required attribute on time inputs when their corresponding date fields have values. Benefits:
- Uses Oqtane's validation for a polished UX.
- Reduces custom validation code.
- Aligns with our internal form logic.

- Tested across all date/time scenarios—works flawlessly!
**Testing Confirmed:**
- Date + Time Provided → Saves successfully.
- No Date + No Time → Optional (no validation).
- Date + No Time → Browser blocks submission with icon error.
2025-06-10 12:27:55 +02:00
ffb3f4fa50 Merge pull request #5353 from mdmontesinos/fix-cookieconsent
fix #5352: remove requests to cookie consent service when not enabled
2025-06-09 15:57:59 -04:00
ff450ca43a Fix for Scheduled Jobs UI #5354
This PR addresses an issue where null date/time values could cause exceptions when processing job scheduling.
Changes Made:
- Added proper null checks for _startDate, _startTime, _endDate, _endTime, _nextDate, and _nextTime
- Improved parsing safety for _retentionHistory using int.TryParse()
- Added validation to fail early with meaningful error messages

Impact:

Prevents NullReferenceException and InvalidOperationException when date/time fields are missing
2025-06-09 10:29:43 +02:00
d4f0805108 fix #5352: remove requests to cookie consent service when not enabled 2025-06-06 10:05:40 +02:00
64ce69d1c7 Merge pull request #5351 from sbwalker/dev
stop gap fix to mitigate date conversion exceptions on WebAssembly
2025-06-05 10:38:51 -04:00
ca3cb48091 Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2025-06-05 10:37:31 -04:00
85085bf4c7 stop gap fix to mitigate date conversion exceptions on WebAssembly 2025-06-05 10:37:25 -04:00
873af6b598 Merge pull request #5349 from leigh-pointer/References
Server References Updated
2025-06-05 09:32:34 -04:00
c423895f31 Merge pull request #5350 from sbwalker/dev
rendering optimizations
2025-06-05 09:32:12 -04:00
4418e27c29 rendering optimizations 2025-06-05 09:31:54 -04:00
f776977af8 Server References Updated
update SixLabors.ImageSharp
update Swashbuckle.AspNetCore
2025-06-04 13:28:49 +02:00
c13ce3d0f1 Update Index.razor
Deprecated .text-muted will be replaced by .text-body-secondary in v6.
2025-06-03 15:24:43 +02:00
2c4c669ea2 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-30 16:06:19 +02:00
29fe3dfd0b Merge pull request #5344 from sbwalker/dev
update Azure ARM template to 6.1.3
2025-05-29 17:05:04 -04:00
985e50d415 update Azure ARM template to 6.1.3 2025-05-29 17:04:45 -04:00
11150b6a10 Update README.md 2025-05-29 17:03:20 -04:00
c499acdc4a Merge pull request #5342 from oqtane/master
Merge pull request #5341 from oqtane/dev
2025-05-29 15:59:32 -04:00
b24e3252d9 Merge pull request #5341 from oqtane/dev
6.1.3 Release
2025-05-29 15:58:55 -04:00
e15787b1e4 Merge pull request #5340 from sbwalker/dev
change id for header/footer
2025-05-29 15:04:28 -04:00
d5f19d97e2 change id for header/footer 2025-05-29 15:04:12 -04:00
5543a4aeed Merge pull request #5339 from sbwalker/dev
fix #5329 - clear Options after updating User Settings
2025-05-29 11:53:29 -04:00
9c333232e2 fix #5329 - clear Options after updating User Settings 2025-05-29 11:53:14 -04:00
d52b95ea23 Merge pull request #5333 from leigh-pointer/TokenReplace
Fix for ModuleBase ReplaceTokens #5332
2025-05-28 13:11:28 -04:00
ef4fbcbb8a Update ModuleBase.cs
This method replaces all tokens in the format [Object:Property] or [Object:SubObject:Property] within a string.
Efficient string parsing and reflection ensure flexibility with performance.
It supports deeply nested properties, optional default fallback values (e.g. [PageState:User:Email|default@email.com]), and uses caching to optimize repeated token resolution without regex.
2025-05-28 17:30:19 +02:00
aa454b411f Merge pull request #5335 from thabaum/Update-Swashbuckle.AspNetCore-Package-Dependency-to-8.1.2
Fixes #5331: Updates Swashbuckle.AspNetCore Package Dependency to 8.1.2
2025-05-28 08:23:39 -04:00
543e9339c7 Update Swashbuckle.AspNetCore Package Dependency to 8.1.2 2025-05-25 09:34:09 -07:00
7fff5c0d18 Fix for ModuleBase ReplaceTokens #5332
Replaced the ReplaceTokens logic to replace all tokens in the string
2025-05-25 10:55:49 +02:00
fa79f3f6fa Merge pull request #5326 from sbwalker/dev
fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase
2025-05-19 21:01:02 -07:00
c098839881 fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase 2025-05-19 21:00:35 -07:00
8d0d88c1b9 Merge pull request #5325 from sbwalker/dev
ensure Content folder is empty when packaging
2025-05-19 18:32:42 -04:00
2b6ba0f410 ensure Content folder is empty when packaging 2025-05-19 15:32:27 -07:00
11235009c0 Merge pull request #5324 from sbwalker/dev
imprvoe help text
2025-05-19 18:15:03 -04:00
4b05f7fdad imprvoe help text 2025-05-19 15:14:49 -07:00
338b0ae509 Merge pull request #5320 from sbwalker/dev
use consistent authorization method
2025-05-16 12:11:16 -04:00
a437082952 use consistent authorization method 2025-05-16 12:11:03 -04:00
ca9aba7b3b Merge pull request #5319 from sbwalker/dev
improve comment
2025-05-16 11:53:17 -04:00
fe9f189734 improve comment 2025-05-16 11:53:04 -04:00
018ac612f4 Merge pull request #5318 from sbwalker/dev
improve messaging
2025-05-16 11:47:06 -04:00
5bde40ec2b improve messaging 2025-05-16 11:46:53 -04:00
4d5780c192 Merge pull request #5317 from sbwalker/dev
Fix #4789 - allow user email verification to be managed by administrator
2025-05-16 11:13:20 -04:00
ff6a810ad5 Fix #4789 - allow user email verification to be managed by administrator 2025-05-16 11:13:03 -04:00
feec01ba00 Merge pull request #5316 from sbwalker/dev
fix spelling mistake
2025-05-16 09:40:10 -04:00
1f05d12ef5 fix spelling mistake 2025-05-16 09:39:57 -04:00
31aba14507 Merge pull request #5315 from sbwalker/dev
fix initialization issue related to time zones
2025-05-16 09:09:21 -04:00
bbd6f13f36 fix initialization issue related to time zones 2025-05-16 09:09:07 -04:00
68edbbbdb9 Merge pull request #5314 from sbwalker/dev
improve filename validation in module content export
2025-05-16 08:26:04 -04:00
eb5a0dc1c9 improve filename validation in module content export 2025-05-16 08:25:50 -04:00
7cea4f1792 Merge pull request #5312 from sbwalker/dev
update module export resource info
2025-05-15 11:06:20 -04:00
c57c6abb1b update module export resource info 2025-05-15 11:06:04 -04:00
a25b706c7b Merge pull request #5311 from sbwalker/dev
allow filename to be provided during module export
2025-05-15 10:59:10 -04:00
5d077e843d allow filename to be provided during module export 2025-05-15 10:58:55 -04:00
65bf3e9899 Merge pull request #5310 from sbwalker/dev
allow module import from a file
2025-05-15 09:34:37 -04:00
51ba3a01f5 allow module import from a file 2025-05-15 09:34:19 -04:00
f9ca611b8b Merge pull request #5309 from sbwalker/dev
improve module export so that content can be saved to a file
2025-05-15 08:56:37 -04:00
a49b8728fd improve module export so that content can be saved to a file 2025-05-15 08:56:21 -04:00
018737c42a Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-15 11:52:51 +02:00
9234b91089 Merge pull request #5306 from sbwalker/dev
fix #5200 - sort folders alphabetically, display folders hierarchically
2025-05-14 15:52:04 -04:00
f3fcef52dd fix #5200 - sort folders alphabetically, display folders hierarchically 2025-05-14 15:51:51 -04:00
7f8b741981 Merge pull request #5305 from sbwalker/dev
fix issue with module header/footer
2025-05-14 14:20:54 -04:00
f1791a709c fix issue with module header/footer 2025-05-14 14:20:44 -04:00
30307fb05e Merge pull request #5304 from sbwalker/dev
support for module header and footer content
2025-05-14 12:18:51 -04:00
57d443be8d support for module header and footer content 2025-05-14 12:18:37 -04:00
84844c5043 Merge pull request #5303 from sbwalker/dev
move ConfigureOqtaneAssemblies to occur before UseEndpoints
2025-05-14 11:13:12 -04:00
9000f05961 move ConfigureOqtaneAssemblies to occur before UseEndpoints 2025-05-14 11:12:58 -04:00
6bef9b5c6e Merge pull request #5302 from sbwalker/dev
update default templates to .NET SDK 9.0.5
2025-05-13 16:52:51 -04:00
ffef1f4820 update default templates to .NET SDK 9.0.5 2025-05-13 16:52:39 -04:00
10c7bdbcaa Merge pull request #5301 from sbwalker/dev
upgrade to .NET SDK 9.0.5
2025-05-13 16:49:59 -04:00
e8f9888a41 upgrade to .NET SDK 9.0.5 2025-05-13 16:49:46 -04:00
8bac702be6 Merge pull request #5300 from sbwalker/dev
rollback change which moved ConfigureOqtaneAssemblies
2025-05-13 16:39:08 -04:00
128bcecfe3 rollback change which moved ConfigureOqtaneAssemblies(env); 2025-05-13 16:38:48 -04:00
1390b3c489 Merge pull request #5299 from sbwalker/dev
fix #5398 - editing page permissions
2025-05-13 15:49:30 -04:00
a0f41341ac fix #5398 - editing page permissions 2025-05-13 15:49:16 -04:00
8ffa7ef7ff Merge pull request #5297 from sbwalker/dev
adding time zone support to admin modules
2025-05-13 13:55:15 -04:00
deb4607081 adding time zone support to admin modules 2025-05-13 13:55:01 -04:00
49c62f80e8 Merge pull request #5296 from sbwalker/dev
display local datetimes in the Job Scheduler (using time zones)
2025-05-13 11:29:40 -04:00
139793f3c0 display local datetimes in the Job Scheduler (using time zones) 2025-05-13 11:29:26 -04:00
f237cb9655 Merge pull request #5295 from sbwalker/dev
add time zone support for sites and users
2025-05-13 09:24:33 -04:00
9f18c460d8 add time zone support for sites and users 2025-05-13 09:24:17 -04:00
306a41b442 Merge pull request #5293 from sbwalker/dev
fix #5292 - fix External Login Provider Info link
2025-05-12 11:32:42 -04:00
b53f54295d fix #5292 - fix External Login Provider Info link 2025-05-12 11:32:27 -04:00
7e4f066694 Merge pull request #5291 from sbwalker/dev
fix #5287 - allow deletion of folder which contains files
2025-05-12 08:42:12 -04:00
90d72489d9 fix #5287 - allow deletion of folder which contains files 2025-05-12 08:41:57 -04:00
045c455324 Merge pull request #5286 from sbwalker/dev
update version to 6.1.3
2025-05-08 16:09:12 -04:00
60da903360 update version to 6.1.3 2025-05-08 16:09:00 -04:00
6d2a71f37e Merge pull request #5283 from thabaum/update-6.1.3
Fixes #5267: Updates Project Dependencies and Version to 6.1.3
2025-05-08 16:07:48 -04:00
c8f60a12a4 Merge pull request #5277 from ohba-ikuo/dev
Modify the module's server-side resource path.
2025-05-07 17:27:15 -04:00
11b2d3aa43 Merge pull request #5284 from sbwalker/dev
change Synchronize button to Check For Updates to improve clarity
2025-05-07 17:12:26 -04:00
3d9c81d850 change Synchronize button to Check For Updates to improve clarity 2025-05-07 17:12:14 -04:00
8ccb1a24f8 Update Version to 6.1.3 2025-05-07 10:25:27 -07:00
48c6796128 Update Version to 6.1.3 2025-05-07 10:24:36 -07:00
fc403f920b Update Version to 6.1.3 2025-05-07 10:24:13 -07:00
ca19496b5c Update Version to 6.1.3 2025-05-07 10:23:32 -07:00
0ae38f8a40 Update Version to 6.1.3 2025-05-07 10:23:11 -07:00
ad868ba841 Update Version to 6.1.3 2025-05-07 10:22:47 -07:00
f2aa39aa85 Update Version to 6.1.3 2025-05-07 10:22:04 -07:00
e76e0fc351 Update Version to 6.1.3 2025-05-07 10:20:52 -07:00
a348913888 Update Version to 6.1.3 2025-05-07 10:20:15 -07:00
5507006c53 Update Version to 6.1.3 2025-05-07 10:19:05 -07:00
994429f098 Update Version to 6.1.3 2025-05-07 10:18:40 -07:00
8efdcb9c49 Update Version to 6.1.3 2025-05-07 10:17:54 -07:00
db9a40db2b Update Version to 6.1.3 2025-05-07 10:17:17 -07:00
25667499e6 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:16:38 -07:00
a728cd2d91 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:14:12 -07:00
3f5f3ef10b Update Version to 6.1.3 2025-05-07 10:10:58 -07:00
3811b8f0c0 Theme Template updated 2025-05-07 11:46:07 +02:00
0d708124c2 Undo Oqtane.Server.csproj 2025-05-07 08:31:34 +09:00
d31f73df14 Merge branch 'oqtane:dev' into dev 2025-05-07 06:20:48 +09:00
3fed45438b Merge pull request #5281 from thabaum/HtmlText.cs-Unused-Namespaces
Fixes #5280: Remove Unused Namespaces from HtmlText.cs in HtmlText Module
2025-05-06 16:50:10 -04:00
8aa967fa1b Remove Unnecessary Namespaces
Removes Unnecessary Namespaces 'System' and 'System.ComponentModel.DataAnnotations.Schema;'.
2025-05-06 13:29:26 -07:00
6eaa3e342c Merge branch 'dev' of https://github.com/ohba-ikuo/oqtane.framework into dev 2025-05-02 23:45:18 +09:00
6fc9e60f62 fixed serverside resource path 2025-05-02 23:45:13 +09:00
d81514e9be Update for Blazor Theme 2025-05-02 12:19:58 +02:00
14b0d7abf0 Updated to Bootstrap 5
Updated to Bootstrap 5.3.5
Update bootswatch Cyborg to 5.3.5 using https://cdn.jsdelivr.net because it is not available at https://cdnjs.com/libraries
2025-05-02 12:16:55 +02:00
c39ffcf51c Merge pull request #5275 from sbwalker/dev
add new Register Url and Profile Url options to User Management / Settings
2025-05-01 23:33:10 -04:00
6f60a91f4c add new Register Url and Profile Url options to User Management / Settings 2025-05-01 23:32:37 -04:00
8031df6f28 Merge pull request #5274 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:35:11 -04:00
da1e859fda use new GetSettingValue() method 2025-04-30 14:34:54 -04:00
8d4d25f1d1 Merge pull request #5273 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:18:44 -04:00
6aff27778d use new GetSettingValue() method 2025-04-30 14:18:29 -04:00
ca2dcbfec0 Merge pull request #5272 from sbwalker/dev
remove unecessary using statment
2025-04-30 13:55:24 -04:00
24b666a382 remove unecessary using statment 2025-04-30 13:55:11 -04:00
9e34295529 Merge pull request #5270 from leigh-pointer/ModBase
GetUrlParameters crash
2025-04-30 13:47:21 -04:00
10c55d056b Merge pull request #5271 from sbwalker/dev
resolve issue with host setting overrides
2025-04-30 13:47:06 -04:00
753ab3bdd7 resolve issue with host setting overrides 2025-04-30 13:46:52 -04:00
feee8def6f GetUrlParameters crash
The _urlparametersstate variable is not initialized so in GetUrlParameters it causes a crash
2025-04-29 09:50:15 +02:00
c208f12f8c Merge pull request #5266 from sbwalker/dev
add a convenience method to get a setting value server-side
2025-04-28 12:43:04 -04:00
dc926bf838 add a convenience method to get a setting value server-side 2025-04-28 12:42:50 -04:00
55a76b5204 Merge pull request #5265 from sbwalker/dev
fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints
2025-04-28 12:35:49 -04:00
53b837e763 fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints 2025-04-28 12:34:48 -04:00
3a551cdf25 Merge pull request #5257 from leigh-pointer/AddMDfiles
Updated UploadableFiles constant
2025-04-22 17:00:03 -04:00
c0c7f87dc9 Merge pull request #5256 from leigh-pointer/Packages
Packages updated
2025-04-22 16:59:49 -04:00
ac77fd138b Updated UploadableFiles constant
Updated UploadableFiles constant to allow for Markdown files to be uploaded.
2025-04-22 10:08:20 +02:00
30d6e9d67c Merge branch 'dev' into Packages 2025-04-22 09:13:15 +02:00
47db4334a1 Packages updated
MySql.Data to 9.3.0
HtmlAgilityPack to 1.12.1
washbuckle.AspNetCore to 8.1.1
2025-04-22 09:11:53 +02:00
fce97179e1 Merge pull request #5255 from sbwalker/dev
url mapping improvements
2025-04-21 15:14:52 -04:00
4f16cd2d01 url mapping improvements 2025-04-21 15:14:39 -04:00
fa587691b1 Merge pull request #5254 from sbwalker/dev
improve validation in Url Mapping
2025-04-21 14:14:03 -04:00
e0044658f9 improve validation in Url Mapping 2025-04-21 14:13:49 -04:00
53de1ddb36 Merge pull request #5253 from mdmontesinos/files-optimization
Files server optimization
2025-04-21 13:57:41 -04:00
da7b046092 Remove extra using 2025-04-21 18:47:34 +02:00
d888d83a98 Simplify files etag calculation 2025-04-21 18:44:16 +02:00
430f83e8e9 Only compute hash when file has query string 2025-04-21 16:46:55 +02:00
e7acd14faa Replace MD5 hash with a longer simple hash 2025-04-21 15:51:25 +02:00
1b00fa74bc Compute file server etag with MD5 and always include ModifiedOn 2025-04-21 11:14:24 +02:00
4d572d8173 Allow earlier return in files server 2025-04-21 10:48:48 +02:00
dbda85d8d9 Merge pull request #5248 from sbwalker/dev
UX improvements for System Update
2025-04-15 09:20:35 -04:00
95cb5dd66c UX improvements for System Update 2025-04-15 09:20:18 -04:00
f64e1c3a6a Merge pull request #5245 from sbwalker/dev
.NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9
2025-04-11 15:54:17 -04:00
26a686c412 .NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9 2025-04-11 15:54:02 -04:00
2505383f53 Merge pull request #5244 from leigh-pointer/Swash
Swagger Updated to latest
2025-04-11 09:03:29 -04:00
1a1e9ac6be Swagger Updated to latest
Swashbuckle.AspNetCore update from 8.1.0 > 8.1.1
2025-04-11 09:24:18 +02:00
7f20a3179e Merge pull request #5241 from sbwalker/dev
fix issue with new UserProfile parameters
2025-04-10 14:36:57 -04:00
46431f0187 fix issue with new UserProfile parameters 2025-04-10 14:36:42 -04:00
1ff8ec78c9 Merge pull request #5240 from sbwalker/dev
backup parameter needs to be backward compatible
2025-04-10 12:27:39 -04:00
7840230c62 backup parameter needs to be backward compatible 2025-04-10 12:27:21 -04:00
523db0a005 Merge pull request #5239 from sbwalker/dev
update Deploy To Azure to the 6.1.2 release
2025-04-10 12:04:40 -04:00
cc906d49ba update Deploy To Azure to the 6.1.2 release 2025-04-10 12:04:26 -04:00
fc23af89d3 Update README.md 2025-04-10 11:54:58 -04:00
5d8829ba63 Merge pull request #5238 from oqtane/master
6.1.2 Release
2025-04-10 11:48:58 -04:00
31ccd80894 Merge pull request #5237 from oqtane/dev
6.1.2 Release
2025-04-10 11:48:36 -04:00
bac2234616 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg 2025-04-10 07:49:01 -04:00
bd61db76a3 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg 2025-04-10 07:48:52 -04:00
bc99e3b992 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg 2025-04-10 07:48:42 -04:00
b7314b0813 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg 2025-04-10 07:48:31 -04:00
4759bd569f Merge pull request #5235 from sbwalker/dev
fix incorrect path in theme template
2025-04-09 17:21:36 -04:00
b88c28f864 fix incorrect path in theme template 2025-04-09 17:21:23 -04:00
774ccb05f8 Merge pull request #5234 from sbwalker/dev
removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712)
2025-04-09 14:58:56 -04:00
0ac48cba34 removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712) 2025-04-09 14:58:30 -04:00
e36880fe3a Merge pull request #5233 from sbwalker/dev
prepare for 6.1.2 release
2025-04-09 11:46:30 -04:00
713cf5de2c prepare for 6.1.2 release 2025-04-09 11:46:16 -04:00
0fa336411f Merge pull request #5232 from sbwalker/dev
update to .NET 9.0.4
2025-04-09 11:41:08 -04:00
8ebdb09d68 update to .NET 9.0.4 2025-04-09 11:40:54 -04:00
40bc53001e Merge pull request #5231 from sbwalker/dev
improve sitemap detection in robots.txt
2025-04-09 11:26:53 -04:00
b1656d1eea improve sitemap detection in robots.txt 2025-04-09 11:26:33 -04:00
7aa54bf979 Merge pull request #5230 from sbwalker/dev
resolve issue with host role support  in external login
2025-04-09 10:55:32 -04:00
231f9bca84 resolve issue with host role support in external login 2025-04-09 10:55:16 -04:00
e4f8596c19 Merge pull request #5227 from sbwalker/dev
fix #5223 - allow robots.txt to be customized for each site
2025-04-08 09:23:35 -04:00
020b7233d0 fix #5223 - allow robots.txt to be customized for each site 2025-04-08 09:23:22 -04:00
85fc0b3e2f Merge pull request #5226 from sbwalker/dev
optimize the System Update process
2025-04-07 13:19:07 -04:00
5dcc7c14f3 optimize the System Update process 2025-04-07 13:18:52 -04:00
7993d27b11 Merge pull request #5224 from sbwalker/dev
added new Azure SQL database provider
2025-04-07 10:48:20 -04:00
1f8c54ce74 added new Azure SQL database provider 2025-04-07 10:48:02 -04:00
73a414a34b Merge pull request #5221 from sbwalker/dev
improve help text
2025-04-02 09:47:31 -04:00
8fa19c4a51 improve help text 2025-04-02 09:47:16 -04:00
0667ae3e15 Merge pull request #5220 from sbwalker/dev
update help text
2025-04-02 09:36:49 -04:00
db1d00cd07 update help text 2025-04-02 09:36:32 -04:00
b27f092bef Merge pull request #5219 from sbwalker/dev
use dynamic .NET major version value
2025-04-01 15:29:26 -04:00
4eaea8e586 use dynamic .NET major version value 2025-04-01 15:29:10 -04:00
89cd7d3bbb Update README.md 2025-04-01 14:30:49 -04:00
2fff1d8d21 Merge pull request #5218 from sbwalker/dev
removing connection string section
2025-04-01 13:55:09 -04:00
850631f00e removing connection string section 2025-04-01 13:54:39 -04:00
1cea8846cf Merge pull request #5217 from sbwalker/dev
fix azuredeploy
2025-04-01 11:04:02 -04:00
af48a48559 fix azuredeploy 2025-04-01 11:03:46 -04:00
655c1762aa Merge pull request #5216 from sbwalker/dev
azuredeploy changes to use ZIP Deploy
2025-04-01 10:56:47 -04:00
f706ccfd87 new version using ZIP Deploy 2025-04-01 10:56:04 -04:00
71e4c7f117 Merge pull request #5214 from sbwalker/dev
remove .deployment file as we are deploying from a package rather than from source code
2025-03-31 16:22:08 -04:00
ad6182f4bd remove .deployment file as we are deploying from a package rather than from source code 2025-03-31 16:21:45 -04:00
86bf0f65b0 Merge pull request #5213 from sbwalker/dev
modifications to use WEBSITE_RUN_FROM_PACKAGE
2025-03-31 16:06:24 -04:00
7742f7747d modifications to use WEBSITE_RUN_FROM_PACKAGE 2025-03-31 16:06:08 -04:00
eb998c41f2 Merge pull request #5212 from sbwalker/dev
fix #4929 deploy to azure
2025-03-31 13:59:12 -04:00
657bd7c97c fix #4929 deploy to azure 2025-03-31 13:58:03 -04:00
c8286148c1 Merge pull request #5211 from sbwalker/dev
fix #5194 - improve performance of retrieving scheduled job logs
2025-03-31 13:16:52 -04:00
e6ba2cce62 fix #5194 - improve performance of retrieving scheduled job logs 2025-03-31 13:16:35 -04:00
6105ff44b4 Merge pull request #5210 from sbwalker/dev
fix #5207 add support for username and displayname in permissions grid
2025-03-31 10:04:43 -04:00
72da77be01 fix #5207 add support for username and displayname in permissions grid 2025-03-31 10:04:26 -04:00
4c29b31f1b Merge pull request #5209 from sbwalker/dev
delete files before deleting folder
2025-03-31 08:58:39 -04:00
6e640108ed delete files before deleting folder 2025-03-31 08:58:23 -04:00
157322441d Merge pull request #5206 from zyhfish/task/fix-5205
Fix #5205: specific the date time as UTC kind.
2025-03-31 08:40:28 -04:00
61d967e6af Merge pull request #5208 from sbwalker/dev
allow custom urls in UserProfile component
2025-03-31 08:38:46 -04:00
99f2158e55 allow custom urls in UserProfile component 2025-03-31 08:38:30 -04:00
Ben
1cba78cc4e Fix #5205: specific the date time as UTC kind. 2025-03-29 09:29:49 +08:00
1770c1ee11 Merge pull request #5201 from sbwalker/dev
include external login support for host role
2025-03-26 17:11:52 -04:00
a57fbea0cc include external login support for host role 2025-03-26 17:11:29 -04:00
f0c27c83f1 Merge pull request #5197 from zyhfish/task/clean-build
suppress build warnings.
2025-03-26 11:16:47 -04:00
Ben
7873ca564c supress build warnings. 2025-03-26 08:12:10 +08:00
5ebc1fec24 Merge pull request #5195 from zyhfish/task/fix-5191
Fix #5191: trigger event when folder changed.
2025-03-25 19:46:24 -04:00
f2559b7d4d Merge pull request #5196 from sbwalker/dev
fix #5193 - prevent scheduled jobs from blocking startup
2025-03-25 19:07:24 -04:00
1ee92a248e fix #5193 - prevent scheduled jobs from blocking startup 2025-03-25 19:07:01 -04:00
Ben
8376f98f21 Fix #5191: trigger event when folder changed. 2025-03-25 22:23:51 +08:00
810a3e0171 Merge pull request #5188 from sbwalker/dev
prevent stylesheet resources from being duplicated
2025-03-21 17:34:21 -04:00
2eac9c3795 prevent stylesheet resources from being duplicated 2025-03-21 17:34:07 -04:00
75f2425668 Merge pull request #5187 from sbwalker/dev
remove unnecessary using statements
2025-03-21 10:10:32 -04:00
2dd1d7e926 remove unnecessary using statements 2025-03-21 10:10:19 -04:00
5bb05a0a51 Merge pull request #5185 from sbwalker/dev
fix page order for new Privacy and Terms pages
2025-03-21 08:19:26 -04:00
bc2c5b00c6 fix page order for new Privacy and Terms pages 2025-03-21 08:19:09 -04:00
09f5e158dd Merge pull request #5181 from sbwalker/dev
add ability to Synchronize local modules and themes with Marketplace
2025-03-19 14:37:50 -04:00
4656471a0a add ability to Synchronize local modules and themes with Marketplace 2025-03-19 14:37:36 -04:00
69d58a4273 Merge pull request #5179 from leigh-pointer/SwashbuckleUpdate8
Swashbuckle Update
2025-03-19 08:42:40 -04:00
53a27677d4 Swashbuckle Update
Update Swashbuckle  to version 8.0
2025-03-19 12:07:15 +01:00
f243ad0348 Merge pull request #5178 from sbwalker/dev
add caching support for ImageUrl
2025-03-18 14:29:23 -04:00
b4ce6bbb42 add caching support for ImageUrl 2025-03-18 14:29:09 -04:00
fa32937045 Merge pull request #5177 from sbwalker/dev
MySql.Data is still required for raw query operations
2025-03-18 09:24:53 -04:00
812e5f3c8e MySql.Data is still required for raw query operations 2025-03-18 09:24:39 -04:00
e2981e802c Merge pull request #5176 from sbwalker/dev
fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency
2025-03-18 09:08:07 -04:00
4ae4705c73 fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency 2025-03-18 09:07:23 -04:00
fbf4b12713 Merge pull request #5168 from zyhfish/task/fix-cookie-consent-layout
adjust the cookie consent layout in small screen.
2025-03-17 13:45:14 -04:00
e4ece3e0dc Merge pull request #5174 from sbwalker/dev
notifications should only convert line breaks to HTML for plain text messages
2025-03-17 13:45:02 -04:00
9f231421be notifications should only convert line breaks to HTML for plain text messages 2025-03-17 13:44:40 -04:00
Ben
b4fdbb5e48 adjust the layout in small screen. 2025-03-13 21:54:43 +08:00
Ben
fbf62ca30d adjust the cookie consent layout in small screen. 2025-03-13 20:54:50 +08:00
484 changed files with 13604 additions and 5928 deletions

View File

@ -1,2 +0,0 @@
[config]
project = Oqtane.Server/Oqtane.Server.csproj

2
.gitignore vendored
View File

@ -35,4 +35,4 @@ Oqtane.Server/wwwroot/Themes/*
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.* !Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
!Oqtane.Server/wwwroot/Themes/Templates !Oqtane.Server/wwwroot/Themes/Templates
Oqtane.Server/wwwroot/Themes/Templates/* Oqtane.Server/wwwroot/Themes/Templates/*
Oqtane.Server/wwwroot/Themes/Templates/External !Oqtane.Server/wwwroot/Themes/Templates/External

17
Directory.Build.props Normal file
View File

@ -0,0 +1,17 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.2.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
<Description>CMS and Application Framework for Blazor and .NET MAUI</Description>
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
</PropertyGroup>
</Project>

9
Oqtane.Application/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.vs/
bin/
obj/
*.user
artifacts/
msbuild.binlog
.vscode/
*.binlog
*.nupkg

View File

@ -0,0 +1,86 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Shaun Walker",
"classifications": [
"Web",
"ASP.NET",
"Blazor",
"Oqtane"
],
"name": "Oqtane Application Template",
"shortName": "oqtane-app",
"defaultName": "MyCompany.MyProject",
"identity": "Oqtane.Application.Template",
"tags": {
"language": "C#",
"type": "solution",
"editorTreatAs":"solution"
},
"sourceName": "Oqtane.Application",
"preferNameDirectory": true,
"guids": [
"04B05448-788F-433D-92C0-FED35122D45A",
"AA8E58A1-CD09-4208-BF66-A8BB341FD669",
"18D73F73-D7BE-4388-85BA-FBD9AC96FCA2"
],
"symbols": {
"Framework": {
"type": "parameter",
"description": "The target framework for the project",
"datatype": "choice",
"choices": [
{
"choice": "net9.0",
"description": "Target net9.0"
}
],
"replaces": "net9.0",
"defaultValue": "net9.0"
},
"HttpPort": {
"type": "parameter",
"datatype": "integer",
"description": "Port number to use for the HTTP endpoint in launchSettings.json."
},
"HttpPortGenerated": {
"type": "generated",
"generator": "port"
},
"HttpPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpPort",
"fallbackVariableName": "HttpPortGenerated"
},
"replaces": "44358"
},
"HttpsPort": {
"type": "parameter",
"datatype": "integer",
"description": "Port number to use for the HTTPS endpoint in launchSettings.json."
},
"HttpsPortGenerated": {
"type": "generated",
"generator": "port",
"parameters": {
"low": 44300,
"high": 44399
}
},
"HttpsPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpsPort",
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44359"
}
},
"primaryOutputs": [
{
"path": "Oqtane.Application.sln"
}
]
}

View File

@ -0,0 +1,3 @@
using Microsoft.Extensions.Localization;
[assembly: RootNamespace("Oqtane.Application.Client")]

View File

@ -0,0 +1,15 @@
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Oqtane.Application
{
public class Interop
{
private readonly IJSRuntime _jsRuntime;
public Interop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
}
}

View File

@ -0,0 +1,112 @@
@using Oqtane.Modules.Controls
@using Oqtane.Application.Services
@using Oqtane.Application.Models
@namespace Oqtane.Application.MyModule
@inherits ModuleBase
@inject IMyModuleService MyModuleService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
<form @ref="form" class="@(validated ? " was-validated" : "needs-validation" )" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter a name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Save">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br /><br />
@if (PageState.Action == "Edit")
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
}
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit";
public override string Title => "Manage MyModule";
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet(ModulePath() + "Module.css")
};
private ElementReference form;
private bool validated = false;
private int _id;
private string _name;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
protected override async Task OnInitializedAsync()
{
try
{
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);
MyModule MyModule = await MyModuleService.GetMyModuleAsync(_id, ModuleState.ModuleId);
if (MyModule != null)
{
_name = MyModule.Name;
_createdby = MyModule.CreatedBy;
_createdon = MyModule.CreatedOn;
_modifiedby = MyModule.ModifiedBy;
_modifiedon = MyModule.ModifiedOn;
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading MyModule {MyModuleId} {Error}", _id, ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Save()
{
try
{
validated = true;
var interop = new Oqtane.UI.Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (PageState.Action == "Add")
{
MyModule MyModule = new MyModule();
MyModule.ModuleId = ModuleState.ModuleId;
MyModule.Name = _name;
MyModule = await MyModuleService.AddMyModuleAsync(MyModule);
await logger.LogInformation("MyModule Added {MyModule}", MyModule);
}
else
{
MyModule MyModule = await MyModuleService.GetMyModuleAsync(_id, ModuleState.ModuleId);
MyModule.Name = _name;
await MyModuleService.UpdateMyModuleAsync(MyModule);
await logger.LogInformation("MyModule Updated {MyModule}", MyModule);
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.SaveValidation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving MyModule {Error}", ex.Message);
AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error);
}
}
}

View File

@ -0,0 +1,77 @@
@using Oqtane.Application.Services
@using Oqtane.Application.Models
@namespace Oqtane.Application.MyModule
@inherits ModuleBase
@inject IMyModuleService MyModuleService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer
@if (_MyModules == null)
{
<p><em>Loading...</em></p>
}
else
{
<ActionLink Action="Add" Security="SecurityAccessLevel.Edit" Text="Add MyModule" ResourceKey="Add" />
<br />
<br />
@if (@_MyModules.Count != 0)
{
<Pager Items="@_MyModules">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.MyModuleId.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete MyModule" Message="Are You Sure You Wish To Delete This MyModule?" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" ResourceKey="Delete" Id="@context.MyModuleId.ToString()" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<p>@Localizer["Message.DisplayNone"]</p>
}
}
@code {
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet(ModulePath() + "Module.css"),
new Script(ModulePath() + "Module.js")
};
List<Models.MyModule> _MyModules;
protected override async Task OnInitializedAsync()
{
try
{
_MyModules = await MyModuleService.GetMyModulesAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading MyModule {Error}", ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Delete(MyModule MyModule)
{
try
{
await MyModuleService.DeleteMyModuleAsync(MyModule.MyModuleId, ModuleState.ModuleId);
await logger.LogInformation("MyModule Deleted {MyModule}", MyModule);
_MyModules = await MyModuleService.GetMyModulesAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting MyModule {MyModule} {Error}", MyModule, ex.Message);
AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error);
}
}
}

View File

@ -0,0 +1,19 @@
using Oqtane.Models;
using Oqtane.Modules;
namespace Oqtane.Application.MyModule
{
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "MyModule",
Description = "Example module",
Version = "1.0.0",
ServerManagerType = "Oqtane.Application.Manager.MyModuleManager, Oqtane.Application.Server.Oqtane",
ReleaseVersions = "1.0.0",
Dependencies = "Oqtane.Application.Shared.Oqtane",
PackageName = "Oqtane.Application"
};
}
}

View File

@ -0,0 +1,47 @@
@namespace Oqtane.Application.MyModule
@inherits ModuleBase
@inject ISettingService SettingService
@inject IStringLocalizer<Settings> Localizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="value" HelpText="Enter a value" ResourceKey="SettingName" ResourceType="@resourceType">Name: </Label>
<div class="col-sm-9">
<input id="value" type="text" class="form-control" @bind="@_value" />
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Application.MyModule.Settings, Oqtane.Application.Client.Oqtane"; // for localization
public override string Title => "MyModdule Settings";
string _value;
protected override async Task OnInitializedAsync()
{
try
{
Dictionary<string, string> settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
_value = SettingService.GetSetting(settings, "SettingName", "");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
Dictionary<string, string> settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "SettingName", _value);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace Oqtane.Application.Client
{
internal class Program
{
static async Task Main(string[] args)
{
// defer client startup to Oqtane - do not modify
await Oqtane.Client.Program.Main(args);
}
}
}

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name.Text" xml:space="preserve">
<value>Name: </value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Message.SaveValidation" xml:space="preserve">
<value>Please Provide All Required Information</value>
</data>
<data name="Message.SaveError" xml:space="preserve">
<value>Error Saving MyModule</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading MyModule</value>
</data>
</root>

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Add.Text" xml:space="preserve">
<value>Add MyModule</value>
</data>
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Delete.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Delete.Header" xml:space="preserve">
<value>Delete MyModule</value>
</data>
<data name="Delete.Message" xml:space="preserve">
<value>Are You Sure You Wish To Delete This MyModule?</value>
</data>
<data name="Message.DisplayNone" xml:space="preserve">
<value>No MyModules To Display</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading MyModule</value>
</data>
<data name="Message.DeleteError" xml:space="preserve">
<value>Error Deleting MyModule</value>
</data>
</root>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingName.Text" xml:space="preserve">
<value>Name: </value>
</data>
<data name="SettingName.HelpText" xml:space="preserve">
<value>Enter a value</value>
</data>
</root>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title.HelpText" xml:space="preserve">
<value>Specify If The Module Title Should Be Displayed</value>
</data>
<data name="Title.Text" xml:space="preserve">
<value>Display Title</value>
</data>
</root>

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Login.HelpText" xml:space="preserve">
<value>Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.</value>
</data>
<data name="Login.Text" xml:space="preserve">
<value>Show Login?</value>
</data>
<data name="Register.HelpText" xml:space="preserve">
<value>Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.</value>
</data>
<data name="Register.Text" xml:space="preserve">
<value>Show Register?</value>
</data>
<data name="Scope.HelpText" xml:space="preserve">
<value>Specify if the settings are applicable to this page or the entire site.</value>
</data>
<data name="Scope.Text" xml:space="preserve">
<value>Setting Scope:</value>
</data>
</root>

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Application.Services
{
public interface IMyModuleService
{
Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId);
Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId);
Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule);
Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule);
Task DeleteMyModuleAsync(int MyModuleId, int ModuleId);
}
public class MyModuleService : ServiceBase, IMyModuleService
{
public MyModuleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("MyModule");
public async Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId)
{
List<Models.MyModule> Tasks = await GetJsonAsync<List<Models.MyModule>>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty<Models.MyModule>().ToList());
return Tasks.OrderBy(item => item.Name).ToList();
}
public async Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId)
{
return await GetJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModuleId}/{ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule)
{
return await PostJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}", EntityNames.Module, MyModule.ModuleId), MyModule);
}
public async Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule)
{
return await PutJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModule.MyModuleId}", EntityNames.Module, MyModule.ModuleId), MyModule);
}
public async Task DeleteMyModuleAsync(int MyModuleId, int ModuleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModuleId}/{ModuleId}", EntityNames.Module, ModuleId));
}
}
}

View File

@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Oqtane.Services;
using Oqtane.Application.Services;
namespace Oqtane.Application.Startup
{
public class ClientStartup : IClientStartup
{
public void ConfigureServices(IServiceCollection services)
{
if (!services.Any(s => s.ServiceType == typeof(IMyModuleService)))
{
services.AddScoped<IMyModuleService, MyModuleService>();
}
}
}
}

View File

@ -0,0 +1,46 @@
@namespace Oqtane.Application.MyTheme
@inherits ContainerBase
@inject ISettingService SettingService
<div class="@_classes">
@if (_title && ModuleState.Title != "-")
{
<div class="row px-4">
<div class="d-flex flex-nowrap">
<ModuleActions /><h2><ModuleTitle /></h2>
</div>
<hr class="app-rule" />
</div>
}
else
{
<ModuleActions />
}
<div class="row px-4">
<div class="container-fluid">
<ModuleInstance />
</div>
</div>
</div>
@code {
public override string Name => "Container";
private bool _title = true;
private string _classes = "container-fluid";
protected override void OnParametersSet()
{
try
{
_title = bool.Parse(SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true"));
}
catch
{
// error loading container settings
}
}
}

View File

@ -0,0 +1,50 @@
@namespace Oqtane.Application.MyTheme
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@attribute [OqtaneIgnore]
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" ResourceKey="Title" ResourceType="@resourceType" HelpText="Specify If The Module Title Should Be Displayed">Display Title?</Label>
<div class="col-sm-9">
<select id="title" class="form-select" @bind="@_title">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Application.MyTheme.ContainerSettings, Oqtane.Application.Client.Oqtane"; // for localization
private string _title = "true";
protected override void OnInitialized()
{
try
{
_title = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Title", _title);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using Oqtane.Models;
using Oqtane.Themes;
using Oqtane.Shared;
namespace Oqtane.Application.MyTheme
{
public class ThemeInfo : ITheme
{
public Oqtane.Models.Theme Theme => new Oqtane.Models.Theme
{
Name = "MyTheme",
Version = "1.0.0",
PackageName = "Oqtane.Application",
ThemeSettingsType = "Oqtane.Application.MyTheme.ThemeSettings, Oqtane.Application.Client.Oqtane",
ContainerSettingsType = "Oqtane.Application.MyTheme.ContainerSettings, Oqtane.Application.Client.Oqtane",
Resources = new List<Resource>()
{
new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}
};
}
}

View File

@ -0,0 +1,118 @@
@namespace Oqtane.Application.MyTheme
@inherits ThemeBase
@inject ISettingService SettingService
<main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto">
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel ButtonClass="btn-outline-light" /></div>
</div>
</nav>
<div class="content">
<div class="container">
<div class="row">
<div class="col-md-12">
<Pane Name="@PaneNames.Admin" />
</div>
</div>
</div>
<Pane Name="Top Full Width" />
<div class="container">
<div class="row">
<div class="col-md-12">
<Pane Name="Top 100%" />
</div>
</div>
<div class="row">
<div class="col-md-6">
<Pane Name="Left 50%" />
</div>
<div class="col-md-6">
<Pane Name="Right 50%" />
</div>
</div>
<div class="row">
<div class="col-md-4">
<Pane Name="Left 33%" />
</div>
<div class="col-md-4">
<Pane Name="Center 33%" />
</div>
<div class="col-md-4">
<Pane Name="Right 33%" />
</div>
</div>
<div class="row">
<div class="col-md-3">
<Pane Name="Left Outer 25%" />
</div>
<div class="col-md-3">
<Pane Name="Left Inner 25%" />
</div>
<div class="col-md-3">
<Pane Name="Right Inner 25%" />
</div>
<div class="col-md-3">
<Pane Name="Right Outer 25%" />
</div>
</div>
<div class="row">
<div class="col-md-3">
<Pane Name="Left 25%" />
</div>
<div class="col-md-6">
<Pane Name="Center 50%" />
</div>
<div class="col-md-3">
<Pane Name="Right 25%" />
</div>
</div>
<div class="row">
<div class="col-md-8">
<Pane Name="Left Sidebar 66%" />
</div>
<div class="col-md-4">
<Pane Name="Right Sidebar 33%" />
</div>
</div>
<div class="row">
<div class="col-md-4">
<Pane Name="Left Sidebar 33%" />
</div>
<div class="col-md-8">
<Pane Name="Right Sidebar 66%" />
</div>
</div>
<div class="row">
<div class="col-md-12">
<Pane Name="Bottom 100%" />
</div>
</div>
</div>
<Pane Name="Bottom Full Width" />
</div>
</main>
@code {
public override string Name => "MyTheme";
public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width";
private bool _login = true;
private bool _register = true;
protected override void OnParametersSet()
{
try
{
var settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
_login = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true"));
_register = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true"));
}
catch
{
// error loading theme settings
}
}
}

View File

@ -0,0 +1,140 @@
@namespace Oqtane.Application.MyTheme
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@inject IStringLocalizer<ThemeSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@attribute [OqtaneIgnore]
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scope" ResourceKey="Scope" ResourceType="@resourceType" HelpText="Specify if the settings are applicable to this page or the entire site.">Setting Scope:</Label>
<div class="col-sm-9">
<select id="scope" class="form-select" value="@_scope" @onchange="(e => ScopeChanged(e))">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<option value="site">@Localizer["Site"]</option>
}
<option value="page">@Localizer["Page"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="login" ResourceKey="Login" ResourceType="@resourceType" HelpText="Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.">Show Login?</Label>
<div class="col-sm-9">
<select id="login" class="form-select" @bind="@_login">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="register" ResourceKey="Register" ResourceType="@resourceType" HelpText="Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.">Show Register?</Label>
<div class="col-sm-9">
<select id="register" class="form-select" @bind="@_register">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private int pageId = -1;
private string resourceType = "Oqtane.Application.MyTheme.ThemeSettings, Oqtane.Application.Client.Oqtane"; // for localization
private string _scope = "page";
private string _login = "-";
private string _register = "-";
protected override async Task OnInitializedAsync()
{
if (PageState.QueryString.ContainsKey("id"))
{
pageId = int.Parse(PageState.QueryString["id"]);
}
try
{
await LoadSettings();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
private async Task LoadSettings()
{
if (_scope == "site")
{
var settings = PageState.Site.Settings;
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true");
}
else
{
var settings = await SettingService.GetPageSettingsAsync(pageId);
settings = SettingService.MergeSettings(PageState.Site.Settings, settings);
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
}
await Task.Yield();
}
private async Task ScopeChanged(ChangeEventArgs eventArgs)
{
try
{
_scope = (string)eventArgs.Value;
await LoadSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
if (_scope == "site")
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
}
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
}
else
{
var settings = await SettingService.GetPageSettingsAsync(pageId);
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
}
await SettingService.UpdatePageSettingsAsync(settings, pageId);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Settings {Error}", ex.Message);
AddModuleMessage("Error Saving Settings", MessageType.Error);
}
}
}

View File

@ -0,0 +1,25 @@
@using System
@using System.Linq
@using System.Collections.Generic
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Oqtane
@using Oqtane.Models
@using Oqtane.Modules
@using Oqtane.Modules.Controls
@using Oqtane.Providers
@using Oqtane.Security
@using Oqtane.Services
@using Oqtane.Shared
@using Oqtane.Themes
@using Oqtane.Themes.Controls
@using Oqtane.UI
@using Oqtane.Enums
@using Oqtane.Interfaces

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Oqtane.Application.Template</id>
<version>6.2.1</version>
<title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
<icon>icon.png</icon>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<description>Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Static Blazor, Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI).</description>
<language>en-US</language>
<tags>Web ASP.NET Blazor Oqtane Modular Multi-Tenant "Open Source" "SQL Server" MySQL PostgreSQL SQLite</tags>
<readme>README.md</readme>
<packageTypes>
<packageType name="Template" />
</packageTypes>
</metadata>
</package>

View File

@ -0,0 +1,33 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35506.116 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Server", "Server\Oqtane.Application.Server.csproj", "{04B05448-788F-433D-92C0-FED35122D45A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Client", "Client\Oqtane.Application.Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Shared", "Shared\Oqtane.Application.Shared.csproj", "{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.Build.0 = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,22 @@
# Oqtane Application Template
This is a Visual Studio Project Template designed for Oqtane development projects. This template relies on the native templating capabilities of the .NET Command Line Interface (CLI):
```
dotnet new install Oqtane.Application.Template
dotnet new oqtane-app -o MyCompany.MyProject
cd MyCompany.MyProject
dotnet build
cd Server
dotnet run
browse to Url
```
When using this approach you do not need to have a local copy of the oqtane.framework source code - you simply utilize Oqtane as a standard application dependency.
The solution also contains Client, Server, and Shared folders which is where you you would implement your custom functionality. An example module and theme are included for reference, and you can add additional modules and themes within the same projects by following the standard Oqtane folder/namespace conventions.
*Known Issues*
- do not use the term "Oqtane" or "Module" in your output name or else you will experience namespace conflicts

View File

@ -0,0 +1,3 @@
using Microsoft.Extensions.Localization;
[assembly: RootNamespace("Oqtane.Application.Server")]

View File

@ -0,0 +1,114 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Application.Services;
using Oqtane.Controllers;
using System.Net;
using System.Threading.Tasks;
namespace Oqtane.Application.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class MyModuleController : ModuleControllerBase
{
private readonly IMyModuleService _MyModuleService;
public MyModuleController(IMyModuleService MyModuleService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{
_MyModuleService = MyModuleService;
}
// GET: api/<controller>?moduleid=x
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<Models.MyModule>> Get(string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _MyModuleService.GetMyModulesAsync(ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.MyModule> Get(int id, int moduleid)
{
Models.MyModule MyModule = await _MyModuleService.GetMyModuleAsync(id, moduleid);
if (MyModule != null && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
return MyModule;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {MyModuleId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.MyModule> Post([FromBody] Models.MyModule MyModule)
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
MyModule = await _MyModuleService.AddMyModuleAsync(MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Post Attempt {MyModule}", MyModule);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
MyModule = null;
}
return MyModule;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.MyModule> Put(int id, [FromBody] Models.MyModule MyModule)
{
if (ModelState.IsValid && MyModule.MyModuleId == id && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
MyModule = await _MyModuleService.UpdateMyModuleAsync(MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Put Attempt {MyModule}", MyModule);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
MyModule = null;
}
return MyModule;
}
// DELETE api/<controller>/5
[HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task Delete(int id, int moduleid)
{
Models.MyModule MyModule = await _MyModuleService.GetMyModuleAsync(id, moduleid);
if (MyModule != null && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
await _MyModuleService.DeleteMyModuleAsync(id, MyModule.ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized v Delete Attempt {MyModuleId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Oqtane.Modules;
using Oqtane.Models;
using Oqtane.Infrastructure;
using Oqtane.Interfaces;
using Oqtane.Enums;
using Oqtane.Repository;
using Oqtane.Application.Repository;
using System.Threading.Tasks;
namespace Oqtane.Application.Manager
{
public class MyModuleManager : MigratableModuleBase, IInstallable, IPortable, ISearchable
{
private readonly IMyModuleRepository _MyModuleRepository;
private readonly IDBContextDependencies _DBContextDependencies;
public MyModuleManager(IMyModuleRepository MyModuleRepository, IDBContextDependencies DBContextDependencies)
{
_MyModuleRepository = MyModuleRepository;
_DBContextDependencies = DBContextDependencies;
}
public bool Install(Tenant tenant, string version)
{
return Migrate(new Context(_DBContextDependencies), tenant, MigrationType.Up);
}
public bool Uninstall(Tenant tenant)
{
return Migrate(new Context(_DBContextDependencies), tenant, MigrationType.Down);
}
public string ExportModule(Module module)
{
string content = "";
List<Models.MyModule> MyModules = _MyModuleRepository.GetMyModules(module.ModuleId).ToList();
if (MyModules != null)
{
content = JsonSerializer.Serialize(MyModules);
}
return content;
}
public void ImportModule(Module module, string content, string version)
{
List<Models.MyModule> MyModules = null;
if (!string.IsNullOrEmpty(content))
{
MyModules = JsonSerializer.Deserialize<List<Models.MyModule>>(content);
}
if (MyModules != null)
{
foreach(var Task in MyModules)
{
_MyModuleRepository.AddMyModule(new Models.MyModule { ModuleId = module.ModuleId, Name = Task.Name });
}
}
}
public Task<List<SearchContent>> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
{
var searchContentList = new List<SearchContent>();
foreach (var MyModule in _MyModuleRepository.GetMyModules(pageModule.ModuleId))
{
if (MyModule.ModifiedOn >= lastIndexedOn)
{
searchContentList.Add(new SearchContent
{
EntityName = "MyModule",
EntityId = MyModule.MyModuleId.ToString(),
Title = MyModule.Name,
Body = MyModule.Name,
ContentModifiedBy = MyModule.ModifiedBy,
ContentModifiedOn = MyModule.ModifiedOn
});
}
}
return Task.FromResult(searchContentList);
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using Oqtane.Application.Migrations.EntityBuilders;
using Oqtane.Application.Repository;
namespace Oqtane.Application.Migrations
{
[DbContext(typeof(Context))]
[Migration("Oqtane.Application.01.00.00.00")]
public class InitializeModule : MultiDatabaseMigration
{
public InitializeModule(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var myModuleEntityBuilder = new MyModuleEntityBuilder(migrationBuilder, ActiveDatabase);
myModuleEntityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var myModuleEntityBuilder = new MyModuleEntityBuilder(migrationBuilder, ActiveDatabase);
myModuleEntityBuilder.Drop();
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using Oqtane.Migrations.EntityBuilders;
namespace Oqtane.Application.Migrations.EntityBuilders
{
public class MyModuleEntityBuilder : AuditableBaseEntityBuilder<MyModuleEntityBuilder>
{
private const string _entityTableName = "MyModule";
private readonly PrimaryKey<MyModuleEntityBuilder> _primaryKey = new("PK_MyModule", x => x.MyModuleId);
private readonly ForeignKey<MyModuleEntityBuilder> _moduleForeignKey = new("FK_MyModule_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade);
public MyModuleEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_moduleForeignKey);
}
protected override MyModuleEntityBuilder BuildTable(ColumnsBuilder table)
{
MyModuleId = AddAutoIncrementColumn(table, "MyModuleId");
ModuleId = AddIntegerColumn(table,"ModuleId");
Name = AddMaxStringColumn(table,"Name");
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> MyModuleId { get; set; }
public OperationBuilder<AddColumnOperation> ModuleId { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Server.Oqtane</AssemblyName>
<PreserveCompilationContext>true</PreserveCompilationContext>
<SatelliteResourceLanguages>none</SatelliteResourceLanguages>
<CompressionEnabled>false</CompressionEnabled>
<StaticWebAssetsFingerprintContent>false</StaticWebAssetsFingerprintContent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Client\Oqtane.Application.Client.csproj" />
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="6.2.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Oqtane.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace Oqtane.Application.Server
{
public class Program
{
public static void Main(string[] args)
{
// defer server startup to Oqtane - do not modify
var host = BuildWebHost(args);
var databaseManager = host.Services.GetService<IDatabaseManager>();
var install = databaseManager.Install();
if (!string.IsNullOrEmpty(install.Message))
{
var filelogger = host.Services.GetRequiredService<ILogger<Program>>();
if (filelogger != null)
{
filelogger.LogError($"[Oqtane.Application.Server.Program.Main] {install.Message}");
}
}
else
{
host.Run();
}
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseConfiguration(new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables()
.Build())
.UseStartup<Startup>()
.ConfigureLocalizationSettings()
.Build();
}
}

View File

@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:44358",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:44359;http://localhost:44358",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Oqtane.Modules;
using Oqtane.Repository;
using Oqtane.Repository.Databases.Interfaces;
namespace Oqtane.Application.Repository
{
public class Context : DBContextBase, ITransientService, IMultiDatabase
{
public virtual DbSet<Models.MyModule> MyModule { get; set; }
public Context(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies)
{
// ContextBase handles multi-tenant database connections
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Models.MyModule>().ToTable(ActiveDatabase.RewriteName("MyModule"));
}
}
}

View File

@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
namespace Oqtane.Application.Repository
{
public interface IMyModuleRepository
{
IEnumerable<Models.MyModule> GetMyModules(int ModuleId);
Models.MyModule GetMyModule(int MyModuleId);
Models.MyModule GetMyModule(int MyModuleId, bool tracking);
Models.MyModule AddMyModule(Models.MyModule MyModule);
Models.MyModule UpdateMyModule(Models.MyModule MyModule);
void DeleteMyModule(int MyModuleId);
}
public class MyModuleRepository : IMyModuleRepository, ITransientService
{
private readonly IDbContextFactory<Context> _factory;
public MyModuleRepository(IDbContextFactory<Context> factory)
{
_factory = factory;
}
public IEnumerable<Models.MyModule> GetMyModules(int ModuleId)
{
using var db = _factory.CreateDbContext();
return db.MyModule.Where(item => item.ModuleId == ModuleId).ToList();
}
public Models.MyModule GetMyModule(int MyModuleId)
{
return GetMyModule(MyModuleId, true);
}
public Models.MyModule GetMyModule(int MyModuleId, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return db.MyModule.Find(MyModuleId);
}
else
{
return db.MyModule.AsNoTracking().FirstOrDefault(item => item.MyModuleId == MyModuleId);
}
}
public Models.MyModule AddMyModule(Models.MyModule MyModule)
{
using var db = _factory.CreateDbContext();
db.MyModule.Add(MyModule);
db.SaveChanges();
return MyModule;
}
public Models.MyModule UpdateMyModule(Models.MyModule MyModule)
{
using var db = _factory.CreateDbContext();
db.Entry(MyModule).State = EntityState.Modified;
db.SaveChanges();
return MyModule;
}
public void DeleteMyModule(int MyModuleId)
{
using var db = _factory.CreateDbContext();
Models.MyModule MyModule = db.MyModule.Find(MyModuleId);
db.MyModule.Remove(MyModule);
db.SaveChanges();
}
}
}

View File

@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Security;
using Oqtane.Shared;
using Oqtane.Application.Repository;
namespace Oqtane.Application.Services
{
public class ServerMyModuleService : IMyModuleService
{
private readonly IMyModuleRepository _MyModuleRepository;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public ServerMyModuleService(IMyModuleRepository MyModuleRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_MyModuleRepository = MyModuleRepository;
_userPermissions = userPermissions;
_logger = logger;
_accessor = accessor;
_alias = tenantManager.GetAlias();
}
public Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_MyModuleRepository.GetMyModules(ModuleId).ToList());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {ModuleId}", ModuleId);
return null;
}
}
public Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_MyModuleRepository.GetMyModule(MyModuleId));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {TaskId} {ModuleId}", MyModuleId, ModuleId);
return null;
}
}
public Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, MyModule.ModuleId, PermissionNames.Edit))
{
MyModule = _MyModuleRepository.AddMyModule(MyModule);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "MyModule Added {MyModule}", MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Add Attempt {MyModule}", MyModule);
MyModule = null;
}
return Task.FromResult(MyModule);
}
public Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, MyModule.ModuleId, PermissionNames.Edit))
{
MyModule = _MyModuleRepository.UpdateMyModule(MyModule);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "MyModule Updated {MyModule}", MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Update Attempt {MyModule}", MyModule);
MyModule = null;
}
return Task.FromResult(MyModule);
}
public Task DeleteMyModuleAsync(int MyModuleId, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
_MyModuleRepository.DeleteMyModule(MyModuleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "MyModule Deleted {MyModuleId}", MyModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Delete Attempt {MyModuleId} {ModuleId}", MyModuleId, ModuleId);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Shared;
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Oqtane.Application.Server
{
public class Startup
{
private readonly IConfigurationRoot _configuration;
private readonly IWebHostEnvironment _environment;
public Startup(IWebHostEnvironment environment)
{
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data"));
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
_configuration = builder.Build();
_environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
// defer server startup to Oqtane - do not modify
services.AddOqtane(_configuration, _environment);
}
public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync)
{
// defer server startup to Oqtane - do not modify
app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync);
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using Oqtane.Application.Repository;
using Oqtane.Application.Services;
namespace Oqtane.Application.Startup
{
public class ServerStartup : IServerStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// not implemented
}
public void ConfigureMvc(IMvcBuilder mvcBuilder)
{
// not implemented
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyModuleService, ServerMyModuleService>();
services.AddDbContextFactory<Context>(opt => { }, ServiceLifetime.Transient);
}
}
}

View File

@ -0,0 +1,63 @@
{
"RenderMode": "Static",
"Runtime": "Server",
"Database": {
"DefaultDBType": ""
},
"ConnectionStrings": {
"DefaultConnection": ""
},
"Installation": {
"DefaultAlias": "",
"HostPassword": "",
"HostEmail": "",
"SiteTemplate": "",
"DefaultTheme": "",
"DefaultContainer": ""
},
"Localization": {
"DefaultCulture": "en"
},
"AvailableDatabases": [
{
"Name": "LocalDB",
"ControlType": "Oqtane.Installer.Controls.LocalDBConfig, Oqtane.Client",
"DBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Server"
},
{
"Name": "SQL Server",
"ControlType": "Oqtane.Installer.Controls.SqlServerConfig, Oqtane.Client",
"DBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Server"
},
{
"Name": "SQLite",
"ControlType": "Oqtane.Installer.Controls.SqliteConfig, Oqtane.Client",
"DBType": "Oqtane.Database.Sqlite.SqliteDatabase, Oqtane.Server"
},
{
"Name": "MySQL",
"ControlType": "Oqtane.Installer.Controls.MySQLConfig, Oqtane.Client",
"DBType": "Oqtane.Database.MySQL.MySQLDatabase, Oqtane.Server"
},
{
"Name": "PostgreSQL",
"ControlType": "Oqtane.Installer.Controls.PostgreSQLConfig, Oqtane.Client",
"DBType": "Oqtane.Database.PostgreSQL.PostgreSQLDatabase, Oqtane.Server"
},
{
"Name": "Azure SQL",
"ControlType": "Oqtane.Installer.Controls.AzureSqlConfig, Oqtane.Client",
"DBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Server"
}
],
"Logging": {
"FileLogger": {
"LogLevel": {
"Default": "Error"
}
},
"LogLevel": {
"Default": "Information"
}
}
}

View File

@ -0,0 +1,5 @@
/* Module Script */
var App = App || {};
App.MyModule = {
};

View File

@ -0,0 +1,19 @@
using System;
using System.ComponentModel.DataAnnotations;
using Oqtane.Models;
namespace Oqtane.Application.Models
{
public class MyModule : IAuditable
{
[Key]
public int MyModuleId { get; set; }
public int ModuleId { get; set; }
public string Name { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedOn { get; set; }
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Shared.Oqtane</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="6.2.1" />
</ItemGroup>
</Project>

BIN
Oqtane.Application/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Localization;
using Oqtane.Interfaces; using Oqtane.Interfaces;
using Oqtane.Providers; using Oqtane.Providers;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
using Radzen;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
@ -23,7 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<SiteState>(); services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>(); services.AddScoped<IInstallationService, InstallationService>();
services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>(); services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
services.AddScoped<IThemeService, ThemeService>(); services.AddScoped<IThemeService, Oqtane.Services.ThemeService>();
services.AddScoped<IAliasService, AliasService>(); services.AddScoped<IAliasService, AliasService>();
services.AddScoped<ITenantService, TenantService>(); services.AddScoped<ITenantService, TenantService>();
services.AddScoped<ISiteService, SiteService>(); services.AddScoped<ISiteService, SiteService>();
@ -39,7 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ILogService, LogService>(); services.AddScoped<ILogService, LogService>();
services.AddScoped<IJobService, JobService>(); services.AddScoped<IJobService, JobService>();
services.AddScoped<IJobLogService, JobLogService>(); services.AddScoped<IJobLogService, JobLogService>();
services.AddScoped<INotificationService, NotificationService>(); services.AddScoped<INotificationService, Oqtane.Services.NotificationService>();
services.AddScoped<IFolderService, FolderService>(); services.AddScoped<IFolderService, FolderService>();
services.AddScoped<IFileService, FileService>(); services.AddScoped<IFileService, FileService>();
services.AddScoped<ISiteTemplateService, SiteTemplateService>(); services.AddScoped<ISiteTemplateService, SiteTemplateService>();
@ -53,11 +55,19 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>(); services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>(); services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<IOutputCacheService, OutputCacheService>(); services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.TextAreaTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.TextAreaTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.RadzenTextEditor>();
services.AddRadzenComponents();
var localizer = services.BuildServiceProvider().GetService<IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor>>();
Oqtane.Modules.Controls.RadzenEditorDefinitions.Localizer = localizer;
return services; return services;
} }

View File

@ -0,0 +1,119 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
@inject IStringLocalizer<SqlServerConfig> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="security" HelpText="Select your security method" ResourceKey="Security">Security:</Label>
<div class="col-sm-9">
<select id="security" class="form-select custom-select" @bind="@_security">
<option value="integrated" selected>@Localizer["Integrated"]</option>
<option value="custom">@Localizer["Custom"]</option>
</select>
</div>
</div>
@if (_security == "custom")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="pwd" type="@_passwordType" class="form-control" @bind="@_pwd" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="encryption" HelpText="Specify if you are using an encrypted database connection. It is highly recommended to use encryption in a production environment." ResourceKey="Encryption">Encryption:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_encryption">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
@if (_encryption == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True." ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option>
<option value="false">@Localizer["Verifiable"]</option>
</select>
</div>
</div>
}
@code {
private string _server = "tcp:{SQL Server Name}.database.windows.net,1433";
private string _database = "{SQL Database Name}";
private string _security = "custom";
private string _uid = "{SQL Administrator Login}";
private string _pwd = String.Empty;
private string _passwordType = "password";
private string _togglePassword = string.Empty;
private string _encryption = "true";
private string _trustservercertificate = "false";
protected override void OnInitialized()
{
_togglePassword = SharedLocalizer["ShowPassword"];
}
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database))
{
connectionString = $"Data Source={_server};Initial Catalog={_database};";
}
if (_security == "integrated")
{
connectionString += "Integrated Security=SSPI;";
}
else
{
connectionString += $"User ID={_uid};Password={_pwd};";
}
connectionString += $"Encrypt={_encryption};";
connectionString += $"TrustServerCertificate={_trustservercertificate};";
return connectionString;
}
private void TogglePassword()
{
if (_passwordType == "password")
{
_passwordType = "text";
_togglePassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordType = "password";
_togglePassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -4,7 +4,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) " ResourceKey="Server">Server:</Label> <Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" /> <input id="server" type="text" class="form-control" @bind="@_server" />
</div> </div>

View File

@ -14,8 +14,8 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="mx-auto text-center"> <div class="mx-auto text-center">
<img src="oqtane-black.png" /> <img src="installer-logo.png" />
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET 9)</div> <div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET @Environment.Version.Major)</div>
</div> </div>
</div> </div>
<hr class="app-rule" /> <hr class="app-rule" />
@ -182,7 +182,7 @@
} }
else else
{ {
_databaseName = "LocalDB"; _databaseName = Constants.DefaultDBName;
} }
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
@ -269,8 +269,8 @@
SiteName = Constants.DefaultSite, SiteName = Constants.DefaultSite,
Register = _register, Register = _register,
SiteTemplate = _template, SiteTemplate = _template,
RenderMode = RenderModes.Static, RenderMode = "", // provided by appsettings.json
Runtime = Runtimes.Server Runtime = "" // provided by appsettings.json
}; };
var installation = await InstallationService.Install(config); var installation = await InstallationService.Install(config);

View File

@ -15,22 +15,34 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label> <Label Class="col-sm-3" For="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required> @if (_parentId == -1)
@if (PageState.QueryString.ContainsKey("id"))
{ {
<select id="parent" class="form-select" @bind="@_parentId" required>
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option> <option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
</select>
} }
else
{
<select id="parent" class="form-select" @bind="@_parentId" required>
@foreach (Folder folder in _folders) @foreach (Folder folder in _folders)
{ {
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option> <option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
} }
</select> </select>
}
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@if (_isSystem)
{
<input id="name" class="form-control" @bind="@_name" readonly />
}
else
{
<input id="name" class="form-control" @bind="@_name" maxlength="256" required /> <input id="name" class="form-control" @bind="@_name" maxlength="256" required />
}
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -229,7 +241,6 @@
if (folder != null) if (folder != null)
{ {
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder); await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -264,20 +275,12 @@
} }
} }
if (!isparent) if (!isparent)
{
var files = await FileService.GetFilesAsync(_folderId);
if (files.Count == 0)
{ {
await FolderService.DeleteFolderAsync(_folderId); await FolderService.DeleteFolderAsync(_folderId);
await logger.LogInformation("Folder Deleted {Folder}", _folderId); await logger.LogInformation("Folder Deleted {Folder}", _folderId);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
else else
{
AddModuleMessage(Localizer["Message.Folder.Files.InvalidDelete"], MessageType.Warning);
}
}
else
{ {
AddModuleMessage(Localizer["Message.Folder.Subfolders.InvalidDelete"], MessageType.Warning); AddModuleMessage(Localizer["Message.Folder.Subfolders.InvalidDelete"], MessageType.Warning);
} }

View File

@ -53,7 +53,7 @@ else
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td> <td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td> <td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td> <td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@context.ModifiedOn</td> <td>@UtcToLocal(context.ModifiedOn)</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td> <td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td> <td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row> </Row>

View File

@ -45,7 +45,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label> <Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="4" required /> <input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -56,7 +56,7 @@
<input id="starting" type="date" class="form-control" @bind="@_startDate" /> <input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div> </div>
<div class="col"> <div class="col">
<input id="starting" type="time" class="form-control" placeholder="hh:mm" @bind="@_startTime" /> <input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" />
</div> </div>
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
<input id="ending" type="date" class="form-control" @bind="@_endDate" /> <input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div> </div>
<div class="col"> <div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" /> <input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" />
</div> </div>
</div> </div>
</div> </div>
@ -82,7 +82,7 @@
<input id="next" type="date" class="form-control" @bind="@_nextDate" /> <input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div> </div>
<div class="col"> <div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" /> <input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div> </div>
</div> </div>
</div> </div>
@ -132,13 +132,13 @@
_isEnabled = job.IsEnabled.ToString(); _isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString(); _interval = job.Interval.ToString();
_frequency = job.Frequency; _frequency = job.Frequency;
_startDate = Utilities.UtcAsLocalDate(job.StartDate); _startDate = UtcToLocal(job.StartDate);
_startTime = Utilities.UtcAsLocalDateTime(job.StartDate); _startTime = UtcToLocal(job.StartDate);
_endDate = Utilities.UtcAsLocalDate(job.EndDate); _endDate = UtcToLocal(job.EndDate);
_endTime = Utilities.UtcAsLocalDateTime(job.EndDate); _endTime = UtcToLocal(job.EndDate);
_retentionHistory = job.RetentionHistory.ToString(); _retentionHistory = job.RetentionHistory.ToString();
_nextDate = Utilities.UtcAsLocalDate(job.NextExecution); _nextDate = UtcToLocal(job.NextExecution);
_nextTime = Utilities.UtcAsLocalDateTime(job.NextExecution); _nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
@ -176,10 +176,18 @@
{ {
job.Interval = int.Parse(_interval); job.Interval = int.Parse(_interval);
} }
job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime); job.StartDate = _startDate.HasValue && _startTime.HasValue
job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime); ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory); job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
try try
{ {
@ -198,5 +206,4 @@
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
} }
} }
} }

View File

@ -29,7 +29,7 @@ else
<td>@context.Name</td> <td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td> <td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td> <td>@DisplayFrequency(context.Interval, context.Frequency)</td>
<td>@context.NextExecution?.ToLocalTime()</td> <td>@UtcToLocal(context.NextExecution)</td>
<td> <td>
@if (context.IsStarted) @if (context.IsStarted)
{ {
@ -115,6 +115,13 @@ else
private async Task StartJob(int jobId) private async Task StartJob(int jobId)
{ {
try try
{
Job _job = await JobService.GetJobAsync(jobId);
if (!_job.IsEnabled)
{
AddModuleMessage(Localizer["Message.Job.Disabled"], MessageType.Warning);
}
else
{ {
await JobService.StartJobAsync(jobId); await JobService.StartJobAsync(jobId);
await logger.LogInformation("Job Started {JobId}", jobId); await logger.LogInformation("Job Started {JobId}", jobId);
@ -122,6 +129,7 @@ else
_jobs = await JobService.GetJobsAsync(); _jobs = await JobService.GetJobsAsync();
StateHasChanged(); StateHasChanged();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Starting Job {JobId} {Error}", jobId, ex.Message); await logger.LogError(ex, "Error Starting Job {JobId} {Error}", jobId, ex.Message);

View File

@ -23,8 +23,8 @@ else
<Row> <Row>
<td>@context.Job.Name</td> <td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td> <td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td> <td>@UtcToLocal(context.StartDate)</td>
<td>@context.FinishDate</td> <td>@UtcToLocal(context.FinishDate)</td>
</Row> </Row>
<Detail> <Detail>
<td colspan="4">@((MarkupString)context.Notes)</td> <td colspan="4">@((MarkupString)context.Notes)</td>
@ -44,14 +44,12 @@ else
private async Task GetJobLogs() private async Task GetJobLogs()
{ {
_jobLogs = await JobLogService.GetJobLogsAsync(); var jobId = -1;
if (PageState.QueryString.ContainsKey("id")) if (PageState.QueryString.ContainsKey("id"))
{ {
_jobLogs = _jobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList(); jobId = int.Parse(PageState.QueryString["id"]);
} }
_jobLogs = await JobLogService.GetJobLogsAsync(jobId);
_jobLogs = _jobLogs.OrderByDescending(item => item.JobLogId).ToList();
} }
private string DisplayStatus(bool isExecuting, bool? succeeded) private string DisplayStatus(bool isExecuting, bool? succeeded)

View File

@ -21,9 +21,7 @@ else
@if (_allowexternallogin) @if (_allowexternallogin)
{ {
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button> <button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /> <br /><br />
<br />
} }
@if (_allowsitelogin) @if (_allowsitelogin)
{ {
@ -49,15 +47,11 @@ else
</div> </div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button> <button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /> <br /><br />
<br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button> <button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration) @if (PageState.Site.AllowRegistration)
{ {
<br /> <br /><br />
<br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink> <NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
} }
} }
@ -100,7 +94,7 @@ else
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } new Stylesheet(ModulePath() + "Module.css")
}; };
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -144,7 +138,7 @@ else
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null) if (user != null)
{ {
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
} }
else else

View File

@ -141,7 +141,7 @@
var log = await LogService.GetLogAsync(_logId); var log = await LogService.GetLogAsync(_logId);
if (log != null) if (log != null)
{ {
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture); _logDate = UtcToLocal(log.LogDate).Value.ToString(CultureInfo.CurrentCulture);
_level = log.Level; _level = log.Level;
_feature = log.Feature; _feature = log.Feature;
_function = log.Function; _function = log.Function;

View File

@ -64,7 +64,7 @@ else
</Header> </Header>
<Row> <Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Text="Details" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td> <td class="@GetClass(context.Function)"><ActionLink Action="Detail" Text="Details" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td> <td class="@GetClass(context.Function)">@UtcToLocal(context.LogDate)</td>
<td class="@GetClass(context.Function)">@context.Level</td> <td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td> <td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td> <td class="@GetClass(context.Function)">@context.Function</td>

View File

@ -101,6 +101,12 @@
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br /> <small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br /> @(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<br /> <br />
@if (_moduledefinitions.Exists(item => item.PackageName == context.PackageId))
{
<button type="button" class="btn btn-info">@SharedLocalizer["Installed"]</button>
}
else
{
@if (!string.IsNullOrEmpty(context.PackageUrl)) @if (!string.IsNullOrEmpty(context.PackageUrl))
{ {
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button> <button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
@ -109,6 +115,7 @@
{ {
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a> <a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
} }
}
<br /> <br />
</div> </div>
</div> </div>
@ -171,6 +178,7 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private List<ModuleDefinition> _moduledefinitions;
private int _page = 1; private int _page = 1;
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
@ -187,7 +195,8 @@
{ {
try try
{ {
await LoadModuleDefinitions(); _moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
await LoadPackages();
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@ -197,24 +206,10 @@
} }
} }
private async Task LoadModuleDefinitions() private async Task LoadPackages()
{ {
ShowProgressIndicator(); ShowProgressIndicator();
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module", _search, _price, "", _sort); _packages = await PackageService.GetPackagesAsync("module", _search, _price, "", _sort);
if (_packages != null)
{
foreach (Package package in _packages.ToArray())
{
if (moduledefinitions.Exists(item => item.PackageName == package.PackageId))
{
_packages.Remove(package);
}
}
}
HideProgressIndicator(); HideProgressIndicator();
} }
@ -222,25 +217,25 @@
{ {
_price = price; _price = price;
_sort = "popularity"; _sort = "popularity";
await LoadModuleDefinitions(); await LoadPackages();
StateHasChanged(); StateHasChanged();
} }
private async Task Search() private async Task Search()
{ {
await LoadModuleDefinitions(); await LoadPackages();
} }
private async Task Reset() private async Task Reset()
{ {
_page = 1; _page = 1;
_search = ""; _search = "";
await LoadModuleDefinitions(); await LoadPackages();
} }
private async Task Refresh() private async Task Refresh()
{ {
await LoadModuleDefinitions(); await LoadPackages();
} }
private void OnPageChange(int page) private void OnPageChange(int page)
@ -251,7 +246,7 @@
private async void SortChanged(ChangeEventArgs e) private async void SortChanged(ChangeEventArgs e)
{ {
_sort = (string)e.Value; _sort = (string)e.Value;
await LoadModuleDefinitions(); await LoadPackages();
} }
private void HideModal() private void HideModal()
@ -310,6 +305,6 @@
private void OnUpload() private void OnUpload()
{ {
AddModuleMessage(string.Format(Localizer["Success.Module.Download"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Upload"], NavigateUrl("admin/system")), MessageType.Success);
} }
} }

View File

@ -14,7 +14,7 @@
@if (_initialized) @if (_initialized)
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Definition" ResourceKey="Definition" Heading="Definition"> <TabPanel Name="Module" ResourceKey="Module" Heading="Module">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -73,7 +73,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the module" ResourceKey="ReferenceUrl">Reference Url: </Label> <Label Class="col-sm-3" For="url" HelpText="The url of the module" ResourceKey="Url">Url: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled /> <input id="url" class="form-control" @bind="@_url" disabled />
</div> </div>
@ -236,11 +236,10 @@
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
#pragma warning restore 649
private List<Page> _pagesWithModules;
private List<Package> _packages; private List<Package> _packages;
private List<Language> _languages; private List<Language> _languages;

View File

@ -17,8 +17,8 @@ else
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<div class="col-sm-6"> <div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" /> <ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;") <ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ms-1" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" /> <button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))"> <select class="form-select" @onchange="(e => CategoryChanged(e))">
@ -220,4 +220,27 @@ else
_category = (string)e.Value; _category = (string)e.Value;
await LoadModuleDefinitions(); await LoadModuleDefinitions();
} }
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var moduleDefinition in _moduleDefinitions)
{
if (!string.IsNullOrEmpty(moduleDefinition.PackageName) && !_packages.Any(item => item.PackageId == moduleDefinition.PackageName))
{
var package = await PackageService.GetPackageAsync(moduleDefinition.PackageName, moduleDefinition.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Module.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Synchronize"], MessageType.Error);
}
}
} }

View File

@ -5,24 +5,57 @@
@inject IStringLocalizer<Export> Localizer @inject IStringLocalizer<Export> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Content" Heading="Content" ResourceKey="Content">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="The Exported Module Content" ResourceKey="Content">Content: </Label> <Label Class="col-sm-3" For="content" HelpText="Select the Export option and you will be able to view the module content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea> <textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
</div> </div>
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button> <button type="button" class="btn btn-success" @onclick="ExportText">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="File" Heading="File" ResourceKey="File">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="folder" HelpText="Select a folder where you wish to save the exported content" ResourceKey="Folder">Folder: </Label>
<div class="col-sm-9">
<FileManager ShowFiles="false" ShowUpload="false" @ref="_filemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="filename" HelpText="Specify a name for the file (without an extension)" ResourceKey="Filename">Filename: </Label>
<div class="col-sm-9">
<input id="content" type="text" class="form-control" @bind="@_filename" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportFile">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
@code { @code {
private string _content = string.Empty; private string _content = string.Empty;
private FileManager _filemanager;
private string _filename = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content"; public override string Title => "Export Content";
private async Task ExportModule() protected override void OnInitialized()
{
_filename = Utilities.GetFriendlyUrl(ModuleState.Title);
}
private async Task ExportText()
{ {
try try
{ {
@ -35,4 +68,34 @@
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
} }
} }
private async Task ExportFile()
{
try
{
var folderid = _filemanager.GetFolderId();
if (folderid != -1 && !string.IsNullOrEmpty(_filename))
{
var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename);
if (fileid != -1)
{
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
} }

View File

@ -2,20 +2,27 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IModuleService ModuleService @inject IModuleService ModuleService
@inject IFileService FileService
@inject IStringLocalizer<Import> Localizer @inject IStringLocalizer<Import> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Enter The Module Content To Import" ResourceKey="Content">Content: </Label> <Label Class="col-sm-3" For="file" HelpText="Optionally upload or select a file to import for this module" ResourceKey="File">File: </Label>
<div class="col-sm-9">
<FileManager Filter="json" OnSelectFile="OnSelectFile" />
</div>
</div>
<hr />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Provide the module content to import" ResourceKey="Content">Content: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea> <textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea>
</div> </div>
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button> <button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form> </form>
@ -28,6 +35,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content"; public override string Title => "Import Content";
private async Task OnSelectFile(int fileId)
{
var bytes = await FileService.DownloadFileAsync(fileId);
_content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
private async Task ImportModule() private async Task ImportModule()
{ {
validated = true; validated = true;

View File

@ -97,6 +97,23 @@
</div> </div>
</div> </div>
</div> </div>
<br />
<Section Name="ModuleContent" Heading="Content" ResourceKey="ModuleContent">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduleheader" HelpText="Optionally provide content to be injected above the module instance" ResourceKey="Header">Header: </Label>
<div class="col-sm-9">
<textarea id="moduleheader" class="form-control" @bind="@_header" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="modulefooter" HelpText="Optionally provide content to be injected below the module instance" ResourceKey="Footer">Footer: </Label>
<div class="col-sm-9">
<textarea id="modulefooter" class="form-control" @bind="@_footer" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
</Section>
} }
</TabPanel> </TabPanel>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions"> <TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@ -144,6 +161,8 @@
private string _pane; private string _pane;
private string _containerType; private string _containerType;
private string _allPages = "false"; private string _allPages = "false";
private string _header = "";
private string _footer = "";
private string _permissionNames = ""; private string _permissionNames = "";
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private string _pageId; private string _pageId;
@ -167,37 +186,47 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
SetModuleTitle(Localizer["ModuleSettings.Title"]); SetModuleTitle(Localizer["ModuleSettings.Title"]);
_title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; _moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.PermissionList;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
_effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (ModuleState.ModuleDefinition != null) var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
{
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType)) _pageId = pagemodule.PageId.ToString();
_title = pagemodule.Title;
_pane = pagemodule.Pane;
_containerType = pagemodule.ContainerType;
if (string.IsNullOrEmpty(_containerType))
{
_containerType = (!string.IsNullOrEmpty(PageState.Page.DefaultContainerType)) ? PageState.Page.DefaultContainerType : PageState.Site.DefaultContainerType;
}
_header = pagemodule.Header;
_footer = pagemodule.Footer;
_effectivedate = Utilities.UtcAsLocalDate(pagemodule.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString();
createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy;
modifiedon = pagemodule.Module.ModifiedOn;
_permissions = pagemodule.Module.PermissionList;
if (pagemodule.Module.ModuleDefinition != null)
{
_module = pagemodule.Module.ModuleDefinition.Name;
_permissionNames = pagemodule.Module.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(pagemodule.Module.ModuleDefinition.SettingsType))
{ {
// module settings type explicitly declared in IModule interface // module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType); _moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.SettingsType);
} }
else else
{ {
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module ) // legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true); _moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
} }
if (_moduleSettingsType != null) if (_moduleSettingsType != null)
{ {
@ -218,7 +247,7 @@
} }
else else
{ {
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error); AddModuleMessage(string.Format(Localizer["Error.Module.Load"], pagemodule.Module.ModuleDefinitionName), MessageType.Error);
} }
var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
@ -270,10 +299,12 @@
{ {
pagemodule.ContainerType = string.Empty; pagemodule.ContainerType = string.Empty;
} }
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState; var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages); module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId; module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList(); module.PermissionList = _permissionGrid.GetPermissionList();

View File

@ -269,8 +269,16 @@
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList))) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList)))
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))

View File

@ -217,6 +217,9 @@
</div> </div>
</Section> </Section>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br /> <br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
</TabPanel> </TabPanel>
@ -225,6 +228,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
<br /><br />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="updatemodulepermissions" HelpText="Specify if changes made to page permissions should be propagated to the modules on this page" ResourceKey="UpdateModulePermissions">Update Module Permissions? </Label>
<div class="col-sm-9">
<select id="updatemodulepermissions" class="form-select" @bind="@_updatemodulepermissions" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div> </div>
</TabPanel> </TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules"> <TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
@ -247,8 +263,10 @@
{ {
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings"> <TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent @_themeSettingsComponent
</TabPanel>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
} }
</TabStrip> </TabStrip>
} }
@ -299,19 +317,21 @@
</div> </div>
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel> </TabPanel>
@if (_themeSettingsType != null) @if (_themeSettingsType != null)
{ {
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings"> <TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent @_themeSettingsComponent
</TabPanel>
<br />
}
</TabStrip>
}
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
}
</TabStrip>
}
</form> </form>
} }
@ -348,6 +368,7 @@
private string _bodycontent; private string _bodycontent;
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private string _updatemodulepermissions;
private List<Module> _pageModules; private List<Module> _pageModules;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@ -422,8 +443,16 @@
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = _page.DefaultContainerType; _containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype)) if (string.IsNullOrEmpty(_containertype))
{ {
@ -436,6 +465,7 @@
// permissions // permissions
_permissions = _page.PermissionList; _permissions = _page.PermissionList;
_updatemodulepermissions = "True";
// page modules // page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@ -651,6 +681,7 @@
if (_page.UserId == null) if (_page.UserId == null)
{ {
_page.PermissionList = _permissionGrid.GetPermissionList(); _page.PermissionList = _permissionGrid.GetPermissionList();
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
} }
_page = await PageService.UpdatePageAsync(_page); _page = await PageService.UpdatePageAsync(_page);

View File

@ -2,6 +2,7 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -56,9 +57,25 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="options" HelpText="A comma delimited list of options the user can select from" ResourceKey="Options">Options: </Label> <Label Class="col-sm-3" For="options" HelpText="A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from Settings." ResourceKey="Options">Options: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group">
@if (_optiontype == "Settings")
{
<input id="options" class="form-control" @bind="@_options" maxlength="2000" /> <input id="options" class="form-control" @bind="@_options" maxlength="2000" />
}
else
{
<select id="entityName" class="form-select" @bind="@_options">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var entityname in _entitynames)
{
<option value="@entityname">@entityname</option>
}
</select>
}
<button type="button" class="btn btn-secondary" @onclick="ToggleOptionType">@Localizer[_optiontype]</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -116,6 +133,8 @@
private string _rows = "1"; private string _rows = "1";
private string _defaultvalue = string.Empty; private string _defaultvalue = string.Empty;
private string _options = string.Empty; private string _options = string.Empty;
private string _optiontype = "Settings";
private List<string> _entitynames;
private string _validation = string.Empty; private string _validation = string.Empty;
private string _autocomplete = string.Empty; private string _autocomplete = string.Empty;
private string _isrequired = "False"; private string _isrequired = "False";
@ -133,6 +152,8 @@
{ {
try try
{ {
_entitynames = await SettingService.GetEntityNamesAsync();
if (PageState.QueryString.ContainsKey("id")) if (PageState.QueryString.ContainsKey("id"))
{ {
_profileid = Int32.Parse(PageState.QueryString["id"]); _profileid = Int32.Parse(PageState.QueryString["id"]);
@ -148,6 +169,11 @@
_rows = profile.Rows.ToString(); _rows = profile.Rows.ToString();
_defaultvalue = profile.DefaultValue; _defaultvalue = profile.DefaultValue;
_options = profile.Options; _options = profile.Options;
if (_options.StartsWith("EntityName:"))
{
_optiontype = "Options";
_options = _options.Substring(11);
}
_validation = profile.Validation; _validation = profile.Validation;
_autocomplete = profile.Autocomplete; _autocomplete = profile.Autocomplete;
_isrequired = profile.IsRequired.ToString(); _isrequired = profile.IsRequired.ToString();
@ -166,6 +192,18 @@
} }
} }
private void ToggleOptionType()
{
if (_optiontype == "Options")
{
_optiontype = "Settings";
}
else
{
_optiontype = "Options";
}
}
private async Task SaveProfile() private async Task SaveProfile()
{ {
validated = true; validated = true;
@ -193,7 +231,14 @@
profile.MaxLength = int.Parse(_maxlength); profile.MaxLength = int.Parse(_maxlength);
profile.Rows = int.Parse(_rows); profile.Rows = int.Parse(_rows);
profile.DefaultValue = _defaultvalue; profile.DefaultValue = _defaultvalue;
if (_optiontype == "Options" && !string.IsNullOrEmpty(_options))
{
profile.Options = "EntityName:" + _options;
}
else
{
profile.Options = _options; profile.Options = _options;
}
profile.Validation = _validation; profile.Validation = _validation;
profile.Autocomplete = _autocomplete; profile.Autocomplete = _autocomplete;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired)); profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));

View File

@ -6,7 +6,10 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@if (_initialized)
{
@if (PageState.Site.AllowRegistration) @if (PageState.Site.AllowRegistration)
{ {
if (!_userCreated) if (!_userCreated)
@ -56,6 +59,18 @@
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" /> <input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button> <button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@ -75,8 +90,11 @@ else
{ {
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" /> <ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
} }
}
@code { @code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private string _passwordrequirements; private string _passwordrequirements;
private string _username = string.Empty; private string _username = string.Empty;
private ElementReference form; private ElementReference form;
@ -87,6 +105,7 @@ else
private string _confirm = string.Empty; private string _confirm = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private bool _userCreated = false; private bool _userCreated = false;
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
@ -96,6 +115,9 @@ else
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
} }
protected override void OnParametersSet() protected override void OnParametersSet()
@ -124,6 +146,7 @@ else
Password = _password, Password = _password,
Email = _email, Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname), DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
PhotoFileId = null PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@ -40,13 +40,16 @@
{ {
<Pager Items="@_searchResults?.Results" <Pager Items="@_searchResults?.Results"
Format="Grid" Format="Grid"
PageSize="@_pageSize"
DisplayPages="@_displayPages"
CurrentPage="@_currentPage"
Columns="1" Columns="1"
Toolbar="Bottom" Toolbar="Bottom"
Parameters="@($"q={_keywords}")"> Parameters="@($"q={_keywords}")">
<Row> <Row>
<div class="search-item mb-2"> <div class="search-item mb-2">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4> <h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4>
<p class="mb-0 text-muted">@((MarkupString)context.Snippet)</p> <p class="mb-0 text-body-secondary">@((MarkupString)context.Snippet)</p>
</div> </div>
</Row> </Row>
</Pager> </Pager>
@ -66,6 +69,7 @@
@code { @code {
public override string RenderMode => RenderModes.Static; public override string RenderMode => RenderModes.Static;
private const string SearchDefaultPageSize = "10";
private string _includeEntities; private string _includeEntities;
private string _excludeEntities; private string _excludeEntities;
@ -75,6 +79,8 @@
private string _sortField; private string _sortField;
private string _sortOrder; private string _sortOrder;
private string _bodyLength; private string _bodyLength;
private string _currentPage = "0";
private string _displayPages = "7";
private string _keywords; private string _keywords;
private SearchResults _searchResults; private SearchResults _searchResults;
@ -89,11 +95,16 @@
_excludeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ExcludeEntities", ""); _excludeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ExcludeEntities", "");
_fromDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_FromDate", DateTime.MinValue.ToString()); _fromDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_FromDate", DateTime.MinValue.ToString());
_toDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ToDate", DateTime.MaxValue.ToString()); _toDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ToDate", DateTime.MaxValue.ToString());
_pageSize = SettingService.GetSetting(ModuleState.Settings, "SearchResults_PageSize", int.MaxValue.ToString()); _pageSize = SettingService.GetSetting(ModuleState.Settings, "SearchResults_PageSize", SearchDefaultPageSize);
_sortField = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortField", "Relevance"); _sortField = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortField", "Relevance");
_sortOrder = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortOrder", "Descending"); _sortOrder = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortOrder", "Descending");
_bodyLength = SettingService.GetSetting(ModuleState.Settings, "SearchResults_BodyLength", "255"); _bodyLength = SettingService.GetSetting(ModuleState.Settings, "SearchResults_BodyLength", "255");
if (PageState.QueryString.ContainsKey("p"))
{
_currentPage = PageState.QueryString["p"];
}
if (_keywords == null && PageState.QueryString.ContainsKey("q")) if (_keywords == null && PageState.QueryString.ContainsKey("q"))
{ {
_keywords = WebUtility.UrlDecode(PageState.QueryString["q"]); _keywords = WebUtility.UrlDecode(PageState.QueryString["q"]);
@ -122,7 +133,7 @@
ExcludeEntities = _excludeEntities, ExcludeEntities = _excludeEntities,
FromDate = (!string.IsNullOrEmpty(_fromDate)) ? DateTime.Parse(_fromDate) : DateTime.MinValue, FromDate = (!string.IsNullOrEmpty(_fromDate)) ? DateTime.Parse(_fromDate) : DateTime.MinValue,
ToDate = (!string.IsNullOrEmpty(_toDate)) ? DateTime.Parse(_toDate) : DateTime.MaxValue, ToDate = (!string.IsNullOrEmpty(_toDate)) ? DateTime.Parse(_toDate) : DateTime.MaxValue,
PageSize = (!string.IsNullOrEmpty(_pageSize)) ? int.Parse(_pageSize) : int.MaxValue, PageSize = int.MaxValue,
PageIndex = 0, PageIndex = 0,
SortField = (!string.IsNullOrEmpty(_sortField)) ? (SearchSortField)Enum.Parse(typeof(SearchSortField), _sortField) : SearchSortField.Relevance, SortField = (!string.IsNullOrEmpty(_sortField)) ? (SearchSortField)Enum.Parse(typeof(SearchSortField), _sortField) : SearchSortField.Relevance,
SortOrder = (!string.IsNullOrEmpty(_sortOrder)) ? (SearchSortOrder)Enum.Parse(typeof(SearchSortOrder), _sortOrder) : SearchSortOrder.Descending, SortOrder = (!string.IsNullOrEmpty(_sortOrder)) ? (SearchSortOrder)Enum.Parse(typeof(SearchSortOrder), _sortOrder) : SearchSortOrder.Descending,

View File

@ -0,0 +1,226 @@
@namespace Oqtane.Modules.Admin.Settings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="entityName" HelpText="Entity Name" ResourceKey="EntityName">Entity:</Label>
<div class="col-sm-9">
<div class="input-group">
@if (_entityNameElement == "input")
{
<input id="entityName" class="form-control" @bind="@_entityName" maxlength="256" required />
}
else
{
<select class="form-select custom-select" value="@_entityName" @onchange="(e => EntityNameChanged(e))">
<option value="-">&lt;@Localizer["Select Entity"]&gt;</option>
@foreach (var entityName in _entityNames)
{
<option value="@entityName">@entityName</option>
}
</select>
}
<button type="button" class="btn btn-secondary" @onclick="@EntityNameClicked" tabindex="-1">@_entityNameTitle</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="entityId" HelpText="Entity Id" ResourceKey="EntityId">Id:</Label>
<div class="col-sm-9">
<div class="input-group">
@if (_entityIdElement == "input")
{
<input id="entityId" class="form-control" @bind="@_entityId" maxlength="256" required />
}
else
{
<select class="form-select custom-select" @bind="@_entityId">
<option value="-">&lt;@Localizer["Select Id"]&gt;</option>
@foreach (var entityId in _entityIds)
{
<option value="@entityId">@entityId</option>
}
</select>
}
<button type="button" class="btn btn-secondary" @onclick="@EntityIdClicked" tabindex="-1">@_entityIdTitle</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settingName" HelpText="Setting Name" ResourceKey="SettingName">Name:</Label>
<div class="col-sm-9">
<input id="settingName" class="form-control" @bind="@_settingName" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settingValue" HelpText="Setting Value" ResourceKey="SettingValue">Value:</Label>
<div class="col-sm-9">
<input id="SettingValue" class="form-control" @bind="@_settingValue" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isPrivate" HelpText="Private" ResourceKey="IsPrivate">Private?</Label>
<div class="col-sm-9">
<select id="isPrivate" class="form-select" @bind="@_isPrivate">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveSetting">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</div>
</form>
@code {
private ElementReference form;
private bool validated = false;
private string _entityName = "-";
private List<string> _entityNames = new List<string>();
private string _entityNameElement = "select";
private string _entityNameTitle = "";
private string _entityId = "-";
private List<int> _entityIds = new List<int>();
private string _entityIdElement = "select";
private string _entityIdTitle = "";
private string _settingName = "";
private string _settingValue = "";
private string _isPrivate = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_entityNameTitle = Localizer["Input"];
_entityIdTitle = Localizer["Input"];
// default entity names
_entityNames.Add(EntityNames.Host);
_entityNames.Add(EntityNames.Job);
_entityNames.Add(EntityNames.ModuleDefinition);
_entityNames.Add(EntityNames.Theme);
_entityNames.Add(EntityNames.Tenant);
_entityNames.Add(EntityNames.Site);
_entityNames.Add(EntityNames.Role);
_entityNames.Add(EntityNames.Page);
_entityNames.Add(EntityNames.Module);
_entityNames.Add(EntityNames.Folder);
_entityNames.Add(EntityNames.User);
_entityNames.Add(EntityNames.Visitor);
// custom entity names
var entityNames = await SettingService.GetEntityNamesAsync();
foreach (var entityName in entityNames)
{
if (!_entityNames.Contains(entityName))
{
_entityNames.Add(entityName);
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Setting {Error}", ex.Message);
AddModuleMessage(Localizer["Error.LoadSetting"], MessageType.Error);
}
}
private void EntityNameClicked()
{
if (_entityNameElement == "select")
{
_entityName = "";
_entityNameElement = "input";
_entityNameTitle = Localizer["Select"];
_entityId = "";
_entityIdElement = "input";
_entityIdTitle = Localizer["Select"];
}
else
{
_entityName = "-";
_entityNameElement = "select";
_entityNameTitle = Localizer["Input"];
}
}
private void EntityIdClicked()
{
if (_entityIdElement == "select")
{
_entityId = "";
_entityIdElement = "input";
_entityIdTitle = Localizer["Select"];
}
else
{
_entityId = "-";
_entityIdElement = "select";
_entityIdTitle = Localizer["Input"];
}
}
private async void EntityNameChanged(ChangeEventArgs e)
{
try
{
_entityName = e.Value.ToString();
_entityId = "-";
_entityIdElement = "select";
_entityIdTitle = Localizer["Input"];
if (_entityName != "-")
{
_entityIds = await SettingService.GetEntityIdsAsync(_entityName);
}
else
{
_entityIds = new List<int>();
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On EntityNameChanged");
}
}
private async Task SaveSetting()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form) && _entityName != "-" && int.TryParse(_entityId, out int entityId))
{
var setting = new Setting();
setting.EntityName = _entityName;
setting.EntityId = entityId;
setting.SettingName = _settingName;
setting.SettingValue = _settingValue;
setting.IsPrivate = (bool.Parse(_isPrivate));
try
{
setting = await SettingService.AddSettingAsync(setting);
await logger.LogInformation("Setting Saved {Setting}", setting);
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Setting {Setting} {Error}", setting, ex.Message);
AddModuleMessage(Localizer["Error.SaveSetting"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -0,0 +1,122 @@
@namespace Oqtane.Modules.Admin.Settings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="entityName" HelpText="Entity Name" ResourceKey="EntityName">Entity:</Label>
<div class="col-sm-9">
<input id="entityName" class="form-control" @bind="@_entityName" maxlength="256" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="entityId" HelpText="Entity Id" ResourceKey="EntityId">Id:</Label>
<div class="col-sm-9">
<input id="entityId" class="form-control" @bind="@_entityId" maxlength="256" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settingName" HelpText="Setting Name" ResourceKey="SettingName">Name:</Label>
<div class="col-sm-9">
<input id="settingName" class="form-control" @bind="@_settingName" maxlength="256" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settingValue" HelpText="Setting Value" ResourceKey="SettingValue">Value:</Label>
<div class="col-sm-9">
<input id="SettingValue" class="form-control" @bind="@_settingValue" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isPrivate" HelpText="Private" ResourceKey="IsPrivate">Private?</Label>
<div class="col-sm-9">
<select id="isPrivate" class="form-select" @bind="@_isPrivate">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveSetting">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
<br /><br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
</div>
</form>
@code {
private ElementReference form;
private bool validated = false;
private int _settingId;
private string _entityName;
private string _entityId;
private string _settingName;
private string _settingValue;
private string _isPrivate;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
_settingId = int.Parse(PageState.QueryString["id"]);
_entityName = PageState.QueryString["entity"];
try
{
var setting = await SettingService.GetSettingAsync(_entityName, _settingId);
if (setting != null)
{
_entityId = setting.EntityId.ToString();
_settingName = setting.SettingName;
_settingValue = setting.SettingValue;
_isPrivate = setting.IsPrivate.ToString();
_createdby = setting.CreatedBy;
_createdon = setting.CreatedOn;
_modifiedby = setting.ModifiedBy;
_modifiedon = setting.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Setting {SettingId} {Error}", _settingId, ex.Message);
AddModuleMessage(Localizer["Error.LoadSetting"], MessageType.Error);
}
}
private async Task SaveSetting()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var setting = await SettingService.GetSettingAsync(_entityName, _settingId);
setting.SettingValue = _settingValue;
setting.IsPrivate = (_isPrivate != null && Boolean.Parse(_isPrivate));
try
{
setting = await SettingService.UpdateSettingAsync(setting);
await logger.LogInformation("Setting Saved {Setting}", setting);
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Setting {Setting} {Error}", setting, ex.Message);
AddModuleMessage(Localizer["Error.SaveSetting"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -0,0 +1,56 @@
@namespace Oqtane.Modules.Admin.Settings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISettingService SettingService
@inject IStringLocalizer<ImportSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settings" HelpText="Provide settings in comma delimited format using the column template specified" ResourceKey="Settings">Settings:</Label>
<div class="col-sm-9">
<textarea id="settings" class="form-control" @bind="@_settings" rows="5" required></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Import">@Localizer["Import"]</button>&nbsp;
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _settings = "Entity,Id,Name,Value,Private\n";
public override string Title => "Import Settings";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private async Task Import()
{
try
{
if (!string.IsNullOrEmpty(_settings))
{
ShowProgressIndicator();
var result = await SettingService.ImportSettingsAsync(new Result { Message = _settings });
if (result.Success)
{
AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
}
else
{
AddModuleMessage(Localizer["Message.Import.Validation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Importing Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Import"], MessageType.Error);
}
}
}

View File

@ -0,0 +1,146 @@
@namespace Oqtane.Modules.Admin.Settings
@inherits ModuleBase
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add Setting" Security="SecurityAccessLevel.Host" ResourceKey="AddSetting" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_entityName, _entityId)))" />
<ActionLink Action="ImportSettings" Text="Import" Class="btn btn-secondary ms-1" Security="SecurityAccessLevel.Host" ResourceKey="ImportSettings" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_entityName, _entityId)))" />
</div>
<div class="col-sm-4">
<select class="form-select custom-select" value="@_entityName" @onchange="(e => EntityNameChanged(e))">
<option value="-">&lt;@Localizer["Select Entity"]&gt;</option>
@foreach (var entityName in _entityNames)
{
<option value="@entityName">@entityName</option>
}
</select>
</div>
<div class="col-sm-4">
<select class="form-select custom-select" value="@_entityId" @onchange="(e => EntityIdChanged(e))">
<option value="-">&lt;@Localizer["Select Id"]&gt;</option>
@foreach (var entityId in _entityIds)
{
<option value="@entityId">@entityId</option>
}
</select>
</div>
</div>
</div>
<br />
<Pager Items="@_settings" SearchProperties="SettingName,SettingValue">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@Localizer["Value"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Text="Edit" Parameters="@($"entity={context.EntityName}&id={context.SettingId}")" Security="SecurityAccessLevel.Host" ResourceKey="EditSetting" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_entityName, _entityId)))" /></td>
<td><ActionDialog Header="Delete Setting" Message="@string.Format(Localizer["Confirm.DeleteSetting"], context.SettingName)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteSetting(context))" ResourceKey="DeleteSetting" /></td>
<td>@context.SettingName</td>
<td>@context.SettingValue</td>
</Row>
</Pager>
@code {
private string _entityName = "-";
private List<string> _entityNames = new List<string>();
private string _entityId = "-";
private List<int> _entityIds = new List<int>();
private List<Setting> _settings = new List<Setting>();
public override string UrlParametersTemplate => "/{entityname}/{entityid}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
{
_entityNames = await SettingService.GetEntityNamesAsync();
if (UrlParameters.ContainsKey("entityname"))
{
_entityName = UrlParameters["entityname"];
await GetEntityIds();
}
if (UrlParameters.ContainsKey("entityid"))
{
_entityId = UrlParameters["entityid"];
await GetSettings();
}
}
private async Task GetEntityIds()
{
if (_entityName != "-")
{
_entityIds = await SettingService.GetEntityIdsAsync(_entityName);
}
else
{
_entityIds = new List<int>();
}
}
private async Task GetSettings()
{
if (_entityName != "-" && _entityId != "-")
{
_settings = await SettingService.GetSettingsAsync(_entityName, int.Parse(_entityId), "");
_settings = _settings.OrderBy(item => item.SettingName).ToList();
}
else
{
_settings = new List<Setting>();
}
}
private async void EntityNameChanged(ChangeEventArgs e)
{
try
{
_entityName = e.Value.ToString();
_entityId = "-";
await GetEntityIds();
await GetSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On EntityNameChanged");
}
}
private async void EntityIdChanged(ChangeEventArgs e)
{
try
{
_entityId = e.Value.ToString();
await GetSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On EntityIdChanged");
}
}
private async Task DeleteSetting(Setting setting)
{
try
{
await SettingService.DeleteSettingAsync(setting.EntityName, setting.EntityId, setting.SettingName);
await logger.LogInformation("Setting Deleted {Setting}", setting);
await GetSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Setting {Setting} {Error}", setting, ex.Message);
AddModuleMessage(Localizer["Error.DeleteSetting"], MessageType.Error);
}
}
}

View File

@ -10,6 +10,7 @@
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@ -41,6 +42,20 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label> <Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -50,6 +65,7 @@
</select> </select>
</div> </div>
</div> </div>
}
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared." ResourceKey="SiteMap">Site Map: </Label> <Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared." ResourceKey="SiteMap">Site Map: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -133,7 +149,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label> <Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" /> <FileManager FileId="@_logofileid" Filter="@_imagefiles" @ref="_logofilemanager" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -180,6 +196,17 @@
</Section> </Section>
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings"> <Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SmtpEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="smtpenabled" class="form-select" value="@_smtpenabled" @onchange="(e => SMTPEnabledChanged(e))">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-3"> <div class="col-sm-3">
</div> </div>
@ -200,14 +227,28 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="UseSsl">SSL Enabled: </Label> <Label Class="col-sm-3" For="smtpssl" HelpText="Specify the type of SSL connection for your SMTP server" ResourceKey="SmtpSSL">SSL Options: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl"> <select id="smtpssl" class="form-select" @bind="@_smtpssl">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="None">@Localizer["None"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="Auto">@Localizer["Auto"]</option>
<option value="StartTls">@Localizer["StartTls"]</option>
<option value="SslOnConnect">@Localizer["SslOnConnect"]</option>
<option value="StartTlsWhenAvailable">@Localizer["StartTlsWhenAvailable"]</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpauthentication" HelpText="Specify the SMTP authentication type" ResourceKey="SMTPAuthentication">Authentication: </Label>
<div class="col-sm-9">
<select id="smtpauthentication" class="form-select" value="@_smtpauthentication" @onchange="(e => SMTPAuthenticationChanged(e))">
<option value="Basic">@Localizer["Basic"]</option>
<option value="OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
@if (_smtpauthentication == "Basic")
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label> <Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -224,13 +265,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmtpSender">Email Sender: </Label> <Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required> <select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@ -238,13 +273,41 @@
</select> </select>
</div> </div>
</div> </div>
}
else
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SMTPEnabled">Enabled? </Label> <Label Class="col-sm-3" For="smtpauthority" HelpText="The Authority Url for the SMTP provider" ResourceKey="SmtpAuthority">Authority Url:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled"> <input id="smtpauthority" class="form-control" @bind="@_smtpauthority" />
<option value="True">@SharedLocalizer["Yes"]</option> </div>
<option value="False">@SharedLocalizer["No"]</option> </div>
</select> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientid" HelpText="The Client ID for the SMTP provider" ResourceKey="SmtpClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="smtpclientid" class="form-control" @bind="@_smtpclientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientsecret" HelpText="The Client Secret for the SMTP provider" ResourceKey="SmtpClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_smtpclientsecrettype" id="smtpclientsecret" class="form-control" @bind="@_smtpclientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSmtpClientSecret">@_togglesmtpclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpscopes" HelpText="A list of Scopes for the SMTP provider (separated by commas)" ResourceKey="SmtpScopes">Scopes:</Label>
<div class="col-sm-9">
<input id="smtpscopes" class="form-control" @bind="@_smtpscopes" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP provider." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -255,6 +318,7 @@
</div> </div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button> <button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br /> <br /><br />
}
</div> </div>
</Section> </Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings"> <Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@ -416,9 +480,11 @@
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages; private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private string _name = string.Empty; private string _name = string.Empty;
private string _homepageid = "-"; private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _isdeleted; private string _isdeleted;
private string _sitemap = ""; private string _sitemap = "";
private string _siteguid = ""; private string _siteguid = "";
@ -435,21 +501,28 @@
private Dictionary<string, string> _textEditors = new Dictionary<string, string>(); private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = ""; private string _textEditor = "";
private string _imageFiles = string.Empty; private string _imagefiles = string.Empty;
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "Auto";
private string _smtpusername = string.Empty; private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty; private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password"; private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty; private string _togglesmtppassword = string.Empty;
private string _smtpauthority = string.Empty;
private string _smtpclientid = string.Empty;
private string _smtpclientsecret = string.Empty;
private string _smtpclientsecrettype = "password";
private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False"; private string _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
@ -493,11 +566,13 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
_timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name; _name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null) if (site.HomePageId != null)
{ {
_homepageid = site.HomePageId.Value.ToString(); _homepageid = site.HomePageId.Value.ToString();
@ -517,9 +592,17 @@
{ {
_faviconfileid = site.FaviconFileId.Value; _faviconfileid = site.FaviconFileId.Value;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty); _cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);
@ -531,24 +614,35 @@
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName)); _textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
} }
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
_bodycontent = site.BodyContent; _bodycontent = site.BodyContent;
// SMTP // SMTP
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty); _smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty); _smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "Auto");
if (_smtpssl == "True") _smtpssl = "SslOnConnect";
if (_smtpssl == "False") _smtpssl = "StartTlsWhenAvailable";
_smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpauthority = SettingService.GetSetting(settings, "SMTPAuthority", string.Empty);
_smtpclientid = SettingService.GetSetting(settings, "SMTPClientId", string.Empty);
_smtpclientsecret = SettingService.GetSetting(settings, "SMTPClientSecret", string.Empty);
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False"); _smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
}
// PWA // PWA
_pwaisenabled = site.PwaIsEnabled.ToString(); _pwaisenabled = site.PwaIsEnabled.ToString();
@ -579,7 +673,8 @@
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
_database = _databases.Find(item => item.DBType == tenant.DBType && item.Name != "LocalDB")?.Name; // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_database = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
_connectionstring = tenant.DBConnectionString; _connectionstring = tenant.DBConnectionString;
} }
} }
@ -650,6 +745,7 @@
if (site != null) if (site != null)
{ {
site.Name = _name; site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
@ -724,16 +820,23 @@
// SMTP // SMTP
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true); settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
}
//cookie consent //cookie consent
settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent); settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent);
@ -741,6 +844,7 @@
// functionality // functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor); settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await logger.LogInformation("Site Settings Saved {Site}", site); await logger.LogInformation("Site Settings Saved {Site}", site);
@ -795,6 +899,46 @@
} }
} }
private void SMTPAuthenticationChanged(ChangeEventArgs e)
{
_smtpauthentication = (string)e.Value;
StateHasChanged();
}
private void SMTPEnabledChanged(ChangeEventArgs e)
{
_smtpenabled = (string)e.Value;
StateHasChanged();
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private void ToggleSmtpClientSecret()
{
if (_smtpclientsecrettype == "password")
{
_smtpclientsecrettype = "text";
_togglesmtpclientsecret = SharedLocalizer["HidePassword"];
}
else
{
_smtpclientsecrettype = "password";
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
}
}
private async Task SendEmail() private async Task SendEmail()
{ {
if (_smtphost != "" && _smtpport != "" && _smtpsender != "") if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@ -805,8 +949,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");
@ -827,20 +976,6 @@
} }
} }
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases() private async Task GetAliases()
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@ -216,7 +216,7 @@ else
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
} }
_urls = PageState.Alias.Name; _urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{ {
@ -237,7 +237,7 @@ else
} }
else else
{ {
_databaseName = "LocalDB"; _databaseName = Constants.DefaultDBName;
} }
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
@ -390,6 +390,8 @@ else
config.DatabaseType = tenant.DBType; config.DatabaseType = tenant.DBType;
config.ConnectionString = tenant.DBConnectionString; config.ConnectionString = tenant.DBConnectionString;
config.IsNewTenant = false; config.IsNewTenant = false;
config.HostEmail = PageState.User.Email;
config.HostName = PageState.User.DisplayName;
} }
} }
@ -403,6 +405,7 @@ else
config.SiteTemplate = _sitetemplatetype; config.SiteTemplate = _sitetemplatetype;
config.RenderMode = _rendermode; config.RenderMode = _rendermode;
config.Runtime = _runtime; config.Runtime = _runtime;
config.Register = false;
ShowProgressIndicator(); ShowProgressIndicator();

View File

@ -200,7 +200,8 @@ else
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
_databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType && item.Name != "LocalDB").Name; // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
} }
} }
else else
@ -211,7 +212,7 @@ else
} }
else else
{ {
_databasetype = "LocalDB"; _databasetype = Constants.DefaultDBName;
} }
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();

View File

@ -2,9 +2,13 @@
@inherits ModuleBase @inherits ModuleBase
@inject ISystemService SystemService @inject ISystemService SystemService
@inject IInstallationService InstallationService @inject IInstallationService InstallationService
@inject IMigrationHistoryService MigrationHistoryService
@inject ITenantService TenantService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized)
{
<TabStrip> <TabStrip>
<TabPanel Name="Info" Heading="Info" ResourceKey="Info"> <TabPanel Name="Info" Heading="Info" ResourceKey="Info">
<div class="container"> <div class="container">
@ -170,12 +174,38 @@
<br /><br /> <br /><br />
<button type="button" class="btn btn-danger" @onclick="ClearLog">@Localizer["Clear"]</button> <button type="button" class="btn btn-danger" @onclick="ClearLog">@Localizer["Clear"]</button>
</TabPanel> </TabPanel>
<TabPanel Name="Migrations" Heading="Migrations" ResourceKey="Migrations">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The name of the current database. Note that this is not the physical database name but rather the tenant name which is used within the framework to identify a database." ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
<input id="tenant" class="form-control" @bind="@_tenant" readonly />
</div>
</div>
</div>
<br />
<Pager Items="@_history" SearchProperties="MigrationId">
<Header>
<th>@Localizer["Migration"]</th>
<th>@Localizer["Date"]</th>
<th>@Localizer["Version"]</th>
</Header>
<Row>
<td>@context.MigrationId</td>
<td>@UtcToLocal(context.AppliedDate)</td>
<td>@context.AppliedVersion</td>
</Row>
</Pager>
</TabPanel>
</TabStrip> </TabStrip>
<br /><br /> <br /><br />
}
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private bool _initialized = false;
private string _version = string.Empty; private string _version = string.Empty;
private string _clrversion = string.Empty; private string _clrversion = string.Empty;
private string _osversion = string.Empty; private string _osversion = string.Empty;
@ -199,6 +229,9 @@
private string _log = string.Empty; private string _log = string.Empty;
private string _tenant = string.Empty;
private List<MigrationHistory> _history;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_version = Constants.Version; _version = Constants.Version;
@ -236,6 +269,12 @@
{ {
_log = systeminfo["Log"].ToString(); _log = systeminfo["Log"].ToString();
} }
var tenants = await TenantService.GetTenantsAsync();
_tenant = tenants.Find(item => item.TenantId == PageState.Site.TenantId).Name;
_history = await MigrationHistoryService.GetMigrationHistoryAsync();
_initialized = true;
} }
private async Task SaveConfig() private async Task SaveConfig()

View File

@ -101,6 +101,12 @@
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br /> <small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br /> @(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<br /> <br />
@if (_themes.Exists(item => item.PackageName == context.PackageId))
{
<button type="button" class="btn btn-info">@SharedLocalizer["Installed"]</button>
}
else
{
@if (!string.IsNullOrEmpty(context.PackageUrl)) @if (!string.IsNullOrEmpty(context.PackageUrl))
{ {
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button> <button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
@ -109,6 +115,7 @@
{ {
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a> <a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
} }
}
<br /> <br />
</div> </div>
</div> </div>
@ -171,6 +178,7 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private List<Theme> _themes;
private int _page = 1; private int _page = 1;
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
@ -187,7 +195,8 @@
{ {
try try
{ {
await LoadThemes(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
await LoadPackages();
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@ -197,24 +206,10 @@
} }
} }
private async Task LoadThemes() private async Task LoadPackages()
{ {
ShowProgressIndicator(); ShowProgressIndicator();
var themes = await ThemeService.GetThemesAsync();
_packages = await PackageService.GetPackagesAsync("theme", _search, _price, "", _sort); _packages = await PackageService.GetPackagesAsync("theme", _search, _price, "", _sort);
if (_packages != null)
{
foreach (Package package in _packages.ToArray())
{
if (themes.Exists(item => item.PackageName == package.PackageId))
{
_packages.Remove(package);
}
}
}
HideProgressIndicator(); HideProgressIndicator();
} }
@ -222,25 +217,25 @@
{ {
_price = price; _price = price;
_sort = "popularity"; _sort = "popularity";
await LoadThemes(); await LoadPackages();
StateHasChanged(); StateHasChanged();
} }
private async Task Search() private async Task Search()
{ {
await LoadThemes(); await LoadPackages();
} }
private async Task Reset() private async Task Reset()
{ {
_page = 1; _page = 1;
_search = ""; _search = "";
await LoadThemes(); await LoadPackages();
} }
private async Task Refresh() private async Task Refresh()
{ {
await LoadThemes(); await LoadPackages();
} }
private void OnPageChange(int page) private void OnPageChange(int page)
@ -251,7 +246,7 @@
private async void SortChanged(ChangeEventArgs e) private async void SortChanged(ChangeEventArgs e)
{ {
_sort = (string)e.Value; _sort = (string)e.Value;
await LoadThemes(); await LoadPackages();
} }
private void HideModal() private void HideModal()
@ -310,6 +305,6 @@
private void OnUpload() private void OnUpload()
{ {
AddModuleMessage(string.Format(Localizer["Success.Theme.Download"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Theme.Upload"], NavigateUrl("admin/system")), MessageType.Success);
} }
} }

View File

@ -9,10 +9,12 @@
@if (_initialized) @if (_initialized)
{ {
<TabStrip>
<TabPanel Name="Theme" ResourceKey="Theme" Heading="Theme">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label> <Label Class="col-sm-3" For="name" HelpText="The name of the theme" ResourceKey="Name">Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" /> <input id="name" class="form-control" @bind="@_name" />
</div> </div>
@ -55,7 +57,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the theme" ResourceKey="ReferenceUrl">Reference Url: </Label> <Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled /> <input id="url" class="form-control" @bind="@_url" disabled />
</div> </div>
@ -87,6 +89,18 @@
<br /> <br />
<br /> <br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Theme" PermissionNames="@PermissionNames.Utilize" PermissionList="@_permissions" @ref="_permissionGrid" />
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
} }
@code { @code {
@ -103,11 +117,14 @@
private string _url = ""; private string _url = "";
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private PermissionGrid _permissionGrid;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -126,6 +143,7 @@
_url = theme.Url; _url = theme.Url;
_contact = theme.Contact; _contact = theme.Contact;
_license = theme.License; _license = theme.License;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy; _createdby = theme.CreatedBy;
_createdon = theme.CreatedOn; _createdon = theme.CreatedOn;
_modifiedby = theme.ModifiedBy; _modifiedby = theme.ModifiedBy;
@ -152,6 +170,7 @@
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
theme.Name = _name; theme.Name = _name;
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
theme.PermissionList = _permissionGrid.GetPermissionList();
await ThemeService.UpdateThemeAsync(theme); await ThemeService.UpdateThemeAsync(theme);
await logger.LogInformation("Theme Saved {Theme}", theme); await logger.LogInformation("Theme Saved {Theme}", theme);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -15,12 +15,11 @@
else else
{ {
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" /> <ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
@((MarkupString)"&nbsp;") <ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ms-1" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary" /> <button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<Pager Items="@_themes"> <Pager Items="@_themes">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
@ -38,7 +37,6 @@ else
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" /> <ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
} }
</td> </td>
<td><NavLink class="btn btn-secondary" href="@NavigateUrl("admin/site")">@Localizer["Assign"]</NavLink></td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Version</td> <td>@context.Version</td>
<td> <td>
@ -80,7 +78,7 @@ else
{ {
try try
{ {
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackageUpdatesAsync("theme"); _packages = await PackageService.GetPackageUpdatesAsync("theme");
} }
catch (Exception ex) catch (Exception ex)
@ -163,7 +161,7 @@ else
{ {
try try
{ {
await ThemeService.DeleteThemeAsync(Theme.ThemeName); await ThemeService.DeleteThemeAsync(Theme.ThemeId, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success); AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
} }
@ -173,4 +171,27 @@ else
AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error); AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error);
} }
} }
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var theme in _themes)
{
if (!string.IsNullOrEmpty(theme.PackageName) && !_packages.Any(item => item.PackageId == theme.PackageName))
{
await PackageService.GetPackageAsync(theme.PackageName, theme.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Theme.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Themes {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Theme.Synchronize"], MessageType.Error);
}
}
} }

View File

@ -13,17 +13,33 @@
<TabPanel Name="Download" ResourceKey="Download"> <TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable) @if (_package != null && _upgradeavailable)
{ {
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage> <div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
@if (!_downloaded)
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button> <button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
}
else
{
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
} }
}
else else
{ {
<ModuleMessage Type="MessageType.Info" Message=@Localizer["Message.Text"]></ModuleMessage> <ModuleMessage Type="MessageType.Info" Message=@Localizer["Message.Text"]></ModuleMessage>
} }
</TabPanel> </TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload"> <TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label> <Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
@ -31,7 +47,17 @@
<FileManager Folder="@Constants.PackagesFolder" /> <FileManager Folder="@Constants.PackagesFolder" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div> </div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
@ -39,8 +65,10 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private bool _downloaded = false;
private Package _package; private Package _package;
private bool _upgradeavailable = false; private bool _upgradeavailable = false;
private string _backup = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -86,7 +114,7 @@
ShowProgressIndicator(); ShowProgressIndicator();
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10); await interop.RedirectBrowser(NavigateUrl(), 10);
await InstallationService.Upgrade(); await InstallationService.Upgrade(bool.Parse(_backup));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -102,6 +130,7 @@
ShowProgressIndicator(); ShowProgressIndicator();
await PackageService.DownloadPackageAsync(packageid, version); await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version); await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
_downloaded = true;
HideProgressIndicator(); HideProgressIndicator();
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success); AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
} }

View File

@ -8,13 +8,16 @@
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The fully qualified Url for this site" ResourceKey="Url">Url:</Label> <Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required /> <input id="url" class="form-control" @bind="@_url" maxlength="500" required />
<button type="button" class="btn btn-primary" @onclick="GenerateUrl">@Localizer["Generate"]</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label> <Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required /> <input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div> </div>
@ -48,13 +51,15 @@
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url; _url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url; _url = (!_url.StartsWith("http")) ? url + _url : _url;
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
if (_url.StartsWith(url)) if (_url.StartsWith(url))
{ {
var urlmapping = new UrlMapping(); var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId; urlmapping.SiteId = PageState.Site.SiteId;
var route = new Route(_url, PageState.Alias.Path); urlmapping.Url = new Route(_url, PageState.Alias.Path).PagePath;
urlmapping.Url = route.PagePath; urlmapping.MappedUrl = _mappedurl;
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.Requests = 0; urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow; urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow; urlmapping.RequestedOn = DateTime.UtcNow;
@ -86,4 +91,18 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
} }
} }
private void GenerateUrl()
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
var chars = "abcdefghijklmnopqrstuvwxyz";
Random rnd = new Random();
for (int i = 0; i < 5; i++)
{
url += chars.Substring(rnd.Next(0, chars.Length - 1), 1);
}
_url = url;
}
} }

View File

@ -8,13 +8,13 @@
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="A fully qualified Url for this site" ResourceKey="Url">Url:</Label> <Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" readonly /> <input id="url" class="form-control" @bind="@_url" maxlength="500" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label> <Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required /> <input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div> </div>
@ -67,8 +67,11 @@
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid); var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
urlmapping.MappedUrl = _mappedurl.Replace(url, ""); urlmapping.MappedUrl = _mappedurl;
urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping); urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -48,7 +48,7 @@ else
} }
</td> </td>
<td>@context.Requests</td> <td>@context.Requests</td>
<td>@context.RequestedOn</td> <td>@UtcToLocal(context.RequestedOn)</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>

View File

@ -9,6 +9,7 @@
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IFileService FileService @inject IFileService FileService
@inject IFolderService FolderService @inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject IJSRuntime jsRuntime @inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@ -16,9 +17,9 @@
@if (_initialized) @if (_initialized)
{ {
@if (PageState.User != null && photo != null) @if (PageState.User != null && _photo != null)
{ {
<img src="@ImageUrl(photofileid, 400, 400)" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block"> <img src="@ImageUrl(_photofileid, 400, 400)" alt="@_displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
} }
else else
{ {
@ -31,7 +32,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label> <Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly /> <input id="username" class="form-control" @bind="@_username" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -47,17 +48,17 @@
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@if (allowtwofactor) @if (_allowtwofactor)
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label> <Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required> <select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@ -67,19 +68,31 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label> <Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label> <Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" /> <select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@_photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@_folderid" @ref="_filemanager" />
</div> </div>
</div> </div>
</div> </div>
@ -91,17 +104,17 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
@p.Category @p.Category
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@ -113,13 +126,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option)) var values = option.Split(':');
var name = values[0];
var value = values.Length > 1 ? values[1] : values[0];
@if (GetProfileValue(p.Name, "") == name || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == name))
{ {
<option value="@option" selected>@option</option> <option value="@name" selected>@value</option>
} }
else else
{ {
<option value="@option">@option</option> <option value="@name">@value</option>
} }
} }
</select> </select>
@ -129,13 +145,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option)) var values = option.Split(':');
var name = values[0];
var value = values.Length > 1 ? values[1] : values[0];
@if (GetProfileValue(p.Name, "") == name || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == name))
{ {
<option value="@option" selected>@option</option> <option value="@name" selected>@value</option>
} }
else else
{ {
<option value="@option">@option</option> <option value="@name">@value</option>
} }
} }
</select> </select>
@ -220,11 +239,11 @@
<option value="from">@Localizer["Items.Sent"]</option> <option value="from">@Localizer["Items.Sent"]</option>
</select> </select>
<br /> <br />
@if (filter == "to") @if (_filter == "to")
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
@ -260,15 +279,15 @@
context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", ""); context.Body = context.Body.Replace("\r", "");
} }
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
} }
@if (context.IsRead) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@ -285,9 +304,9 @@
} }
else else
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;"></th> <th style="width: 1px;"></th>
<th style="width: 1px;"></th> <th style="width: 1px;"></th>
@ -324,15 +343,15 @@
context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", ""); context.Body = context.Body.Replace("\r", "");
} }
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
} }
@if (context.IsRead) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@ -356,27 +375,30 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private string username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string _confirm = string.Empty;
private bool allowtwofactor = false; private bool _allowtwofactor = false;
private string twofactor = "False"; private string _twofactor = "False";
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _displayname = string.Empty;
private FileManager filemanager; private FileManager _filemanager;
private int folderid = -1; private int _folderid = -1;
private int photofileid = -1; private List<Models.TimeZone> _timezones;
private File photo = null; private string _timezoneid = string.Empty;
private string _ImageFiles = string.Empty; private int _photofileid = -1;
private List<Profile> profiles; private File _photo = null;
private Dictionary<string, string> userSettings; private string _imagefiles = string.Empty;
private string category = string.Empty;
private string filter = "to"; private List<Profile> _profiles;
private List<Notification> notifications; private Dictionary<string, string> _userSettings;
private string notificationSummary = string.Empty; private string _category = string.Empty;
private string _filter = "to";
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
@ -386,43 +408,49 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_timezones = TimeZoneService.GetTimeZones();
if (PageState.User != null) if (PageState.User != null)
{ {
username = PageState.User.Username; _username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString(); _twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email; _email = PageState.User.Email;
displayname = PageState.User.DisplayName; _displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder // get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
{ {
folderid = folder.FolderId; _folderid = folder.FolderId;
} }
if (PageState.User.PhotoFileId != null) if (PageState.User.PhotoFileId != null)
{ {
photofileid = PageState.User.PhotoFileId.Value; _photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
userSettings = PageState.User.Settings; _userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_ImageFiles = SettingService.GetSetting(userSettings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
@ -442,13 +470,13 @@
private async Task LoadNotificationsAsync() private async Task LoadNotificationsAsync()
{ {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); _notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); _notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -460,38 +488,39 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) if (ValidateProfiles())
{ {
var user = PageState.User; var user = PageState.User;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor); user.TwoFactorRequired = bool.Parse(_twofactor);
user.Email = email; user.Email = _email;
user.DisplayName = (displayname == string.Empty ? username : displayname); user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.PhotoFileId = filemanager.GetFileId(); user.TimeZoneId = _timezoneid;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {
user.PhotoFileId = null; user.PhotoFileId = null;
} }
if (user.PhotoFileId != null) if (user.PhotoFileId != null)
{ {
photofileid = user.PhotoFileId.Value; _photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved"); await logger.LogInformation("User Profile Saved");
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
@ -557,12 +586,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); _userSettings = SettingService.SetSetting(_userSettings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -594,7 +623,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
} }
private async Task Delete(Notification Notification) private async Task Delete(Notification Notification)
@ -624,7 +653,7 @@
private async void FilterChanged(ChangeEventArgs e) private async void FilterChanged(ChangeEventArgs e)
{ {
filter = (string)e.Value; _filter = (string)e.Value;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
StateHasChanged(); StateHasChanged();
} }
@ -634,7 +663,7 @@
try try
{ {
ShowProgressIndicator(); ShowProgressIndicator();
foreach(var Notification in notifications) foreach(var Notification in _notifications)
{ {
if (!Notification.IsDeleted) if (!Notification.IsDeleted)
{ {

View File

@ -5,6 +5,7 @@
@inject IUserService UserService @inject IUserService UserService
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -12,7 +13,7 @@
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity"> <TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null) @if (_profiles != null)
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -27,12 +28,33 @@
<input id="email" class="form-control" @bind="@_email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label> <Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -48,15 +70,15 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@ -66,13 +88,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option)) var values = option.Split(':');
var name = values[0];
var value = values.Length > 1 ? values[1] : values[0];
@if (GetProfileValue(p.Name, "") == name || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == name))
{ {
<option value="@option" selected>@option</option> <option value="@name" selected>@value</option>
} }
else else
{ {
<option value="@option">@option</option> <option value="@name">@value</option>
} }
} }
</select> </select>
@ -103,14 +128,17 @@
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True"; private string _notify = "True";
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> settings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -118,8 +146,19 @@
{ {
try try
{ {
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _timezones = TimeZoneService.GetTimeZones();
settings = new Dictionary<string, string>(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@ -131,7 +170,7 @@
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -152,7 +191,9 @@
user.Username = _username; user.Username = _username;
user.Password = ""; // will be auto generated user.Password = ""; // will be auto generated
user.Email = _email; user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null; user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify); user.SuppressNotification = !bool.Parse(_notify);
@ -160,7 +201,7 @@
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Created {User}", user); await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -185,12 +226,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue); _settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -217,6 +258,6 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value); _settings = SettingService.SetSetting(_settings, SettingName, value);
} }
} }

View File

@ -6,6 +6,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -17,13 +18,13 @@
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" /> <ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label> <Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly /> <input id="username" class="form-control" @bind="@_username" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
@ -32,32 +33,53 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label> <Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" /> <select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName">Full Name:</Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label> <Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted">Deleted?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted"> <select id="isdeleted" class="form-select" @bind="@_isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@ -65,15 +87,15 @@
</div> </div>
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label> <Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin">Last Login:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly /> <input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label> <Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress">Last IP Address:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly /> <input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
</div> </div>
</div> </div>
</div> </div>
@ -81,15 +103,15 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@ -99,13 +121,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option)) var values = option.Split(':');
var name = values[0];
var value = values.Length > 1 ? values[1] : values[0];
@if (GetProfileValue(p.Name, "") == name || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == name))
{ {
<option value="@option" selected>@option</option> <option value="@name" selected>@value</option>
} }
else else
{ {
<option value="@option">@option</option> <option value="@name">@value</option>
} }
} }
</select> </select>
@ -128,47 +153,50 @@
</div> </div>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !_ishost && _isdeleted != "True")
{ {
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button> <button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
} }
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
{ {
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
} }
<br /><br /> <br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
} }
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private int userid; private int _userid;
private string username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string _confirm = string.Empty;
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _confirmed = string.Empty;
private string isdeleted; private string _displayname = string.Empty;
private string lastlogin; private string _timezoneid = string.Empty;
private string lastipaddress; private string _isdeleted;
private bool ishost = false; private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> userSettings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
private string createdby; private string _createdby;
private DateTime createdon; private DateTime _createdon;
private string modifiedby; private string _modifiedby;
private DateTime modifiedon; private DateTime _modifiedon;
private string deletedby; private string _deletedby;
private DateTime? deletedon; private DateTime? _deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -178,29 +206,41 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_timezones = TimeZoneService.GetTimeZones();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{ {
userid = UserId; _userid = UserId;
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
username = user.Username; _username = user.Username;
email = user.Email; _email = user.Email;
displayname = user.DisplayName; _confirmed = user.EmailConfirmed.ToString();
isdeleted = user.IsDeleted.ToString(); _displayname = user.DisplayName;
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); _timezoneid = PageState.User.TimeZoneId;
lastipaddress = user.LastIPAddress; _isdeleted = user.IsDeleted.ToString();
ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
userSettings = user.Settings; _settings = user.Settings;
createdby = user.CreatedBy; _createdby = user.CreatedBy;
createdon = user.CreatedOn; _createdon = user.CreatedOn;
modifiedby = user.ModifiedBy; _modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn; _modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy; _deletedby = user.DeletedBy;
deletedon = user.DeletedOn; _deletedon = user.DeletedOn;
} }
} }
@ -208,14 +248,14 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message); await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -227,27 +267,29 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) if (ValidateProfiles())
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
} }
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Saved {User}", user); await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -269,7 +311,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message); await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
} }
} }
@ -278,17 +320,17 @@
{ {
try try
{ {
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username); await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", _username, PageState.User.Username);
// post back to the server so that the cookies are set correctly // post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path }; var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, returnurl = PageState.Alias.Path };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message); await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
} }
} }
@ -297,9 +339,9 @@
{ {
try try
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId)
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user); await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
@ -307,19 +349,19 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
} }
} }
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); _settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -346,7 +388,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _settings = SettingService.SetSetting(_settings, SettingName, value);
} }
private void TogglePassword() private void TogglePassword()

View File

@ -17,8 +17,21 @@ else
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users"> <TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp; <ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Text="Import Users" Class="btn btn-secondary ms-2" Action="Users" Security="SecurityAccessLevel.Admin" ResourceKey="ImportUsers" /> <ActionLink Text="Import Users" Class="btn btn-secondary ms-2" Action="Users" Security="SecurityAccessLevel.Admin" ResourceKey="ImportUsers" />
</div>
<div class="col-sm-6">
<select id="deleted" class="form-select custom-select" value="@_deleted" @onchange="(e => DeletedChanged(e))">
<option value="false">@Localizer["Active Users"]</option>
<option value="true">@Localizer["Deleted Users"]</option>
</select>
</div>
</div>
</div>
<br />
<Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName"> <Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Header> <Header>
@ -43,7 +56,7 @@ else
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@context.User.DisplayName</td> <td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td> <td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td> <td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", UtcToLocal(context.User.LastLoginOn)) : "")</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
@ -59,31 +72,34 @@ else
</select> </select>
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_allowregistration == "true")
{
@if (_providertype != "")
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label> <Label Class="col-sm-3" For="registerurl" HelpText="Optionally provide a custom registration url" ResourceKey="RegisterUrl">Register Url:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin"> <input id="registerurl" class="form-control" @bind="@_registerurl" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileurl" HelpText="Optionally provide a custom profile url" ResourceKey="ProfileUrl">Profile Url:</Label>
<div class="col-sm-9">
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireconfirmedemail" HelpText="Do you want to require registered users to verify their email address before they are allowed to log in?" ResourceKey="RequireConfirmedEmail">Require Verified Email?</Label>
<div class="col-sm-9">
<select id="requireconfirmedemail" class="form-select" @bind="@_requireconfirmedemail">
<option value="true">@SharedLocalizer["Yes"]</option> <option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option> <option value="false">@SharedLocalizer["No"]</option>
</select> </select>
</div> </div>
</div> </div>
} @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
else
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label> <Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor Authentication?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor"> <select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option> <option value="false">@Localizer["Disabled"]</option>
@ -98,6 +114,12 @@ else
<input id="cookiename" class="form-control" @bind="@_cookiename" /> <input id="cookiename" class="form-control" @bind="@_cookiename" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiedomain" HelpText="If you would like to share cookies across subdomains you will need to specify a root domain with a leading dot (ie. '.example.com')" ResourceKey="CookieDomain">Cookie Domain:</Label>
<div class="col-sm-9">
<input id="cookiedomain" class="form-control" @bind="@_cookiedomain" />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieexpiration" HelpText="You can choose to use a custom authentication cookie expiration timespan for each site (e.g. '08:00:00' for 8 hours). The default is 14 days if not specified." ResourceKey="CookieExpiration">Cookie Expiration Timespan:</Label> <Label Class="col-sm-3" For="cookieexpiration" HelpText="You can choose to use a custom authentication cookie expiration timespan for each site (e.g. '08:00:00' for 8 hours). The default is 14 days if not specified." ResourceKey="CookieExpiration">Cookie Expiration Timespan:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -298,6 +320,24 @@ else
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirenonce" HelpText="Specify if Nonce validation is required for the ID token (the default is true)" ResourceKey="RequireNonce">Require Nonce?</Label>
<div class="col-sm-9">
<select id="requirenonce" class="form-select" @bind="@_requirenonce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="singlelogout" HelpText="Specify if users should be logged out of both the application and provider (the default is false indicating they will only be logged out of the application)" ResourceKey="SingleLogout">Use Single Logout?</Label>
<div class="col-sm-9">
<select id="singlelogout" class="form-select" @bind="@_singlelogout" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label> <Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
@ -421,6 +461,27 @@ else
</select> </select>
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowhostrole" HelpText="Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation." ResourceKey="AllowHostRole">Allow Host Role?</Label>
<div class="col-sm-9">
<select id="allowhostrole" class="form-select" @bind="@_allowhostrole" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Local Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
} }
</Section> </Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings"> <Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
@ -471,11 +532,15 @@ else
@code { @code {
private List<UserRole> users; private List<UserRole> users;
private string _deleted = "false";
private string _allowregistration; private string _allowregistration;
private string _allowsitelogin; private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor; private string _twofactor;
private string _cookiename; private string _cookiename;
private string _cookiedomain;
private string _cookieexpiration; private string _cookieexpiration;
private string _alwaysremember; private string _alwaysremember;
private string _logouteverywhere; private string _logouteverywhere;
@ -503,6 +568,8 @@ else
private string _clientsecrettype = "password"; private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty; private string _toggleclientsecret = string.Empty;
private string _authresponsetype; private string _authresponsetype;
private string _requirenonce;
private string _singlelogout;
private string _scopes; private string _scopes;
private string _parameters; private string _parameters;
private string _pkce; private string _pkce;
@ -520,6 +587,8 @@ else
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
private string _verifyusers; private string _verifyusers;
private string _allowhostrole;
private string _allowsitelogin;
private string _secret; private string _secret;
private string _secrettype = "password"; private string _secrettype = "password";
@ -536,16 +605,19 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await LoadUsersAsync(true); await LoadUsersAsync();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application"); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", "");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", ""); _cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
_alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false"); _alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false");
_logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false"); _logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false");
@ -585,6 +657,8 @@ else
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "true");
_singlelogout = SettingService.GetSetting(settings, "ExternalLogin:SingleLogout", "false");
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
@ -602,11 +676,11 @@ else
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
} }
private async Task LoadUsersAsync(bool load) private async Task LoadUsersAsync()
{
if (load)
{ {
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered); users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -615,6 +689,20 @@ else
users.AddRange(hosts); users.AddRange(hosts);
users = users.OrderBy(u => u.User.DisplayName).ToList(); users = users.OrderBy(u => u.User.DisplayName).ToList();
} }
users = users.Where(item => item.User.IsDeleted == bool.Parse(_deleted)).ToList();
}
private async void DeletedChanged(ChangeEventArgs e)
{
try
{
_deleted = e.Value.ToString();
await LoadUsersAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On DeletedChanged");
} }
} }
@ -640,7 +728,7 @@ else
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
} }
AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success); AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success);
await LoadUsersAsync(true); await LoadUsersAsync();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -659,12 +747,15 @@ else
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false); settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false); settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false);
@ -690,6 +781,8 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RequireNonce", _requirenonce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:SingleLogout", _singlelogout, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
@ -705,6 +798,8 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);

View File

@ -101,8 +101,8 @@
_url = visitor.Url; _url = visitor.Url;
_referrer = visitor.Referrer; _referrer = visitor.Referrer;
_visits = visitor.Visits.ToString(); _visits = visitor.Visits.ToString();
_visited = visitor.VisitedOn.ToString(CultureInfo.CurrentCulture); _visited = UtcToLocal(visitor.VisitedOn).Value.ToString(CultureInfo.CurrentCulture);
_created = visitor.CreatedOn.ToString(CultureInfo.CurrentCulture); _created = UtcToLocal(visitor.CreatedOn).Value.ToString(CultureInfo.CurrentCulture);
if (visitor.UserId != null) if (visitor.UserId != null)
{ {

View File

@ -53,8 +53,8 @@ else
</td> </td>
<td>@context.Language</td> <td>@context.Language</td>
<td>@context.Visits</td> <td>@context.Visits</td>
<td>@context.VisitedOn</td> <td>@UtcToLocal(context.VisitedOn)</td>
<td>@context.CreatedOn</td> <td>@UtcToLocal(context.CreatedOn)</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
@ -100,6 +100,18 @@ else
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel> </TabPanel>
<TabPanel Name="Robots" Heading="Robots.txt" ResourceKey="Robots">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="robots" HelpText="Specify your robots.txt instructions to provide bots with guidance on which parts of your site should be indexed" ResourceKey="Robots">Instructions: </Label>
<div class="col-sm-9">
<textarea id="robots" class="form-control" @bind="@_robots" rows="3"></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip> </TabStrip>
} }
@ -113,6 +125,7 @@ else
private string _filter = ""; private string _filter = "";
private int _retention = 30; private int _retention = 30;
private string _correlation = "true"; private string _correlation = "true";
private string _robots = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -139,6 +152,7 @@ else
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); _filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true"); _correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
_robots = SettingService.GetSetting(settings, "Robots", "");
} }
private async void TypeChanged(ChangeEventArgs e) private async void TypeChanged(ChangeEventArgs e)
@ -191,6 +205,7 @@ else
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true); settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
settings = SettingService.SetSetting(settings, "Robots", _robots, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);

View File

@ -52,7 +52,7 @@
if (CreatedOn != null) if (CreatedOn != null)
{ {
_text += $" {Localizer["On"]} <b>{CreatedOn.Value.ToString(DateTimeFormat)}</b>"; _text += $" {Localizer["On"]} <b>{UtcToLocal(CreatedOn).Value.ToString(DateTimeFormat)}</b>";
} }
_text += "</p>"; _text += "</p>";
@ -69,7 +69,7 @@
if (ModifiedOn != null) if (ModifiedOn != null)
{ {
_text += $" {Localizer["On"]} <b>{ModifiedOn.Value.ToString(DateTimeFormat)}</b>"; _text += $" {Localizer["On"]} <b>{UtcToLocal(ModifiedOn).Value.ToString(DateTimeFormat)}</b>";
} }
_text += "</p>"; _text += "</p>";
@ -86,7 +86,7 @@
if (DeletedOn != null) if (DeletedOn != null)
{ {
_text += $" {Localizer["On"]} <b>{DeletedOn.Value.ToString(DateTimeFormat)}</b>"; _text += $" {Localizer["On"]} <b>{UtcToLocal(DeletedOn).Value.ToString(DateTimeFormat)}</b>";
} }
_text += "</p>"; _text += "</p>";

View File

@ -107,7 +107,7 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private List<Folder> _folders; private List<Folder> _folders = new List<Folder>();
private List<File> _files = new List<File>(); private List<File> _files = new List<File>();
private string _fileinputid = string.Empty; private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty; private string _progressinfoid = string.Empty;
@ -157,12 +157,22 @@
[Parameter] [Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - default false
[Parameter] [Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter] [Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
[Parameter]
public EventCallback<int> OnSelectFolder { get; set; } // optional - executes a method in the calling component when a folder is selected
[Parameter]
public EventCallback<int> OnSelectFile { get; set; } // optional - executes a method in the calling component when a file is selected
[Obsolete("Use OnSelectFile instead.")]
[Parameter] [Parameter]
public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected
@ -188,8 +198,10 @@
Filter = "nupkg"; Filter = "nupkg";
ShowSuccess = true; ShowSuccess = true;
} }
else
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder) {
// folder path specified rather than folderid
if (!string.IsNullOrEmpty(Folder))
{ {
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder); Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
if (folder != null) if (folder != null)
@ -203,6 +215,7 @@
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
}
if (ShowFolders) if (ShowFolders)
{ {
@ -235,25 +248,24 @@
} }
} }
await SetImage();
if (!string.IsNullOrEmpty(Filter)) if (!string.IsNullOrEmpty(Filter))
{ {
_filter = "." + Filter.Replace(",", ",."); _filter = "." + Filter.Replace(",", ",.");
} }
GetFolderPermission();
await SetImage();
await GetFiles(); await GetFiles();
_initialized = true; _initialized = true;
} }
private async Task GetFiles() private void GetFolderPermission()
{ {
_haseditpermission = false; _haseditpermission = false;
if (Folder == Constants.PackagesFolder) if (Folder == Constants.PackagesFolder)
{ {
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); _haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = new List<File>();
} }
else else
{ {
@ -261,62 +273,12 @@
if (folder != null) if (folder != null)
{ {
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList); _haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList);
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Browse, folder.PermissionList))
{
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_files = new List<File>();
}
} }
else else
{ {
_haseditpermission = false; _haseditpermission = false;
_files = new List<File>();
}
if (_filter != "*")
{
List<File> filtered = new List<File>();
foreach (File file in _files)
{
if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1)
{
filtered.Add(file);
} }
} }
_files = filtered;
}
}
}
private async Task FolderChanged(ChangeEventArgs e)
{
_message = string.Empty;
try
{
FolderId = int.Parse((string)e.Value);
await GetFiles();
FileId = -1;
_file = null;
_image = string.Empty;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
_message = Localizer["Error.File.Load"];
_messagetype = MessageType.Error;
}
}
private async Task FileChanged(ChangeEventArgs e)
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
await SetImage();
await OnSelect.InvokeAsync(FileId);
StateHasChanged();
} }
private async Task SetImage() private async Task SetImage()
@ -342,6 +304,74 @@
} }
} }
private async Task GetFiles()
{
if (ShowFiles)
{
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Browse, folder.PermissionList))
{
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_files = new List<File>();
}
}
else
{
_files = new List<File>();
}
if (_filter != "*")
{
List<File> filtered = new List<File>();
foreach (File file in _files)
{
if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1)
{
filtered.Add(file);
}
}
_files = filtered;
}
}
}
private async Task FolderChanged(ChangeEventArgs e)
{
_message = string.Empty;
try
{
FolderId = int.Parse((string)e.Value);
FileId = -1;
GetFolderPermission();
await SetImage();
await GetFiles();
await OnSelectFolder.InvokeAsync(FolderId);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
_message = Localizer["Error.File.Load"];
_messagetype = MessageType.Error;
}
}
private async Task FileChanged(ChangeEventArgs e)
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
StateHasChanged();
}
private async Task UploadFiles() private async Task UploadFiles()
{ {
_message = string.Empty; _message = string.Empty;
@ -396,7 +426,7 @@
} }
// upload files // upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token); var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token);
// reset progress indicators // reset progress indicators
if (ShowProgress) if (ShowProgress)
@ -418,32 +448,35 @@
_message = Localizer["Success.File.Upload"]; _message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
} }
}
else
{
await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
if (Folder == Constants.PackagesFolder) FileId = -1;
{ if (Folder != Constants.PackagesFolder && !AnonymizeUploadFilenames)
await OnUpload.InvokeAsync(-1);
}
else
{ {
// set FileId to first file in upload collection // set FileId to first file in upload collection
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0].Split(":")[0]); var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0].Split(":")[0]);
if (file != null) if (file != null)
{ {
FileId = file.FileId; FileId = file.FileId;
await SetImage();
await OnSelect.InvokeAsync(FileId);
await OnUpload.InvokeAsync(FileId);
} }
}
await SetImage();
await OnUpload.InvokeAsync(FileId);
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await GetFiles(); await GetFiles();
StateHasChanged(); StateHasChanged();
} }
else
{
await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -456,7 +489,6 @@
finally { finally {
tokenSource.Dispose(); tokenSource.Dispose();
} }
} }
else else
{ {
@ -486,10 +518,15 @@
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
} }
await GetFiles();
FileId = -1; FileId = -1;
await SetImage(); await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await GetFiles();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -515,15 +552,16 @@
public async Task Refresh(int fileId) public async Task Refresh(int fileId)
{ {
await GetFiles(); await GetFiles();
FileId = -1;
if (fileId != -1) if (fileId != -1)
{ {
var file = _files.Where(item => item.FileId == fileId).FirstOrDefault(); var file = _files.Where(item => item.FileId == fileId).FirstOrDefault();
if (file != null) if (file != null)
{ {
FileId = file.FileId; FileId = file.FileId;
}
}
await SetImage(); await SetImage();
}
}
StateHasChanged(); StateHasChanged();
} }
} }

View File

@ -3,6 +3,8 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (!string.IsNullOrEmpty(Message)) @if (!string.IsNullOrEmpty(Message))
{
@if (_style == MessageStyle.Alert)
{ {
<div class="@_classname alert-dismissible fade show mb-3" role="alert"> <div class="@_classname alert-dismissible fade show mb-3" role="alert">
@((MarkupString)Message) @((MarkupString)Message)
@ -10,23 +12,35 @@
{ {
<NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink> <NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink>
} }
@if (ModuleState != null) <form method="post" class="app-form-inline" @formname="ModuleMessageForm" @onsubmit="CloseMessage" data-enhance>
{ <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@if (ModuleState.RenderMode == RenderModes.Static) <button type="submit" class="btn-close" data-dismiss="alert" aria-label="close"></button>
{ </form>
<a href="@NavigationManager.Uri" class="btn-close" data-dismiss="alert" aria-label="close"></a>
}
else
{
<button type="button" class="btn-close" data-dismiss="alert" aria-label="close" @onclick="CloseMessage"></button>
}
}
</div> </div>
} }
@if (_style == MessageStyle.Toast)
{
<div class="app-modulemessage-toast bottom-0 end-0">
<div class="@_classname alert-dismissible fade show mb-3" role="alert">
@((MarkupString)Message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink>
}
<form method="post" class="app-form-inline" @formname="ModuleMessageForm" @onsubmit="CloseMessage" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn-close" data-dismiss="alert" aria-label="close"></button>
</form>
</div>
</div>
}
}
@code { @code {
private string _message = string.Empty; private string _message = string.Empty;
private string _classname = string.Empty; private string _classname = string.Empty;
private MessageStyle _style;
[Parameter] [Parameter]
public string Message { get; set; } public string Message { get; set; }
@ -34,6 +48,9 @@
[Parameter] [Parameter]
public MessageType Type { get; set; } public MessageType Type { get; set; }
[Parameter]
public MessageStyle Style { get; set; } = MessageStyle.Alert;
[Parameter] [Parameter]
public RenderModeBoundary Parent { get; set; } public RenderModeBoundary Parent { get; set; }
@ -43,6 +60,11 @@
if (!string.IsNullOrEmpty(_message)) if (!string.IsNullOrEmpty(_message))
{ {
_classname = GetMessageType(Type); _classname = GetMessageType(Type);
_style = Style;
if (Type == MessageType.Error)
{
_style = MessageStyle.Alert; // errors should always be displayed as alerts
}
} }
} }
@ -67,7 +89,8 @@
return classname; return classname;
} }
private void CloseMessage(MouseEventArgs e)
private void CloseMessage()
{ {
if (Parent != null) if (Parent != null)
{ {

View File

@ -28,7 +28,7 @@
@foreach (var permissionname in _permissionnames) @foreach (var permissionname in _permissionnames)
{ {
<td style="text-align: center;"> <td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, role.Name, -1) Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" /> <TriStateCheckBox Value="@GetPermissionValue(permissionname, role.Name, -1)" Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td> </td>
} }
</tr> </tr>
@ -60,11 +60,11 @@
@foreach (User user in _users) @foreach (User user in _users)
{ {
<tr> <tr>
<td>@user.DisplayName</td> <td>@user.DisplayName (@user.Username)</td>
@foreach (var permissionname in _permissionnames) @foreach (var permissionname in _permissionnames)
{ {
<td style="text-align: center; width: 1px;"> <td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" /> <TriStateCheckBox Value="@GetPermissionValue(permissionname, "", user.UserId)" Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
</td> </td>
} }
</tr> </tr>
@ -119,10 +119,7 @@
} }
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true); _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) _roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role
{
_roles.RemoveAll(item => item.Name == RoleNames.Host);
}
// get permission names // get permission names
if (string.IsNullOrEmpty(PermissionNames)) if (string.IsNullOrEmpty(PermissionNames))
@ -222,24 +219,24 @@
private bool GetPermissionDisabled(string permissionName, string roleName) private bool GetPermissionDisabled(string permissionName, string roleName)
{ {
var disabled = false;
// administrator role permissions can only be changed by a host
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
return true; disabled = true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
} }
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId) // API permissions can only be changed by an administrator
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
disabled = true;
}
return disabled;
}
private bool? PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{ {
if (roleName != "") if (roleName != "")
{ {
@ -248,6 +245,14 @@
{ {
_permissions.Remove(permission); _permissions.Remove(permission);
} }
// system roles cannot be denied - only custom roles can be denied
var role = _roles.FirstOrDefault(item => item.Name == roleName);
if (value != null && !value.Value && role.IsSystem)
{
value = null;
}
if (value != null) if (value != null)
{ {
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value)); _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
@ -265,13 +270,14 @@
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value)); _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
} }
} }
return value;
} }
private async Task<Dictionary<string, string>> GetUsers(string filter) private async Task<Dictionary<string, string>> GetUsers(string filter)
{ {
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered); var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase) || item.User.Username.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName); .ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName + " (" + item.User.Username + ")");
} }
private async Task AddUser() private async Task AddUser()
@ -305,29 +311,20 @@
private void ValidatePermissions() private void ValidatePermissions()
{ {
// remove deny all users, unauthenticated, and registered users
var permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Everyone || item.RoleName == RoleNames.Unauthenticated || item.RoleName == RoleNames.Registered)).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
// remove deny administrators and host users // remove host role permissions
permissions = _permissions.Where(item => !item.IsAuthorized && var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList();
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList();
foreach (var permission in permissions) foreach (var permission in permissions)
{ {
_permissions.Remove(permission); _permissions.Remove(permission);
} }
// add host role permissions if administrator role is not assigned (to prevent lockout)
foreach (var permissionname in _permissionnames) foreach (var permissionname in _permissionnames)
{ {
// add administrators role if neither host or administrator is assigned if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin))
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)))
{ {
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Admin, null, true)); _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Host, null, true));
} }
} }
} }

View File

@ -177,14 +177,14 @@
</div> </div>
@code { @code {
public string Name => "QuillJS"; public string Name => "QuillJS Text Editor";
private string resourceType = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client"; private string resourceType = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
private bool _settingsLoaded; private bool _settingsLoaded;
private bool _initialized = false; private bool _initialized = false;
private QuillEditorInterop _interop; private QuillJSTextEditorInterop _interop;
private FileManager _fileManager; private FileManager _fileManager;
private string _activetab = "Rich"; private string _activetab = "Rich";
private bool _allowSettings = false; private bool _allowSettings = false;
@ -246,14 +246,14 @@
public override List<Resource> Resources { get; set; } = new List<Resource>() public override List<Resource> Resources { get; set; } = new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/texteditors/quilljs/quill.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/texteditors/quilljs/quill-blot-formatter.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/texteditors/quilljs/quill-interop.js", Location = ResourceLocation.Body }
}; };
protected override void OnInitialized() protected override void OnInitialized()
{ {
_interop = new QuillEditorInterop(JSRuntime); _interop = new QuillJSTextEditorInterop(JSRuntime);
if (string.IsNullOrEmpty(Placeholder)) if (string.IsNullOrEmpty(Placeholder))
{ {
@ -277,7 +277,7 @@
{ {
// include CSS theme // include CSS theme
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", $"{PageState?.Alias.BaseUrl}/css/quill/quill.{_theme}.css", "text/css", "", "", ""); await interop.IncludeLink("", "stylesheet", $"{PageState?.Alias.BaseUrl}/css/texteditors/quilljs/quill.{_theme}.css", "text/css", "", "", "");
} }
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);

View File

@ -4,11 +4,11 @@ using System.Threading.Tasks;
namespace Oqtane.Modules.Controls namespace Oqtane.Modules.Controls
{ {
public class QuillEditorInterop public class QuillJSTextEditorInterop
{ {
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
public QuillEditorInterop(IJSRuntime jsRuntime) public QuillJSTextEditorInterop(IJSRuntime jsRuntime)
{ {
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }

View File

@ -0,0 +1,44 @@
@namespace Oqtane.Modules.Controls
@using Radzen
@using Radzen.Blazor
@inject DialogService DialogService
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
@if (!string.IsNullOrEmpty(_message))
{
<div class="rz-html-editor-dialog-item">
<div class="alert alert-warning alert-dismissible fade show mb-3" role="alert">
@((MarkupString)_message)
</div>
</div>
}
<div class="rz-html-editor-dialog-item">
<FileManager @ref="_fileManager" Filter="@Filters" />
</div>
<div class="rz-html-editor-dialog-buttons">
<RadzenButton Text="@Localizer["InsertImage"]" Click="InsertImage" />
<RadzenButton Text="@Localizer["Cancel"]" Click="() => DialogService.Close()" ButtonStyle="ButtonStyle.Secondary" />
</div>
@code {
private FileManager _fileManager;
private string _message = string.Empty;
[Parameter]
public string Filters { get; set; }
private void InsertImage()
{
_message = string.Empty;
var file = _fileManager.GetFile();
if (file != null)
{
var result = $"<img src=\"{file.Url}\" style=\"max-width: 100%\" alt=\"{file.Name}\" />";
DialogService.Close(result);
}
else
{
_message = Localizer["Message.Require.Image"];
StateHasChanged();
}
}
}

Some files were not shown because too many files have changed in this diff Show More