Compare commits

...

469 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
018737c42a Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-15 11:52:51 +02:00
3811b8f0c0 Theme Template updated 2025-05-07 11:46:07 +02: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
426 changed files with 11612 additions and 5019 deletions

2
.gitignore vendored
View File

@ -35,4 +35,4 @@ Oqtane.Server/wwwroot/Themes/*
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
!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.Extensions.Localization;
using Oqtane.Interfaces;
using Oqtane.Providers;
using Oqtane.Services;
using Oqtane.Shared;
using Radzen;
namespace Microsoft.Extensions.DependencyInjection
{
@ -23,7 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>();
services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<IThemeService, Oqtane.Services.ThemeService>();
services.AddScoped<IAliasService, AliasService>();
services.AddScoped<ITenantService, TenantService>();
services.AddScoped<ISiteService, SiteService>();
@ -39,7 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ILogService, LogService>();
services.AddScoped<IJobService, JobService>();
services.AddScoped<IJobLogService, JobLogService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<INotificationService, Oqtane.Services.NotificationService>();
services.AddScoped<IFolderService, FolderService>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ISiteTemplateService, SiteTemplateService>();
@ -53,12 +55,19 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
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;
}

View File

@ -14,7 +14,7 @@
<div class="container">
<div class="row">
<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 @Environment.Version.Major)</div>
</div>
</div>
@ -182,7 +182,7 @@
}
else
{
_databaseName = "LocalDB";
_databaseName = Constants.DefaultDBName;
}
LoadDatabaseConfigComponent();
@ -269,8 +269,8 @@
SiteName = Constants.DefaultSite,
Register = _register,
SiteTemplate = _template,
RenderMode = RenderModes.Static,
Runtime = Runtimes.Server
RenderMode = "", // provided by appsettings.json
Runtime = "" // provided by appsettings.json
};
var installation = await InstallationService.Install(config);

View File

@ -56,7 +56,7 @@
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<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>
@ -69,7 +69,7 @@
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<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>
@ -82,7 +82,7 @@
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<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>
@ -176,10 +176,18 @@
{
job.Interval = int.Parse(_interval);
}
job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay));
job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay));
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay));
job.StartDate = _startDate.HasValue && _startTime.HasValue
? 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);
try
{
@ -198,5 +206,4 @@
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
}

View File

@ -116,11 +116,19 @@ else
{
try
{
await JobService.StartJobAsync(jobId);
await logger.LogInformation("Job Started {JobId}", jobId);
AddModuleMessage(Localizer["Message.Job.Start"], MessageType.Success);
_jobs = await JobService.GetJobsAsync();
StateHasChanged();
Job _job = await JobService.GetJobAsync(jobId);
if (!_job.IsEnabled)
{
AddModuleMessage(Localizer["Message.Job.Disabled"], MessageType.Warning);
}
else
{
await JobService.StartJobAsync(jobId);
await logger.LogInformation("Job Started {JobId}", jobId);
AddModuleMessage(Localizer["Message.Job.Start"], MessageType.Success);
_jobs = await JobService.GetJobsAsync();
StateHasChanged();
}
}
catch (Exception ex)
{

View File

@ -21,9 +21,7 @@ else
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br />
<br />
<br /><br />
}
@if (_allowsitelogin)
{
@ -49,15 +47,11 @@ else
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</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>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
}
}
@ -100,7 +94,7 @@ else
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()

View File

@ -101,13 +101,20 @@
<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 />
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
@if (_moduledefinitions.Exists(item => item.PackageName == context.PackageId))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
<button type="button" class="btn btn-info">@SharedLocalizer["Installed"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
else
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
}
<br />
</div>
@ -171,6 +178,7 @@
@code {
private bool _initialized = false;
private List<ModuleDefinition> _moduledefinitions;
private int _page = 1;
private List<Package> _packages;
private string _price = "free";
@ -187,7 +195,8 @@
{
try
{
await LoadModuleDefinitions();
_moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
await LoadPackages();
_initialized = true;
}
catch (Exception ex)
@ -197,24 +206,10 @@
}
}
private async Task LoadModuleDefinitions()
private async Task LoadPackages()
{
ShowProgressIndicator();
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_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();
}
@ -222,25 +217,25 @@
{
_price = price;
_sort = "popularity";
await LoadModuleDefinitions();
await LoadPackages();
StateHasChanged();
}
private async Task Search()
{
await LoadModuleDefinitions();
await LoadPackages();
}
private async Task Reset()
{
_page = 1;
_search = "";
await LoadModuleDefinitions();
await LoadPackages();
}
private async Task Refresh()
{
await LoadModuleDefinitions();
await LoadPackages();
}
private void OnPageChange(int page)
@ -251,7 +246,7 @@
private async void SortChanged(ChangeEventArgs e)
{
_sort = (string)e.Value;
await LoadModuleDefinitions();
await LoadPackages();
}
private void HideModal()
@ -310,6 +305,6 @@
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)
{
<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>
<div class="container">
<div class="row mb-1 align-items-center">
@ -236,11 +236,10 @@
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid;
#pragma warning restore 649
private List<Page> _pagesWithModules;
private List<Package> _packages;
private List<Language> _languages;

View File

@ -17,8 +17,8 @@ else
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">

View File

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

View File

@ -443,8 +443,16 @@
{
_themetype = PageState.Site.DefaultThemeType;
}
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
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);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype))
{

View File

@ -2,6 +2,7 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -56,9 +57,25 @@
</div>
</div>
<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">
<input id="options" class="form-control" @bind="@_options" maxlength="2000" />
<div class="input-group">
@if (_optiontype == "Settings")
{
<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 class="row mb-1 align-items-center">
@ -95,7 +112,7 @@
<br />
<button type="button" class="btn btn-success" @onclick="SaveProfile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (PageState.QueryString.ContainsKey("id"))
@if (PageState.QueryString.ContainsKey("id"))
{
<br />
<br />
@ -116,6 +133,8 @@
private string _rows = "1";
private string _defaultvalue = string.Empty;
private string _options = string.Empty;
private string _optiontype = "Settings";
private List<string> _entitynames;
private string _validation = string.Empty;
private string _autocomplete = string.Empty;
private string _isrequired = "False";
@ -133,6 +152,8 @@
{
try
{
_entitynames = await SettingService.GetEntityNamesAsync();
if (PageState.QueryString.ContainsKey("id"))
{
_profileid = Int32.Parse(PageState.QueryString["id"]);
@ -148,6 +169,11 @@
_rows = profile.Rows.ToString();
_defaultvalue = profile.DefaultValue;
_options = profile.Options;
if (_options.StartsWith("EntityName:"))
{
_optiontype = "Options";
_options = _options.Substring(11);
}
_validation = profile.Validation;
_autocomplete = profile.Autocomplete;
_isrequired = profile.IsRequired.ToString();
@ -166,6 +192,18 @@
}
}
private void ToggleOptionType()
{
if (_optiontype == "Options")
{
_optiontype = "Settings";
}
else
{
_optiontype = "Options";
}
}
private async Task SaveProfile()
{
validated = true;
@ -193,7 +231,14 @@
profile.MaxLength = int.Parse(_maxlength);
profile.Rows = int.Parse(_rows);
profile.DefaultValue = _defaultvalue;
profile.Options = _options;
if (_optiontype == "Options" && !string.IsNullOrEmpty(_options))
{
profile.Options = "EntityName:" + _options;
}
else
{
profile.Options = _options;
}
profile.Validation = _validation;
profile.Autocomplete = _autocomplete;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));

View File

@ -3,10 +3,10 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@if (_initialized)
{
@ -115,7 +115,7 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}

View File

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

@ -54,15 +54,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9">
<select id="isDeleted" class="form-select" @bind="@_isdeleted" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9">
<select id="isDeleted" class="form-select" @bind="@_isdeleted" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
}
<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>
<div class="col-sm-9">
@ -194,80 +197,128 @@
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<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">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
<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>
<div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<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>
<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>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off"/>
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
</div>
<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>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</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">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
</div>
<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" @bind="@_smtpenabled">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<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">
<select id="smtpssl" class="form-select" @bind="@_smtpssl">
<option value="None">@Localizer["None"]</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>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
<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>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
@if (_smtpauthentication == "Basic")
{
<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>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div>
</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 below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<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">
<input id="smtpauthority" class="form-control" @bind="@_smtpauthority" />
</div>
</div>
<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 class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
}
</div>
</Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@ -455,16 +506,23 @@
private string _headcontent = string.Empty;
private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty;
private string _smtpport = string.Empty;
private string _smtpssl = "False";
private string _smtpssl = "Auto";
private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password";
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 _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30;
private string _pwaisenabled;
@ -508,7 +566,7 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
@ -534,9 +592,17 @@
{
_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;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);
@ -556,16 +622,27 @@
_bodycontent = site.BodyContent;
// SMTP
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_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);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_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);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
}
// PWA
_pwaisenabled = site.PwaIsEnabled.ToString();
@ -596,7 +673,8 @@
if (tenant != null)
{
_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;
}
}
@ -742,16 +820,23 @@
// SMTP
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, 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);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, 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, "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, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
}
//cookie consent
settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent);
@ -759,6 +844,7 @@
// functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await logger.LogInformation("Site Settings Saved {Site}", site);
@ -813,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()
{
if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@ -823,8 +949,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, 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, "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);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
@ -845,20 +976,6 @@
}
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@ -118,42 +118,42 @@ else
<Label Class="col-sm-3" For="databaseType" HelpText="Select the database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
@if (_databases != null)
{
<div class="input-group">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
@if (!_showConnectionString)
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionString"]</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionParameters"]</button>
}
</div>
}
</div>
{
<div class="input-group">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
@if (!_showConnectionString)
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionString"]</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionParameters"]</button>
}
</div>
}
</div>
</div>
@if (!_showConnectionString)
{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent
}
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">Settings:</Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div>
</div>
}
@if (!_showConnectionString)
{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent
}
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">Settings:</Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hostUsername" HelpText="Enter the username of an existing host user" ResourceKey="HostUsername">Host Username:</Label>
<div class="col-sm-9">
@ -216,7 +216,7 @@ else
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
}
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync();
_themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{
@ -237,7 +237,7 @@ else
}
else
{
_databaseName = "LocalDB";
_databaseName = Constants.DefaultDBName;
}
LoadDatabaseConfigComponent();
}
@ -390,6 +390,8 @@ else
config.DatabaseType = tenant.DBType;
config.ConnectionString = tenant.DBConnectionString;
config.IsNewTenant = false;
config.HostEmail = PageState.User.Email;
config.HostName = PageState.User.DisplayName;
}
}
@ -403,6 +405,7 @@ else
config.SiteTemplate = _sitetemplatetype;
config.RenderMode = _rendermode;
config.Runtime = _runtime;
config.Register = false;
ShowProgressIndicator();

View File

@ -200,7 +200,8 @@ else
if (tenant != null)
{
_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
@ -211,7 +212,7 @@ else
}
else
{
_databasetype = "LocalDB";
_databasetype = Constants.DefaultDBName;
}
_showConnectionString = false;
LoadDatabaseConfigComponent();

View File

@ -2,241 +2,280 @@
@inherits ModuleBase
@inject ISystemService SystemService
@inject IInstallationService InstallationService
@inject IMigrationHistoryService MigrationHistoryService
@inject ITenantService TenantService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Info" Heading="Info" ResourceKey="Info">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="Framework Version" ResourceKey="FrameworkVersion">Framework Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" readonly />
@if (_initialized)
{
<TabStrip>
<TabPanel Name="Info" Heading="Info" ResourceKey="Info">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="Framework Version" ResourceKey="FrameworkVersion">Framework Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clrversion" HelpText="Common Language Runtime Version" ResourceKey="CLRVersion">CLR Version: </Label>
<div class="col-sm-9">
<input id="clrversion" class="form-control" @bind="@_clrversion" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="osversion" HelpText="Operating System Version" ResourceKey="OSVersion">OS Version: </Label>
<div class="col-sm-9">
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="process" HelpText="Indicates if the current process is 32 bit or 64 bit" ResourceKey="Process">Process: </Label>
<div class="col-sm-9">
<input id="process" class="form-control" @bind="@_process" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9">
<input id="machinename" class="form-control" @bind="@_machinename" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ipaddress" HelpText="Server IP Address" ResourceKey="IPAddress">IP Address: </Label>
<div class="col-sm-9">
<input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="environment" HelpText="Environment name" ResourceKey="Environment">Environment: </Label>
<div class="col-sm-9">
<input id="environment" class="form-control" @bind="@_environment" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contentrootpath" HelpText="Root Path" ResourceKey="ContentRootPath">Root Path: </Label>
<div class="col-sm-9">
<input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="webrootpath" HelpText="Web Path" ResourceKey="WebRootPath">Web Path: </Label>
<div class="col-sm-9">
<input id="webrootpath" class="form-control" @bind="@_webrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="servertime" HelpText="Server Date/Time (in UTC)" ResourceKey="ServerTime">Server Date/Time: </Label>
<div class="col-sm-9">
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9">
<input id="workingset" class="form-control" @bind="@_workingset" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label>
<div class="col-sm-9">
<input id="installationid" class="form-control" @bind="@_installationid" readonly />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clrversion" HelpText="Common Language Runtime Version" ResourceKey="CLRVersion">CLR Version: </Label>
<div class="col-sm-9">
<input id="clrversion" class="form-control" @bind="@_clrversion" readonly />
<br /><br />
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel>
<TabPanel Name="Options" Heading="Options" ResourceKey="Options">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="detailederrors" HelpText="Specify If Detailed Errors Are Enabled For Blazor. This Option Should Not Not Be Enabled In Production." ResourceKey="DetailedErrors">Detailed Errors? </Label>
<div class="col-sm-9">
<select id="detailederrors" class="form-select" @bind="@_detailederrors">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logginglevel" HelpText="The Minimum Logging Level For The Event Log. This Option Can Be Used To Control The Volume Of Items Stored In Your Event Log." ResourceKey="LoggingLevel">Logging Level: </Label>
<div class="col-sm-9">
<select id="logginglevel" class="form-select" @bind="@_logginglevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notificationlevel" HelpText="The Minimum Logging Level For Which Notifications Should Be Sent To Host Users." ResourceKey="NotificationLevel">Notification Level: </Label>
<div class="col-sm-9">
<select id="notificationlevel" class="form-select" @bind="@_notificationlevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="swagger" HelpText="Specify If Swagger Is Enabled For Your Server API" ResourceKey="Swagger">Swagger Enabled? </Label>
<div class="col-sm-9">
<select id="swagger" class="form-select" @bind="@_swagger">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cachecontrol" HelpText="Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled." ResourceKey="CacheControl">Static Asset Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager Url: </Label>
<div class="col-sm-9">
<input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryemail" HelpText="Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations." ResourceKey="PackageManagerEmail">Package Manager Email: </Label>
<div class="col-sm-9">
<input id="packageregistryemail" class="form-control" @bind="@_packageregistryemail" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="osversion" HelpText="Operating System Version" ResourceKey="OSVersion">OS Version: </Label>
<div class="col-sm-9">
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<br /><br />
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<a class="btn btn-secondary" href="api/endpoint" target="_new">@Localizer["Endpoints"]</a>
</TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="log" HelpText="System log information for current day" ResourceKey="Log">Log: </Label>
<div class="col-sm-9">
<textarea id="log" class="form-control" rows="10" @bind="@_log" readonly />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="process" HelpText="Indicates if the current process is 32 bit or 64 bit" ResourceKey="Process">Process: </Label>
<div class="col-sm-9">
<input id="process" class="form-control" @bind="@_process" readonly />
<br /><br />
<button type="button" class="btn btn-danger" @onclick="ClearLog">@Localizer["Clear"]</button>
</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>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9">
<input id="machinename" class="form-control" @bind="@_machinename" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ipaddress" HelpText="Server IP Address" ResourceKey="IPAddress">IP Address: </Label>
<div class="col-sm-9">
<input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="environment" HelpText="Environment name" ResourceKey="Environment">Environment: </Label>
<div class="col-sm-9">
<input id="environment" class="form-control" @bind="@_environment" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contentrootpath" HelpText="Root Path" ResourceKey="ContentRootPath">Root Path: </Label>
<div class="col-sm-9">
<input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="webrootpath" HelpText="Web Path" ResourceKey="WebRootPath">Web Path: </Label>
<div class="col-sm-9">
<input id="webrootpath" class="form-control" @bind="@_webrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="servertime" HelpText="Server Date/Time (in UTC)" ResourceKey="ServerTime">Server Date/Time: </Label>
<div class="col-sm-9">
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9">
<input id="workingset" class="form-control" @bind="@_workingset" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label>
<div class="col-sm-9">
<input id="installationid" class="form-control" @bind="@_installationid" readonly />
</div>
</div>
</div>
<br /><br />
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel>
<TabPanel Name="Options" Heading="Options" ResourceKey="Options">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="detailederrors" HelpText="Specify If Detailed Errors Are Enabled For Blazor. This Option Should Not Not Be Enabled In Production." ResourceKey="DetailedErrors">Detailed Errors? </Label>
<div class="col-sm-9">
<select id="detailederrors" class="form-select" @bind="@_detailederrors">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logginglevel" HelpText="The Minimum Logging Level For The Event Log. This Option Can Be Used To Control The Volume Of Items Stored In Your Event Log." ResourceKey="LoggingLevel">Logging Level: </Label>
<div class="col-sm-9">
<select id="logginglevel" class="form-select" @bind="@_logginglevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notificationlevel" HelpText="The Minimum Logging Level For Which Notifications Should Be Sent To Host Users." ResourceKey="NotificationLevel">Notification Level: </Label>
<div class="col-sm-9">
<select id="notificationlevel" class="form-select" @bind="@_notificationlevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="swagger" HelpText="Specify If Swagger Is Enabled For Your Server API" ResourceKey="Swagger">Swagger Enabled? </Label>
<div class="col-sm-9">
<select id="swagger" class="form-select" @bind="@_swagger">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cachecontrol" HelpText="Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled." ResourceKey="CacheControl">Static Asset Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager Url: </Label>
<div class="col-sm-9">
<input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryemail" HelpText="Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations." ResourceKey="PackageManagerEmail">Package Manager Email: </Label>
<div class="col-sm-9">
<input id="packageregistryemail" class="form-control" @bind="@_packageregistryemail" />
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<br /><br />
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<a class="btn btn-secondary" href="api/endpoint" target="_new">@Localizer["Endpoints"]</a>
</TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="log" HelpText="System log information for current day" ResourceKey="Log">Log: </Label>
<div class="col-sm-9">
<textarea id="log" class="form-control" rows="10" @bind="@_log" readonly />
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-danger" @onclick="ClearLog">@Localizer["Clear"]</button>
</TabPanel>
</TabStrip>
<br /><br />
<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>
<br /><br />
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private string _version = string.Empty;
private string _clrversion = string.Empty;
private string _osversion = string.Empty;
private bool _initialized = false;
private string _version = string.Empty;
private string _clrversion = string.Empty;
private string _osversion = string.Empty;
private string _process = string.Empty;
private string _machinename = string.Empty;
private string _ipaddress = string.Empty;
private string _environment = string.Empty;
private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty;
private string _ipaddress = string.Empty;
private string _environment = string.Empty;
private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty;
private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _cachecontrol = string.Empty;
private string _packageregistryurl = string.Empty;
private string _packageregistryemail = string.Empty;
private string _log = string.Empty;
protected override async Task OnInitializedAsync()
{
_version = Constants.Version;
private string _tenant = string.Empty;
private List<MigrationHistory> _history;
var systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null)
{
_clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString();
protected override async Task OnInitializedAsync()
{
_version = Constants.Version;
var systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null)
{
_clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString();
_process = systeminfo["Process"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].ToString();
_contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC";
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
}
_ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].ToString();
_contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC";
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
}
systeminfo = await SystemService.GetSystemInfoAsync("configuration");
if (systeminfo != null)
{
_installationid = systeminfo["InstallationId"].ToString();
_detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
systeminfo = await SystemService.GetSystemInfoAsync("configuration");
if (systeminfo != null)
{
_installationid = systeminfo["InstallationId"].ToString();
_detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_cachecontrol = systeminfo["CacheControl"].ToString();
_packageregistryurl = systeminfo["PackageRegistryUrl"].ToString();
_packageregistryemail = systeminfo["PackageRegistryEmail"].ToString();
}
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
{
_log = systeminfo["Log"].ToString();
}
}
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
{
_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()
{

View File

@ -101,13 +101,20 @@
<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 />
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
@if (_themes.Exists(item => item.PackageName == context.PackageId))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
<button type="button" class="btn btn-info">@SharedLocalizer["Installed"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
else
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
}
<br />
</div>
@ -171,6 +178,7 @@
@code {
private bool _initialized = false;
private List<Theme> _themes;
private int _page = 1;
private List<Package> _packages;
private string _price = "free";
@ -187,7 +195,8 @@
{
try
{
await LoadThemes();
_themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
await LoadPackages();
_initialized = true;
}
catch (Exception ex)
@ -197,24 +206,10 @@
}
}
private async Task LoadThemes()
private async Task LoadPackages()
{
ShowProgressIndicator();
var themes = await ThemeService.GetThemesAsync();
ShowProgressIndicator();
_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();
}
@ -222,25 +217,25 @@
{
_price = price;
_sort = "popularity";
await LoadThemes();
await LoadPackages();
StateHasChanged();
}
private async Task Search()
{
await LoadThemes();
await LoadPackages();
}
private async Task Reset()
{
_page = 1;
_search = "";
await LoadThemes();
await LoadPackages();
}
private async Task Refresh()
{
await LoadThemes();
await LoadPackages();
}
private void OnPageChange(int page)
@ -251,7 +246,7 @@
private async void SortChanged(ChangeEventArgs e)
{
_sort = (string)e.Value;
await LoadThemes();
await LoadPackages();
}
private void HideModal()
@ -310,6 +305,6 @@
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,84 +9,98 @@
@if (_initialized)
{
<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="The name of the module" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
<TabStrip>
<TabPanel Name="Theme" ResourceKey="Theme" Heading="Theme">
<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="The name of the theme" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="isenabled" class="form-select" @bind="@_isenabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<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>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="isenabled" class="form-select" @bind="@_isenabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
<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 {
@ -103,11 +117,14 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private PermissionGrid _permissionGrid;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
@ -126,6 +143,7 @@
_url = theme.Url;
_contact = theme.Contact;
_license = theme.License;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy;
_createdon = theme.CreatedOn;
_modifiedby = theme.ModifiedBy;
@ -152,6 +170,7 @@
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
theme.Name = _name;
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
theme.PermissionList = _permissionGrid.GetPermissionList();
await ThemeService.UpdateThemeAsync(theme);
await logger.LogInformation("Theme Saved {Theme}", theme);
NavigationManager.NavigateTo(NavigateUrl());

View File

@ -15,12 +15,11 @@
else
{
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<Pager Items="@_themes">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</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" />
}
</td>
<td><NavLink class="btn btn-secondary" href="@NavigateUrl("admin/site")">@Localizer["Assign"]</NavLink></td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@ -80,7 +78,7 @@ else
{
try
{
_themes = await ThemeService.GetThemesAsync();
_themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackageUpdatesAsync("theme");
}
catch (Exception ex)
@ -163,7 +161,7 @@ else
{
try
{
await ThemeService.DeleteThemeAsync(Theme.ThemeName);
await ThemeService.DeleteThemeAsync(Theme.ThemeId, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}

View File

@ -126,13 +126,16 @@
<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))
{
@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
{
<option value="@option">@option</option>
<option value="@name">@value</option>
}
}
</select>
@ -142,13 +145,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@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
{
<option value="@option">@option</option>
<option value="@name">@value</option>
}
}
</select>
@ -367,7 +373,6 @@
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private string _username = string.Empty;
@ -381,6 +386,7 @@
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private int _photofileid = -1;
private File _photo = null;
@ -404,7 +410,16 @@
_togglepassword = SharedLocalizer["ShowPassword"];
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
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)
{
@ -414,11 +429,6 @@
_displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(_email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)

View File

@ -28,6 +28,15 @@
<input id="email" class="form-control" @bind="@_email" />
</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">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
@ -79,13 +88,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@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
{
<option value="@option">@option</option>
<option value="@name">@value</option>
}
}
</select>
@ -120,6 +132,7 @@
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True";
@ -133,8 +146,17 @@
{
try
{
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
_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;
@ -169,6 +191,7 @@
user.Username = _username;
user.Password = ""; // will be auto generated
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null;

View File

@ -48,7 +48,7 @@
</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">Confirmed?</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">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
@ -121,13 +121,16 @@
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@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
{
<option value="@option">@option</option>
<option value="@name">@value</option>
}
}
</select>
@ -153,13 +156,13 @@
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<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>
}
@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 />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@ -204,7 +207,16 @@
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
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))
{

View File

@ -17,8 +17,21 @@ else
{
<TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<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"/>
<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 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">
<Header>
@ -74,10 +87,19 @@ else
<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="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<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>
<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">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
@ -92,6 +114,12 @@ else
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</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">
<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">
@ -292,6 +320,24 @@ else
</select>
</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">
<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>
@ -486,12 +532,15 @@ else
@code {
private List<UserRole> users;
private string _deleted = "false";
private string _allowregistration;
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor;
private string _cookiename;
private string _cookiedomain;
private string _cookieexpiration;
private string _alwaysremember;
private string _logouteverywhere;
@ -519,6 +568,8 @@ else
private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty;
private string _authresponsetype;
private string _requirenonce;
private string _singlelogout;
private string _scopes;
private string _parameters;
private string _pkce;
@ -554,17 +605,19 @@ else
protected override async Task OnInitializedAsync()
{
await LoadUsersAsync(true);
await LoadUsersAsync();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_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))
{
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", "");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
_alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false");
_logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false");
@ -604,6 +657,8 @@ else
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_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", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
@ -625,20 +680,32 @@ else
_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);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var hosts = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Host);
users.AddRange(hosts);
users = users.OrderBy(u => u.User.DisplayName).ToList();
}
var hosts = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Host);
users.AddRange(hosts);
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");
}
}
private async Task DeleteUser(UserRole UserRole)
{
try
@ -661,7 +728,7 @@ else
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success);
await LoadUsersAsync(true);
await LoadUsersAsync();
StateHasChanged();
}
catch (Exception ex)
@ -685,8 +752,10 @@ else
{
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:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false);
@ -712,6 +781,8 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, 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:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);

View File

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

View File

@ -107,7 +107,7 @@
@code {
private bool _initialized = false;
private List<Folder> _folders;
private List<Folder> _folders = new List<Folder>();
private List<File> _files = new List<File>();
private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty;
@ -157,6 +157,9 @@
[Parameter]
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]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
@ -195,19 +198,22 @@
Filter = "nupkg";
ShowSuccess = true;
}
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
else
{
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
if (folder != null)
// folder path specified rather than folderid
if (!string.IsNullOrEmpty(Folder))
{
FolderId = folder.FolderId;
}
else
{
FolderId = -1;
_message = "Folder Path " + Folder + " Does Not Exist";
_messagetype = MessageType.Error;
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
if (folder != null)
{
FolderId = folder.FolderId;
}
else
{
FolderId = -1;
_message = "Folder Path " + Folder + " Does Not Exist";
_messagetype = MessageType.Error;
}
}
}
@ -242,25 +248,24 @@
}
}
await SetImage();
if (!string.IsNullOrEmpty(Filter))
{
_filter = "." + Filter.Replace(",", ",.");
}
GetFolderPermission();
await SetImage();
await GetFiles();
_initialized = true;
}
private async Task GetFiles()
private void GetFolderPermission()
{
_haseditpermission = false;
if (Folder == Constants.PackagesFolder)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = new List<File>();
}
else
{
@ -268,69 +273,14 @@
if (folder != null)
{
_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
{
_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;
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 SetImage()
{
_image = string.Empty;
@ -354,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()
{
_message = string.Empty;
@ -408,7 +426,7 @@
}
// 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
if (ShowProgress)
@ -430,6 +448,28 @@
_message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success;
}
FileId = -1;
if (Folder != Constants.PackagesFolder && !AnonymizeUploadFilenames)
{
// set FileId to first file in upload collection
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0].Split(":")[0]);
if (file != null)
{
FileId = file.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();
StateHasChanged();
}
else
{
@ -437,28 +477,6 @@
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
if (Folder == Constants.PackagesFolder)
{
await OnUpload.InvokeAsync(-1);
}
else
{
// set FileId to first file in upload collection
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0].Split(":")[0]);
if (file != null)
{
FileId = file.FileId;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await OnUpload.InvokeAsync(FileId);
}
await GetFiles();
StateHasChanged();
}
}
catch (Exception ex)
{
@ -471,7 +489,6 @@
finally {
tokenSource.Dispose();
}
}
else
{
@ -501,47 +518,50 @@
_messagetype = MessageType.Success;
}
await GetFiles();
FileId = -1;
await SetImage();
FileId = -1;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await GetFiles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
_message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error;
}
}
_message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error;
}
}
public int GetFileId() => FileId;
public int GetFileId() => FileId;
public int GetFolderId() => FolderId;
public int GetFolderId() => FolderId;
public File GetFile() => _file;
public File GetFile() => _file;
public async Task Refresh()
{
await Refresh(-1);
}
public async Task Refresh()
{
await Refresh(-1);
}
public async Task Refresh(int fileId)
{
await GetFiles();
if (fileId != -1)
public async Task Refresh(int fileId)
{
await GetFiles();
FileId = -1;
if (fileId != -1)
{
var file = _files.Where(item => item.FileId == fileId).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
}
}
StateHasChanged();
await SetImage();
StateHasChanged();
}
}

View File

@ -4,29 +4,43 @@
@if (!string.IsNullOrEmpty(Message))
{
<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>
}
@if (ModuleState != null)
{
@if (ModuleState.RenderMode == RenderModes.Static)
@if (_style == MessageStyle.Alert)
{
<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))
{
<a href="@NavigationManager.Uri" class="btn-close" data-dismiss="alert" aria-label="close"></a>
<NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink>
}
else
{
<button type="button" class="btn-close" data-dismiss="alert" aria-label="close" @onclick="CloseMessage"></button>
}
}
</div>
<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>
}
@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 {
private string _message = string.Empty;
private string _classname = string.Empty;
private MessageStyle _style;
[Parameter]
public string Message { get; set; }
@ -34,6 +48,9 @@
[Parameter]
public MessageType Type { get; set; }
[Parameter]
public MessageStyle Style { get; set; } = MessageStyle.Alert;
[Parameter]
public RenderModeBoundary Parent { get; set; }
@ -43,6 +60,11 @@
if (!string.IsNullOrEmpty(_message))
{
_classname = GetMessageType(Type);
_style = Style;
if (Type == MessageType.Error)
{
_style = MessageStyle.Alert; // errors should always be displayed as alerts
}
}
}
@ -67,9 +89,10 @@
return classname;
}
private void CloseMessage(MouseEventArgs e)
private void CloseMessage()
{
if(Parent != null)
if (Parent != null)
{
Parent.DismissMessage();
}

View File

@ -9,62 +9,26 @@
@if (_permissions != null)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
@foreach (Role role in _roles)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<td>@role.Name</td>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<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))" />
</td>
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
}
</tbody>
</table>
<br />
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
@foreach (Role role in _roles)
{
<tr>
<td>@user.DisplayName (@user.Username)</td>
<td>@role.Name</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
<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))" />
</td>
}
</tr>
@ -72,200 +36,242 @@
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
{
<tr>
<td>@user.DisplayName (@user.Username)</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value="@GetPermissionValue(permissionname, "", user.UserId)" Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
</td>
}
</tr>
}
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
}
@code {
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; }
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles.RemoveAll(item => item.Name == RoleNames.Host);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
_roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
var disabled = false;
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
}
// administrator role permissions can only be changed by a host
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
disabled = true;
}
// 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 != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_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)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_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)
{
@ -305,29 +311,20 @@
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))
{
// remove deny administrators and host users
permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList();
foreach (var permission in permissions)
// remove host role permissions
var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
// add host role permissions if administrator role is not assigned (to prevent lockout)
foreach (var permissionname in _permissionnames)
{
_permissions.Remove(permission);
}
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 || item.RoleName == RoleNames.Host)))
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin))
{
_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>
@code {
public string Name => "QuillJS";
public string Name => "QuillJS Text Editor";
private string resourceType = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
private bool _settingsLoaded;
private bool _initialized = false;
private QuillEditorInterop _interop;
private QuillJSTextEditorInterop _interop;
private FileManager _fileManager;
private string _activetab = "Rich";
private bool _allowSettings = false;
@ -246,14 +246,14 @@
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/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.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/texteditors/quilljs/quill-interop.js", Location = ResourceLocation.Body }
};
protected override void OnInitialized()
{
_interop = new QuillEditorInterop(JSRuntime);
_interop = new QuillJSTextEditorInterop(JSRuntime);
if (string.IsNullOrEmpty(Placeholder))
{
@ -277,7 +277,7 @@
{
// include CSS theme
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);

View File

@ -4,11 +4,11 @@ using System.Threading.Tasks;
namespace Oqtane.Modules.Controls
{
public class QuillEditorInterop
public class QuillJSTextEditorInterop
{
private readonly IJSRuntime _jsRuntime;
public QuillEditorInterop(IJSRuntime jsRuntime)
public QuillJSTextEditorInterop(IJSRuntime 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();
}
}
}

View File

@ -0,0 +1,148 @@
@namespace Oqtane.Modules.Controls
@using Radzen
@using Radzen.Blazor
@using System.Text
@inject DialogService DialogService
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
@if (_linkAttributes != null)
{
@if (!string.IsNullOrWhiteSpace(_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">
<RadzenDropDown TValue="int" class="form-control" PopupStyle="color: var(--rz-input-value-color);" @bind-Value="_linkType" Data="_linkTypes" TextProperty="Value" ValueProperty="Key" />
</div>
@if (_linkType == 0)
{
<div class="rz-html-editor-dialog-item">
<RadzenTextBox class="form-control" @bind-Value="@_linkAttributes.Href" Placeholder="@Localizer["WebAddress"]" />
</div>
}
else
{
<div class="rz-html-editor-dialog-item">
<FileManager @ref="_fileManager" OnSelectFile="SelectFile" OnSelectFolder="SelectFile" />
</div>
}
@if (_linkTextEditable)
{
<div class="rz-html-editor-dialog-item">
<RadzenTextBox class="form-control" @bind-Value="@_linkAttributes.InnerText" Placeholder="@Localizer["LinkText"]" />
</div>
}
<div class="rz-html-editor-dialog-item">
<RadzenDropDown TValue="bool" class="form-control" PopupStyle="color: var(--rz-input-value-color);" @bind-Value="_blank" Data="_linkTargets" TextProperty="Value" ValueProperty="Key" />
</div>
}
<div class="rz-html-editor-dialog-buttons">
<RadzenButton Text=@Localizer["InsertLink"] Click="InsertLink" />
<RadzenButton Text=@Localizer["Cancel"] Click="() => DialogService.Close()" ButtonStyle="ButtonStyle.Secondary" />
</div>
@code {
class LinkAttributes
{
public string InnerText { get; set; }
public string InnerHtml { get; set; }
public string Href { get; set; }
public string Target { get; set; }
}
[Parameter]
public RadzenHtmlEditor Editor { get; set; }
private IDictionary<int, string> _linkTypes;
private IDictionary<bool, string> _linkTargets;
private LinkAttributes _linkAttributes;
private bool _blank;
private int _linkType;
private string _message;
private bool _linkTextEditable;
private FileManager _fileManager;
private File _previousFile;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_linkAttributes = await Editor.GetSelectionAttributes<LinkAttributes>("a", new[] { "innerText", "href", "target" });
if (_linkAttributes.Target == "_blank")
{
_blank = true;
}
_linkTextEditable = string.IsNullOrWhiteSpace(_linkAttributes.InnerHtml) || _linkAttributes.InnerHtml == "<br>";
_linkTypes = new Dictionary<int, string>
{
{ 0, Localizer["WebLink"] },
{ 1, Localizer["FileLink"] }
};
_linkTargets = new Dictionary<bool, string>
{
{ false, Localizer["OpenInCurrentWindow"] },
{ true, Localizer["OpenInNewWindow"] }
};
}
private void SelectFile()
{
var file = _fileManager.GetFile();
if(file != null)
{
_linkAttributes.Href = file.Url;
if ((string.IsNullOrWhiteSpace(_linkAttributes.InnerText) || _linkAttributes.InnerText == _previousFile?.Name) && _linkTextEditable)
{
_linkAttributes.InnerText = file.Name;
}
}
else
{
_linkAttributes.Href = string.Empty;
if (_linkAttributes.InnerText == _previousFile?.Name)
{
_linkAttributes.InnerText = string.Empty;
}
}
_previousFile = file;
StateHasChanged();
}
private void InsertLink()
{
_message = string.Empty;
if (string.IsNullOrWhiteSpace(_linkAttributes.Href))
{
_message = _linkType == 1 ? Localizer["Message.Require.File"] : Localizer["Message.Require.WebAddress"];
}
else if (string.IsNullOrWhiteSpace(_linkAttributes.InnerText) && _linkTextEditable)
{
_message = Localizer["Message.Require.LinkText"];
}
if (string.IsNullOrWhiteSpace(_message))
{
var html = new StringBuilder();
html.AppendFormat("<a href=\"{0}\"", _linkAttributes.Href);
if (_blank)
{
html.Append(" target=\"_blank\"");
}
html.AppendFormat(">{0}</a>", string.IsNullOrWhiteSpace(_linkAttributes.InnerText) ? _linkAttributes.InnerHtml : _linkAttributes.InnerText);
DialogService.Close(html.ToString());
}
else
{
StateHasChanged();
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
//
// Note added 2025-09-23 by @tvatavuk.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.Modules.Controls;
public partial class RadzenTextEditor;

View File

@ -0,0 +1,215 @@
@using Microsoft.Extensions.Configuration
@using Oqtane.Interfaces
@using System.Text.RegularExpressions
@using Radzen
@using Radzen.Blazor
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
@implements IDisposable
@inject Radzen.ThemeService ThemeService
@inject IRadzenEditorSettingService EditorSettingService
@inject DialogService DialogService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
<RadzenTheme Theme="@RadzenEditorDefinitions.DefaultTheme" />
<RadzenComponents />
<RadzenHtmlEditor @ref="_editor" Visible="_visible" Placeholder="@Placeholder" style="@($"height: {Height}px;")"
@bind-Value="_value" Execute="OnExecute" class="rz-text-editor">
<ChildContent>
@_toolbar
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<RadzenHtmlEditorCustomTool CommandName="Settings" Icon="settings" Title="@Localizer["Settings"]" />
}
</ChildContent>
</RadzenHtmlEditor>
@code {
private Oqtane.Modules.Controls.RadzenTextEditorInterop _interop;
private RadzenHtmlEditor _editor;
private string _value;
private bool _visible = false;
private string _theme;
private string _background;
private IList<string> _toolbarItems;
private RenderFragment _toolbar;
[Parameter]
public string Placeholder { get; set; }
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public int Height { get; set; } = 450;
public string Name => "Radzen HTML Editor";
public override List<Resource> Resources { get; set; } = new List<Resource>()
{
new Script("_content/Radzen.Blazor/Radzen.Blazor.js"),
new Script("js/texteditors/radzen/radzen-interop.js")
};
protected override void OnInitialized()
{
_interop = new Oqtane.Modules.Controls.RadzenTextEditorInterop(JSRuntime);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", $"{PageState?.Alias.BaseUrl}/css/texteditors/radzen/radzentexteditor.css", "text/css", "", "", "");
await LoadSettings();
_visible = true;
StateHasChanged();
await _interop.Initialize(_editor.Element);
if (!string.IsNullOrEmpty(_theme))
{
ThemeService.SetTheme(_theme);
}
if (!string.IsNullOrEmpty(_background))
{
var backgroundColor = RadzenEditorDefinitions.TransparentBackgroundColor;
switch (_background)
{
case "Light":
backgroundColor = RadzenEditorDefinitions.LightBackgroundColor;
break;
case "Dark":
backgroundColor = RadzenEditorDefinitions.DarkBackgroundColor;
break;
}
await _interop.SetBackgroundColor(_editor.Element, backgroundColor);
}
}
}
public void Initialize(string content)
{
_value = !string.IsNullOrEmpty(content) ? content : string.Empty;
DialogService.OnOpen += OnDialogOpened;
}
public void Dispose()
{
if (DialogService != null)
{
DialogService.OnOpen -= OnDialogOpened;
}
}
public async Task<string> GetContent()
{
await Task.CompletedTask;
return _value;
}
private async Task LoadSettings()
{
var scope = await EditorSettingService.GetSettingScopeAsync(ModuleState.ModuleId);
var editorSetting = scope == 1
? await EditorSettingService.LoadSettingsFromModuleAsync(ModuleState.ModuleId)
: await EditorSettingService.LoadSettingsFromSiteAsync(PageState.Site.SiteId);
_theme = editorSetting.Theme;
_background = editorSetting.Background;
_toolbarItems = editorSetting.ToolbarItems.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
_toolbar = SetupToolbarItems();
}
private RenderFragment SetupToolbarItems()
{
return builder =>
{
var sequence = 0;
foreach (var item in _toolbarItems)
{
if (RadzenEditorDefinitions.ToolbarItems.ContainsKey(item))
{
sequence = RadzenEditorDefinitions.ToolbarItems[item](builder, sequence);
}
}
};
}
private async Task OnExecute(HtmlEditorExecuteEventArgs args)
{
switch(args.CommandName)
{
case "InsertImage":
await InsertImage(args.Editor);
break;
case "InsertLink":
await InsertLink(args.Editor);
break;
case "Settings":
await UpdateSettings(args.Editor);
break;
}
}
private async Task InsertImage(RadzenHtmlEditor editor)
{
await editor.SaveSelectionAsync();
var result = await DialogService.OpenAsync<RadzenFileManagerDialog>(Localizer["DialogTitle.SelectImage"], new Dictionary<string, object>
{
{ "Filters", PageState.Site.ImageFiles }
}, new DialogOptions { CssClass = "rz-text-editor-dialog" });
await editor.RestoreSelectionAsync();
if (result != null)
{
await editor.ExecuteCommandAsync(HtmlEditorCommands.InsertHtml, result);
}
}
private async Task InsertLink(RadzenHtmlEditor editor)
{
await editor.SaveSelectionAsync();
var result = await DialogService.OpenAsync<RadzenInsertLinkDialog>(Localizer["DialogTitle.InsertLink"], new Dictionary<string, object>
{
{ "Editor", editor }
}, new DialogOptions { CssClass = "rz-text-editor-dialog" });
await editor.RestoreSelectionAsync();
if (result != null)
{
await editor.ExecuteCommandAsync(HtmlEditorCommands.InsertHtml, result);
}
}
private async Task UpdateSettings(RadzenHtmlEditor editor)
{
await editor.SaveSelectionAsync();
var result = await DialogService.OpenAsync<RadzenTextEditorSettingsDialog>(Localizer["Settings"], null, new DialogOptions { Width = "650px" });
if (result == true)
{
NavigationManager.NavigateTo(NavigationManager.Uri);
}
await editor.RestoreSelectionAsync();
}
private async void OnDialogOpened(string title, Type componentType, Dictionary<string, object> parameters, DialogOptions options)
{
await _interop.UpdateDialogLayout(_editor.Element);
}
}

View File

@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
namespace Oqtane.Modules.Controls
{
public sealed class RadzenEditorDefinitions
{
public static IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer { get; internal set; }
public const string TransparentBackgroundColor = "rgba(0, 0, 0, 0)";
public const string LightBackgroundColor = "rgba(255, 255, 255, 1)";
public const string DarkBackgroundColor = "rgba(0, 0, 0, 1)";
public const string DefaultTheme = "default";
public const string DefaultBackground = "Default";
public static readonly IDictionary<string, Func<RenderTreeBuilder, int, int>> ToolbarItems = new Dictionary<string, Func<RenderTreeBuilder, int, int>>()
{
{ "AlignCenter", (builder, sequence) => CreateFragment(builder, sequence, "AlignCenter", "RadzenHtmlEditorAlignCenter") },
{ "AlignLeft", (builder, sequence) => CreateFragment(builder, sequence, "AlignLeft", "RadzenHtmlEditorAlignLeft") },
{ "AlignRight", (builder, sequence) => CreateFragment(builder, sequence, "AlignRight", "RadzenHtmlEditorAlignRight") },
{ "Background", (builder, sequence) => CreateFragment(builder, sequence, "Background", "RadzenHtmlEditorBackground") },
{ "Color", (builder, sequence) => CreateFragment(builder, sequence, "Color", "RadzenHtmlEditorColor") },
{ "FontName", (builder, sequence) => CreateFragment(builder, sequence, "FontName", "RadzenHtmlEditorFontName") },
{ "FontSize", (builder, sequence) => CreateFragment(builder, sequence, "FontSize", "RadzenHtmlEditorFontSize") },
{ "FormatBlock", (builder, sequence) => CreateFragment(builder, sequence, "FormatBlock", "RadzenHtmlEditorFormatBlock") },
{ "Indent", (builder, sequence) => CreateFragment(builder, sequence, "Indent", "RadzenHtmlEditorIndent") },
{ "InsertImage", (builder, sequence) => CreateFragment(builder, sequence, "InsertImage", "RadzenHtmlEditorCustomTool", "InsertImage", "image") },
{ "Italic", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorItalic") },
{ "Justify", (builder, sequence) => CreateFragment(builder, sequence, "Justify", "RadzenHtmlEditorJustify") },
{ "Link", (builder, sequence) => CreateFragment(builder, sequence, "InsertLink", "RadzenHtmlEditorCustomTool", "InsertLink", "insert_link") },
{ "OrderedList", (builder, sequence) => CreateFragment(builder, sequence, "OrderedList", "RadzenHtmlEditorOrderedList") },
{ "Outdent", (builder, sequence) => CreateFragment(builder, sequence, "Outdent", "RadzenHtmlEditorOutdent") },
{ "Redo", (builder, sequence) => CreateFragment(builder, sequence, "Redo", "RadzenHtmlEditorRedo") },
{ "RemoveFormat", (builder, sequence) => CreateFragment(builder, sequence, "RemoveFormat", "RadzenHtmlEditorRemoveFormat") },
{ "Separator", (builder, sequence) => CreateFragment(builder, sequence, "Separator", "RadzenHtmlEditorSeparator") },
{ "Source", (builder, sequence) => CreateFragment(builder, sequence, "Source", "RadzenHtmlEditorSource") },
{ "StrikeThrough", (builder, sequence) => CreateFragment(builder, sequence, "StrikeThrough", "RadzenHtmlEditorStrikeThrough") },
{ "Subscript", (builder, sequence) => CreateFragment(builder, sequence, "Subscript", "RadzenHtmlEditorSubscript") },
{ "Superscript", (builder, sequence) => CreateFragment(builder, sequence, "Superscript", "RadzenHtmlEditorSuperscript") },
{ "Underline", (builder, sequence) => CreateFragment(builder, sequence, "Underline", "RadzenHtmlEditorUnderline") },
{ "Undo", (builder, sequence) => CreateFragment(builder, sequence, "Undo", "RadzenHtmlEditorUndo") },
{ "Unlink", (builder, sequence) => CreateFragment(builder, sequence, "Unlink", "RadzenHtmlEditorUnlink") },
{ "UnorderedList", (builder, sequence) => CreateFragment(builder, sequence, "UnorderedList", "RadzenHtmlEditorUnorderedList") },
};
public static readonly string DefaultToolbarItems = "Undo,Redo,Separator,FontName,FontSize,FormatBlock,Bold,Italic,Underline,StrikeThrough,Separator,AlignLeft,AlignCenter,AlignRight,Justify,Separator,Indent,Outdent,UnorderedList,OrderedList,Separator,Color,Background,RemoveFormat,Separator,Subscript,Superscript,Separator,Link,Unlink,InsertImage,Separator,Source";
private static int CreateFragment(RenderTreeBuilder builder, int sequence, string name, string typeName, string commaneName = "", string icon = "")
{
var fullTypeName = $"Radzen.Blazor.{typeName}, Radzen.Blazor";
var type = Type.GetType(fullTypeName);
if (type != null)
{
var title = Localizer[$"{name}.Title"];
var placeholder = Localizer[$"{name}.Placeholder"];
builder.OpenComponent(sequence++, type);
if (!string.IsNullOrEmpty(title) && title != $"{name}.Title" && type.GetProperty("Title") != null)
{
builder.AddAttribute(sequence++, "Title", title);
}
if (!string.IsNullOrEmpty(placeholder) && placeholder != $"{name}.Placeholder" && type.GetProperty("Placeholder") != null)
{
builder.AddAttribute(sequence++, "Placeholder", placeholder);
}
if (!string.IsNullOrEmpty(commaneName) && type.GetProperty("CommandName") != null)
{
builder.AddAttribute(sequence++, "CommandName", commaneName);
}
if (!string.IsNullOrEmpty(icon) && type.GetProperty("Icon") != null)
{
builder.AddAttribute(sequence++, "Icon", icon);
}
builder.CloseComponent();
}
return sequence;
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Oqtane.Modules.Controls
{
public class RadzenTextEditorInterop
{
private readonly IJSRuntime _jsRuntime;
public RadzenTextEditorInterop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public Task Initialize(ElementReference editor)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.RadzenTextEditor.initialize", editor);
}
catch
{
}
return Task.CompletedTask;
}
public Task SetBackgroundColor(ElementReference editor, string color)
{
try
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.RadzenTextEditor.setBackgroundColor",
editor, color);
}
catch
{
}
return Task.CompletedTask;
}
public Task UpdateDialogLayout(ElementReference editor)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.RadzenTextEditor.updateDialogLayout", editor);
}
catch
{
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,11 @@
namespace Oqtane.Modules.Controls
{
public class RadzenEditorSetting
{
public string Theme { get; set; }
public string Background { get; set; }
public string ToolbarItems { get; set; }
}
}

View File

@ -0,0 +1,222 @@
@namespace Oqtane.Modules.Controls
@using System.IO
@using Radzen
@using Radzen.Blazor
@inherits ModuleControlBase
@inject DialogService DialogService
@inject Radzen.ThemeService ThemeService
@inject ISettingService SettingService
@inject IRadzenEditorSettingService EditorSettingService
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
<div class="row">
<div class="col-12 col-sm-3">
@Localizer["Scope"]
</div>
<div class="col-12 col-sm-9">
<RadzenRadioButtonList @bind-Value="@_settingScope" TValue="int" Change="OnScopeChanged">
<Items>
<RadzenRadioButtonListItem Text="@Localizer["Site"]" Value="0" />
<RadzenRadioButtonListItem Text="@Localizer["Module"]" Value="1" />
</Items>
</RadzenRadioButtonList>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Theme"]
</div>
<div class="col-12 col-sm-9">
<RadzenDropDown @bind-Value="_theme" TValue="string" Data="@_themes" Style="width: 100%;">
<Template>
<span>@Localizer[$"theme.{context}"]</span>
</Template>
</RadzenDropDown>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Background"]
</div>
<div class="col-12 col-sm-9">
<RadzenDropDown @bind-Value="_background" TValue="string" Data="_backgroundColors" Style="width: 100%;">
<Template>
<span>@Localizer[context]</span>
</Template>
</RadzenDropDown>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Toolbar"]
</div>
<div class="col-12 col-sm-9">
<div class="row">
<div class="col-12 col-sm-7">
<RadzenDropDown TValue="string" @bind-Value="_addToolbarItem" Data="@RadzenEditorDefinitions.ToolbarItems.Keys" Style="width: 100%;">
</RadzenDropDown>
</div>
<div class="col-12 col-sm-5 text-end">
<button type="button" class="btn btn-primary" @onclick="AddToolbarItem">@Localizer["Add"]</button>
<button type="button" class="btn btn-secondary" @onclick="ResetToolbarItem">@Localizer["Reset"]</button>
</div>
</div>
<div class="row mt-2" style="max-height: 500px; overflow-y: scroll;">
<div class="col">
<RadzenDropZoneContainer TItem="ToolbarItem" Data="_toolbarItems"
ItemSelector="@((i, z) => true)"
CanDrop="@((i) => true)"
Drop="OnToolbarItemDrop"
ItemRender="OnToolbarItemRender">
<ChildContent>
<RadzenDropZone TItem="ToolbarItem" class="rounded">
</RadzenDropZone>
</ChildContent>
<Template>
<div>
<strong>@context.Name</strong>
<RadzenButton Icon="delete" Click="@((e) => DeleteToolbarItem(context))" Size="ButtonSize.ExtraSmall" ButtonStyle="ButtonStyle.Light" />
</div>
</Template>
</RadzenDropZoneContainer>
</div>
</div>
</div>
</div>
<div class="mt-2 text-end">
<RadzenButton Text="OK" Click=@OnOkClick />
<RadzenButton Text="Cancel" Click=@OnCancelClick ButtonStyle="ButtonStyle.Secondary" />
</div>
@code {
private readonly IList<string> _themes = new List<string>
{
"default",
"dark",
"material",
"material-dark",
"standard",
"standard-dark",
"humanistic",
"humanistic-dark",
"software",
"software-dark"
};
private readonly IList<string> _backgroundColors = new List<string> { "Default", "Light", "Dark" };
private int _settingScope;
private string _theme;
private string _background;
private IList<ToolbarItem> _toolbarItems = new List<ToolbarItem>();
private string _addToolbarItem;
protected override async Task OnInitializedAsync()
{
_settingScope = await EditorSettingService.GetSettingScopeAsync(ModuleState.ModuleId);
await LoadSettings();
}
private async Task<RadzenEditorSetting> LoadSettingsFromModule()
{
return await EditorSettingService.LoadSettingsFromModuleAsync(ModuleState.ModuleId);
}
private async Task<RadzenEditorSetting> LoadSettingsFromSite()
{
return await EditorSettingService.LoadSettingsFromSiteAsync(PageState.Site.SiteId);
}
private async Task LoadSettings()
{
var editorSetting = _settingScope == 1 ? await LoadSettingsFromModule() : await LoadSettingsFromSite();
_theme = editorSetting.Theme;
_background = editorSetting.Background;
_toolbarItems = editorSetting.ToolbarItems.Split(',').Select((v, i) =>
{
return new ToolbarItem { Key = i, Name = v };
}).ToList();
}
private async Task OnScopeChanged()
{
await LoadSettings();
StateHasChanged();
}
private void AddToolbarItem()
{
if (!string.IsNullOrEmpty(_addToolbarItem))
{
_toolbarItems.Add(new ToolbarItem { Key = _toolbarItems.Count, Name = _addToolbarItem });
_addToolbarItem = string.Empty;
StateHasChanged();
}
}
private void ResetToolbarItem()
{
_toolbarItems = RadzenEditorDefinitions.DefaultToolbarItems.Split(',').Select((v, i) =>
{
return new ToolbarItem { Key = i, Name = v };
}).ToList();
StateHasChanged();
}
private void DeleteToolbarItem(ToolbarItem item)
{
_toolbarItems.Remove(item);
StateHasChanged();
}
private void OnCancelClick()
{
DialogService.Close(false);
}
private async Task OnOkClick()
{
var editorSetting = new RadzenEditorSetting
{
Theme = _theme,
Background = _background,
ToolbarItems = string.Join(",", _toolbarItems.Select(i => i.Name))
};
await EditorSettingService.UpdateSettingScopeAsync(ModuleState.ModuleId, _settingScope);
if (_settingScope == 1)
{
await EditorSettingService.SaveModuleSettingsAsync(ModuleState.ModuleId, editorSetting);
}
else
{
await EditorSettingService.SaveSiteSettingsAsync(PageState.Site.SiteId, editorSetting);
}
DialogService.Close(true);
}
private void OnToolbarItemDrop(RadzenDropZoneItemEventArgs<ToolbarItem> args)
{
if (args.ToItem != null && args.ToItem.Key != args.Item.Key)
{
_toolbarItems.Remove(args.Item);
_toolbarItems.Insert(_toolbarItems.IndexOf(args.ToItem), args.Item);
}
}
private void OnToolbarItemRender(RadzenDropZoneItemRenderEventArgs<ToolbarItem> args)
{
args.Attributes.Add("class", "rz-card rz-variant-flat rz-background-color-primary-lighter rz-color-on-primary-lighter rz-p-2 d-inline-block ms-1 mt-1");
}
public class ToolbarItem
{
public int Key { get; set; }
public string Name { get; set; }
}
}

View File

@ -7,7 +7,7 @@
</div>
@code {
public string Name => "TextArea";
public string Name => "Basic Text Editor";
private ElementReference _editor;
private string _content;

View File

@ -16,7 +16,7 @@
public bool Disabled { get; set; }
[Parameter]
public Action<bool?> OnChange { get; set; }
public Func<bool?, bool?> OnChange { get; set; }
protected override void OnInitialized()
{
@ -41,27 +41,35 @@
break;
}
_value = OnChange(_value);
SetImage();
OnChange(_value);
}
}
private void SetImage()
{
switch (_value)
if (!Disabled)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
switch (_value)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
}
}
else
{
_src = "images/disabled.png";
_title = Localizer["PermissionDisabled"];
}
StateHasChanged();

View File

@ -0,0 +1,8 @@
namespace Oqtane.Modules
{
public enum MessageStyle
{
Alert,
Toast
}
}

View File

@ -18,7 +18,7 @@ namespace Oqtane.Modules.HtmlText
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
Resources = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }
new Stylesheet("~/Module.css")
}
};
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Security;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
@ -139,14 +140,24 @@ namespace Oqtane.Modules
}
}
// path method
// path methods
public string ModulePath()
{
return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
}
public string StaticAssetPath
{
get
{
// requires module to have implemented IModule
return PageState?.Alias.BaseUrl + "_content/" + ModuleState.ModuleDefinition?.PackageName + "/";
}
}
// fingerprint hash code for static assets
public string Fingerprint
{
get
@ -155,6 +166,18 @@ namespace Oqtane.Modules
}
}
// authorization methods
public bool IsAuthorizedRole(string roleName)
{
return UserSecurity.IsAuthorized(PageState.User, roleName);
}
public bool IsAuthorizedPermission(string permissionName)
{
return UserSecurity.IsAuthorized(PageState.User, permissionName, ModuleState.PermissionList);
}
// url methods
// navigate url
@ -356,7 +379,17 @@ namespace Oqtane.Modules
public void AddModuleMessage(string message, MessageType type, string position)
{
RenderModeBoundary.AddModuleMessage(message, type, position);
AddModuleMessage(message, type, position, MessageStyle.Alert);
}
public void AddModuleMessage(string message, MessageType type, MessageStyle style)
{
AddModuleMessage(message, type, "top", style);
}
public void AddModuleMessage(string message, MessageType type, string position, MessageStyle style)
{
RenderModeBoundary.AddModuleMessage(message, type, position, style);
}
public void ClearModuleMessage()
@ -417,6 +450,9 @@ namespace Oqtane.Modules
await interop.ScrollTo(0, 0, "smooth");
}
// token replace methods
public string ReplaceTokens(string content)
{
return ReplaceTokens(content, null);
@ -500,33 +536,46 @@ namespace Oqtane.Modules
};
}
// date methods
// date conversion methods
public DateTime? UtcToLocal(DateTime? datetime)
{
TimeZoneInfo timezone = null;
// Early return if input is null
if (datetime == null || datetime.Value == DateTime.MinValue || datetime.Value == DateTime.MaxValue)
return datetime;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.UtcAsLocalDateTime(datetime, timezone);
return Utilities.UtcAsLocalDateTime(datetime, timezoneId);
}
public DateTime? LocalToUtc(DateTime? datetime)
{
TimeZoneInfo timezone = null;
// Early return if input is null
if (datetime == null || datetime.Value == DateTime.MinValue || datetime.Value == DateTime.MaxValue)
return datetime;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.LocalDateAndTimeAsUtc(datetime, timezone);
return Utilities.LocalDateAndTimeAsUtc(datetime, timezoneId);
}
// logging methods

View File

@ -1,43 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>6.1.3</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.1.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
<IsPackable>true</IsPackable>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<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="Radzen.Blazor" Version="7.4.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Installer\Controls\AzureSqlConfig.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>

View File

@ -183,4 +183,7 @@
<data name="Refresh.Text" xml:space="preserve">
<value>Refresh</value>
</data>
</root>
<data name="Message.Job.Disabled" xml:space="preserve">
<value>The job cannot be started while in the disabled state.</value>
</data>
</root>

View File

@ -133,7 +133,7 @@
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification.</value>
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That New User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification Containing Further Instructions.</value>
</data>
<data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value>

View File

@ -126,6 +126,9 @@
<data name="Success.Module.Download" xml:space="preserve">
<value>Module Package Downloaded Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data>
<data name="Success.Module.Upload" xml:space="preserve">
<value>Module Package Uploaded Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data>
<data name="Error.Module.Download" xml:space="preserve">
<value>Error Downloading Module</value>
</data>

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