Compare commits

..

302 Commits

Author SHA1 Message Date
cd1f12e9b4 Merge pull request #2557 from oqtane/master
Merge pull request #2556 from oqtane/dev
2023-01-12 12:50:09 -05:00
734b9b6458 Merge pull request #2556 from oqtane/dev
3.3.0 Release
2023-01-12 12:49:49 -05:00
a120449c8d Update README.md 2023-01-12 12:48:54 -05:00
b005a1da56 Merge pull request #2555 from sbwalker/dev
remove extra info from Body as the From display name is now always set to the user's name
2023-01-12 11:47:32 -05:00
afc75a09d9 remove extra info from Body as the From display name is now always set to the user's name 2023-01-12 11:48:25 -05:00
c6e6c98875 improve notification job 2023-01-12 09:13:13 -05:00
fa1a5ab913 Merge pull request #2554 from sbwalker/dev
improve notification job
2023-01-12 09:12:11 -05:00
b671b590ad change Sql Manager logging level 2023-01-12 08:18:36 -05:00
a46c98eed3 Merge pull request #2553 from sbwalker/dev
change Sql Manager logging level
2023-01-12 08:17:42 -05:00
7ab5ed5bcb copyright year update 2023-01-11 14:40:16 -05:00
5e609edc31 Merge pull request #2551 from sbwalker/dev
copyright year update
2023-01-11 14:39:15 -05:00
ac466429f8 improve release batch file 2023-01-11 10:24:50 -05:00
2804c50c8a Merge pull request #2550 from sbwalker/dev
improve release batch file
2023-01-11 10:24:18 -05:00
c4315c25bc prepare for 3.3.0 release 2023-01-10 14:02:23 -05:00
e2d5fa48cf Merge pull request #2548 from sbwalker/dev
prepare for 3.3.0 release
2023-01-10 14:01:48 -05:00
c2375c897d permission updates 2023-01-10 08:20:32 -05:00
49f181583b Merge pull request #2546 from sbwalker/dev
permission updates
2023-01-10 08:20:27 -05:00
ea463a6548 fix #2534 - added Relay Configured site setting to enable sending from users email address 2023-01-09 16:37:06 -05:00
1a567dbdc1 Merge pull request #2545 from sbwalker/dev
fix #2534 - added Relay Configured site setting to enable sending from users email address
2023-01-09 16:36:30 -05:00
e4ec10ef49 format PermissionNames to be more readable 2023-01-09 15:36:41 -05:00
3db4fc687a Merge pull request #2544 from sbwalker/dev
format PermissionNames to be more readable
2023-01-09 15:35:59 -05:00
e136972cd7 add support for API permissions at the UI layer - including ability to delegate user, role, profile management 2023-01-09 11:38:25 -05:00
7b1b32e16f Merge pull request #2543 from sbwalker/dev
add support for API permissions at the UI layer - including ability to delegate user, role, profile management
2023-01-09 11:37:54 -05:00
1616f94b86 add ability to view error.log in System Info 2023-01-05 10:18:55 -05:00
7043d1f3bf Merge pull request #2542 from sbwalker/dev
add ability to view error.log in System Info
2023-01-05 10:18:10 -05:00
7bebfe1919 fix typo and help text 2023-01-05 09:55:06 -05:00
d33ded0426 Merge pull request #2541 from sbwalker/dev
fix typo and help text
2023-01-05 09:54:23 -05:00
66aa67581f improve dynamic policy registration to handle possible race conditions 2023-01-05 09:43:59 -05:00
3a95c05db9 Merge pull request #2540 from sbwalker/dev
improve dynamic policy registration to handle possible race conditions
2023-01-05 09:43:15 -05:00
67046e9d36 Merge pull request #2535 from thabaum/Readme-Glow-Logo
Update framework root to use the new dark/light theme glow logo
2023-01-04 14:51:39 -05:00
6f2965a5b0 Merge pull request #2537 from leigh-pointer/Pager-Pointer
A change to the Pager bar to set the mouse pointer to pointer
2023-01-04 14:51:29 -05:00
197db449a1 Merge pull request #2539 from sbwalker/dev
include owner in migration tag name in external module template
2023-01-04 14:51:11 -05:00
f4800bb7f0 include owner in migration tag name in external module template 2023-01-04 14:51:55 -05:00
6a213561f6 added an autocomplete component and implemented in permission grid 2023-01-04 14:50:05 -05:00
8f8d31f0c9 Merge pull request #2538 from sbwalker/dev
added an autocomplete component and implemented in permission grid
2023-01-04 14:49:23 -05:00
f8cfdacc26 A change to the Pager bar to set the mouse pointer to pointer
Currently the mouse pointer shows the Selector icon when hoovered over the page number buttons. This is an update changing the icon to the Pointer icon.

Updated the CSS class name to 'app-pager-pointer' in app.css and the Component.
2023-01-03 14:24:17 +01:00
07223e27a4 Add Oqtane Glow Logo for Dark/Light Themes 2022-12-27 10:30:00 -08:00
4c6f46ad17 Remove Dark Logo from Framework Root 2022-12-27 10:28:37 -08:00
39dc288f9c Merge pull request #2533 from sbwalker/dev
fix #2526 - support multiple TabStrip components on a page
2022-12-19 15:57:30 -05:00
467e88ef55 fix #2526 - support multiple TabStrip components on a page 2022-12-19 15:58:04 -05:00
0965db5d57 fix skip pages logic in pager where screen was not being refreshed 2022-12-19 15:11:04 -05:00
c30339d9b8 Merge pull request #2532 from sbwalker/dev
fix skip pages logic in pager where screen was not being refreshed
2022-12-19 15:10:27 -05:00
369adcb173 allow profile management to be delegated 2022-12-19 15:07:38 -05:00
d837b981d4 Merge pull request #2531 from sbwalker/dev
allow profile management to be delegated
2022-12-19 15:07:03 -05:00
f455461c1e Update README.md 2022-12-14 14:54:23 -05:00
0087b188a1 improve null handling 2022-12-07 10:33:25 -05:00
98602f49d8 Merge pull request #2522 from sbwalker/dev
improve null handling
2022-12-07 10:33:21 -05:00
70466e5626 Merge pull request #2521 from sbwalker/dev
fix #2513 - add new methods for deleting a setting and retrieving a list of settings
2022-12-07 08:59:07 -05:00
cc7f98a6fe fix #2513 - add new methods for deleting a setting and retrieving a list of settings 2022-12-07 08:59:03 -05:00
fd13ad1fca initialize API permissions based on default roles 2022-12-06 17:16:51 -05:00
5077f6fbca Merge pull request #2520 from sbwalker/dev
initialize API permissions based on default roles
2022-12-06 17:16:47 -05:00
9b15ce6d29 Merge pull request #2519 from sbwalker/dev
make casing consistent in route template definition and method parameter declation or else Swagger will not be able to resolve
2022-12-06 10:48:57 -05:00
5a8ca24566 make casing consistent in route template definition and method parameter declation or else Swagger will not be able to resolve 2022-12-06 10:48:56 -05:00
d3f982cae1 Merge pull request #2518 from sbwalker/dev
add ModuleControllerBase helper method for validating EntityId
2022-12-05 14:21:15 -05:00
28b58b9048 add ModuleControllerBase helper method for validating EntityId 2022-12-05 14:21:12 -05:00
cb10dde97d added API Management for managing site level entity permissions 2022-12-02 16:42:43 -05:00
2c179867e8 Merge pull request #2517 from sbwalker/dev
added API Management for managing site level entity permissions
2022-12-02 16:42:41 -05:00
44fc6de82b hide connection string by default in SQL Management and provide toggle for display 2022-12-02 08:16:18 -05:00
f671af43cd Merge pull request #2516 from sbwalker/dev
hide connection string by default in SQL Management and provide toggle for display
2022-12-02 08:16:18 -05:00
8c0dc6422e Merge pull request #2515 from sbwalker/dev
fix #2512 - provide guidance about password complexity policy during install, and ensure modified passwords meet complexity policy
2022-12-02 07:42:55 -05:00
c91e285475 fix #2512 - provide guidance about password complexity policy during install, and ensure modified passwords meet complexity policy 2022-12-02 07:42:49 -05:00
642e41530e Merge pull request #2514 from sbwalker/dev
enhance dynamic authorization policies to support default role specification
2022-12-02 07:34:08 -05:00
b09a3ccdae enhance dynamic authorization policies to support default role specification 2022-12-02 07:34:06 -05:00
a1aab62cea Merge pull request #2504 from leigh-pointer/PagerPointer
A change to the Pager bar to set the mouse pointer to pointer
2022-11-23 11:26:57 -05:00
e8b1aca45c Merge pull request #2507 from sbwalker/dev
fix #2502 - invalid logic checking querystring parameter
2022-11-23 11:26:21 -05:00
3de98873d6 fix #2502 - invalid logic checking querystring parameter 2022-11-23 11:26:23 -05:00
c030ede12c Merge pull request #2506 from sbwalker/dev
fix #2503 - generate password using CultureInfo.InvariantCulture to ensure it satisfies password complexity criteria
2022-11-23 11:11:05 -05:00
67f740c264 fix #2503 - generate password using CultureInfo.InvariantCulture to ensure it satisfies password complexity criteria 2022-11-23 11:10:59 -05:00
9b68337047 Merge pull request #2505 from sbwalker/dev
fix #2501 - set default Visibility to Same As Page when adding module to a page
2022-11-23 10:51:50 -05:00
2bae971b92 fix #2501 - set default Visibility to Same As Page when adding modules to a page 2022-11-23 10:51:37 -05:00
c5c5fd859f A change to the Pager bar to set the mouse pointer to pointer
Currently the mouse pointer shows the Selector icon when hoovered over the page number buttons.  This is an update changing the icon to the Pointer icon.
2022-11-21 09:44:30 +01:00
15c46fd157 Merge pull request #2496 from sbwalker/dev
Fix #2488 - add ability to include inline script resource definitions in modules and themes
2022-11-12 10:59:25 -05:00
424950bd3e Fix #2488 - add ability to include inline script resource definitions in modules and themes 2022-11-12 10:58:58 -05:00
39a9968971 Merge pull request #2493 from sbwalker/dev
fix JS Interop methods for includeScript and includeMeta
2022-11-10 14:19:52 -05:00
075a09f0df fix JS Interop methods for includeScript and includeMeta 2022-11-10 14:19:31 -05:00
26e628e189 Merge pull request #2492 from sbwalker/dev
move UI logic from FileService to FileManager, add progressive retry logic, update file attributes if uploading a new version of a file, clean up temporary artifacts on failure, improve upload efficiency
2022-11-09 21:11:42 -05:00
7489d9d186 move UI logic from FileService to FileManager, add progressive retry logic, update file attributes if uploading a new version of a file, clean up temporary artifacts on failure, improve upload efficiency 2022-11-09 21:11:02 -05:00
7db5b6c7f3 Update README.md 2022-11-08 08:59:49 -05:00
9b7fa8cac2 Update README.md 2022-11-08 08:53:33 -05:00
5028051a34 Update README.md 2022-11-08 08:52:51 -05:00
83b3767df6 Merge pull request #2490 from sbwalker/dev
Scope permissions by SiteId to support entity level authorization as well as improve caching and performance. Optimize GetTenant to use existing cache.
2022-11-07 18:17:04 -05:00
6182b96d16 Scope permissions by SiteId to support entity level authorization as well as improve caching and performance. Optimize GetTenant to use existing cache. 2022-11-07 18:16:32 -05:00
a719382563 Merge pull request #2482 from sbwalker/dev
add support for dynamic authorization policies
2022-11-04 08:08:33 -04:00
2aa6eb90e2 add support for dynamic authorization policies 2022-11-04 08:08:10 -04:00
d2495455cd Merge pull request #2477 from sbwalker/dev
added ETag / 304 Not Modified logic to File server for performance optimization
2022-10-29 10:23:58 -04:00
23d1dd23d1 added ETag / 304 Not Modified logic to File server for performance optimization 2022-10-29 10:23:04 -04:00
573acd6a5d Merge pull request #2476 from sbwalker/dev
fix File Update API to update the file size and image dimensions
2022-10-27 09:39:05 -04:00
40ddbbfbb7 fix File Update API to update the file size and image dimensions 2022-10-27 09:38:26 -04:00
1b488b298b Merge pull request #2475 from sbwalker/dev
remove IDeletable fields from Folder and File entities as they are never set and not used
2022-10-26 17:43:09 -04:00
54b45943db remove IDeletable fields from Folder and File entities as they are never set and not used 2022-10-26 17:42:26 -04:00
89b3ae4c74 Merge pull request #2474 from sbwalker/dev
fix IDeletable code documentation
2022-10-26 17:33:52 -04:00
fe97a76d00 fix IDeletable code documentation 2022-10-26 17:33:17 -04:00
78094f69d7 Merge pull request #2473 from sbwalker/dev
update models to use new ModelBase
2022-10-26 17:27:22 -04:00
4499d55464 update models to use new ModelBase 2022-10-26 17:26:46 -04:00
4c12ca0607 Merge pull request #2472 from sbwalker/dev
introduced a ModelBase to move the IAuditable properties to a base class
2022-10-26 17:12:38 -04:00
1daa9575db introduced a ModelBase to move the IAuditable properties to a base class 2022-10-26 17:12:03 -04:00
f7c9961f8c Merge pull request #2471 from sbwalker/dev
add loading icon to Maui project
2022-10-26 10:06:57 -04:00
e27a625069 add loading icon to Maui project 2022-10-26 09:04:04 -04:00
42b1669cce Merge pull request #2469 from sbwalker/dev
remove Oqtane.Server from external templates as they cause random compilation issues
2022-10-20 14:05:45 -04:00
74571afc9e remove Oqtane.Server from external templates as they cause random compilation issues 2022-10-20 14:04:53 -04:00
f678f11c59 Merge pull request #2468 from sbwalker/dev
add validation message for missing package name
2022-10-20 14:00:56 -04:00
e685252b1d add validation message for missing package name 2022-10-20 14:00:17 -04:00
bb75fcb5a3 Merge pull request #2467 from sbwalker/dev
fix language delete refresh
2022-10-20 13:43:36 -04:00
7653f36f31 fix language delete refresh 2022-10-20 13:42:54 -04:00
0670148064 Merge pull request #2466 from sbwalker/dev
fix #2464 - translation install/upgrade experience
2022-10-20 13:17:06 -04:00
368b900a6e fix #2464 - translation install/upgrade experience 2022-10-20 13:16:18 -04:00
6378a78cbd Update README.md 2022-10-18 08:10:48 -04:00
d3bcdec0f2 Update README.md 2022-10-18 08:10:02 -04:00
82c074ce2e Merge pull request #2463 from sbwalker/dev
replace assembly references with package references
2022-10-18 07:52:33 -04:00
a313e2d386 replace assembly references with package references 2022-10-18 07:51:50 -04:00
578cb2f7e3 Update README.md 2022-10-18 07:43:18 -04:00
c8f56e1659 Merge pull request #2461 from oqtane/master
Merge pull request #2460 from oqtane/dev
2022-10-17 16:46:03 -04:00
56631a3e6b Merge pull request #2460 from oqtane/dev
3.2.1 release
2022-10-17 16:45:47 -04:00
84ee9de18c Merge pull request #2459 from sbwalker/dev
Changed default service url in MAUI so users can immediately run client app
2022-10-17 08:12:53 -04:00
0aeb4e9173 Changed default service url in MAUI so users can immediately run client app 2022-10-17 08:12:04 -04:00
bb65e5c373 Merge pull request #2455 from sbwalker/dev
prepare for 3.2.1 release
2022-10-13 13:35:36 -04:00
45e2027c56 prepare for 3.2.1 release 2022-10-13 13:34:43 -04:00
6dc5ef44b7 Merge pull request #2454 from sbwalker/dev
Resolve deserialization issue with System.Text.Json when accessing remote services
2022-10-12 12:38:08 -04:00
e88d3cca07 Resolve deserialization issue with System.Text.Json when accessing remote services 2022-10-12 12:37:03 -04:00
a4b7381141 Merge pull request #2451 from sbwalker/dev
fix #2435 - remove NewtonSoft.Json dependency
2022-10-11 08:35:44 -04:00
2ea054dc72 fix #2435 - remove NewtonSoft.Json dependency 2022-10-11 08:34:33 -04:00
13ec726ab2 Merge pull request #2450 from sbwalker/dev
add file download event
2022-10-05 08:02:22 -04:00
2e32b65421 add file download event 2022-10-05 08:00:45 -04:00
f48ca53cdb Merge pull request #2449 from sbwalker/dev
Enhance SyncManager to raise events which can be handled on the server within hosted services. Raise create, update, delete events for all major entities. Include support for refresh and reload events to synchronize client state. Move client state cache invalidation to a hosted service to separate concerns and demonstrate events.
2022-10-04 19:21:35 -04:00
c5b632cb24 Enhance SyncManager to raise events which can be handled on the server within hosted services. Raise create, update, delete events for all major entities. Include support for refresh and reload events to synchronize client state. Move client state cache invalidation to a hosted service to separate concerns and demonstrate events. 2022-10-04 19:20:02 -04:00
422e9ae99e Update README.md 2022-09-30 15:24:15 -04:00
68ada8fbe4 Merge pull request #2431 from chlupac/InstallFix
Unattended installation fix
2022-09-30 11:49:06 -04:00
e40bf08691 Merge pull request #2446 from sbwalker/dev
add upgrade logic for sites using remapped identifier and email claim…
2022-09-30 09:54:59 -04:00
a04c7222b2 add upgrade logic for sites using remapped identifier and email claim types 2022-09-30 09:53:37 -04:00
172faec5a0 Merge pull request #2445 from sbwalker/dev
log any user creation errors from .NET Identity
2022-09-29 17:18:48 -04:00
e01c3e7e4a log any user creation errors from .NET Identity 2022-09-29 17:16:29 -04:00
7a3d5d0429 Merge pull request #2444 from sbwalker/dev
fix #2432 - add support for roles as part of external login via OIDC
2022-09-29 16:34:18 -04:00
ddf1caaaaa fix #2432 - add support for roles as part of external login via OIDC 2022-09-29 16:32:50 -04:00
021293cbb0 Merge pull request #2443 from sbwalker/dev
fix #2427 - issue with upgrade available in Language Management
2022-09-28 16:18:08 -04:00
1438e61f1b fix #2427 - issue with upgrade available in Language Management 2022-09-28 16:16:46 -04:00
182f4dbae7 Merge pull request #2442 from sbwalker/dev
fix #2426 - error in recycle bin
2022-09-28 13:56:46 -04:00
26ec3fc7cf fix #2426 - error in recycle bin 2022-09-28 13:55:12 -04:00
225c758795 Merge pull request #2440 from leigh-pointer/PagerFooter
Add footer to the Pager control
2022-09-28 09:45:48 -04:00
5e653250f3 Merge pull request #2441 from sbwalker/dev
Fix #2439 - ensure resource urls are constructed consistently on client and server
2022-09-28 09:44:32 -04:00
b7a3713946 Fix #2439 - ensure resource urls are constructed consistently on client and server 2022-09-28 09:43:02 -04:00
44242fdb4a Add footer to the Pager control
Mirrored the Head functionality for <tfoot> tag
2022-09-28 14:00:53 +02:00
45515b2c06 Unattented instalation fix 2022-09-24 15:44:20 +02:00
2d95fe294c Merge pull request #2430 from sbwalker/dev
Add Blazor Server reconnect script, fix event log direct link from notification email, add more validation to Pager, improve browser refresh script to wait for server availability
2022-09-24 08:38:54 -04:00
72cc44641b Add Blazor Server reconnect script, fix event log direct link from notification email, add more validation to Pager, improve browser refresh script to wait for server availability 2022-09-24 08:37:18 -04:00
9ac3fa5269 Merge pull request #2425 from sbwalker/dev
fix new id convention in file server
2022-09-21 15:39:20 -04:00
d1ea141165 fix new id convention in file server 2022-09-21 15:37:52 -04:00
dca21fbb8c Merge pull request #2424 from sbwalker/dev
improve BaseUrl handling for MAUI, replace ContentUrl with FileUrl and improve file server
2022-09-21 13:39:51 -04:00
06812d5df8 improve BaseUrl handling for MAUI, replace ContentUrl with FileUrl and improve file server 2022-09-21 13:38:21 -04:00
564410d3cd Update README.md 2022-09-19 17:10:11 -04:00
4b1cec979a Merge pull request #2422 from sbwalker/dev
rename list name to match intent
2022-09-14 22:30:05 -04:00
a5f1bc3895 rename list name to match intent 2022-09-14 22:28:31 -04:00
b097d031b5 Merge pull request #2421 from sbwalker/dev
clean up pdb files on client, hash assembly file names
2022-09-14 10:11:22 -04:00
45df729711 clean up pdb files on client, hash assembly file names 2022-09-14 10:09:50 -04:00
802ee8a1ff Merge pull request #2419 from oqtane/master
Merge pull request #2418 from oqtane/dev
2022-09-13 09:18:35 -04:00
e312970212 Merge pull request #2418 from oqtane/dev
3.2.0 release
2022-09-13 09:18:16 -04:00
c0f4069a9b Merge pull request #2417 from sbwalker/dev
refactor IndexedDB interop functions
2022-09-13 07:44:10 -04:00
654352827e refactor IndexedDB interop functions 2022-09-13 07:42:27 -04:00
7dd210976d Merge pull request #2416 from sbwalker/dev
optimize assembly list retrieval
2022-09-12 16:21:11 -04:00
5302be8bc1 optimize assembly list retrieval 2022-09-12 16:19:32 -04:00
9ed7181e28 Merge pull request #2415 from sbwalker/dev
remove unnecessary using statements
2022-09-12 14:56:35 -04:00
23ae4b01cb remove unnecessary using statements 2022-09-12 14:54:31 -04:00
59764d3378 Merge pull request #2414 from sbwalker/dev
cache assemblies in IndexedDB on WebAssembly
2022-09-12 14:48:17 -04:00
b8e2c729c1 cache assemblies in IndexedDB on WebAssembly 2022-09-12 14:46:46 -04:00
530d80a011 Merge pull request #2412 from sbwalker/dev
optimize assembly loading for MAUI to use client storage
2022-09-11 10:50:20 -04:00
2d306e8fda optimize assembly loading for MAUI to use client storage 2022-09-11 10:48:40 -04:00
b3d9a70fd1 Merge pull request #2410 from sbwalker/dev
remove Oqtane.Server from Oqtane.Maui solution
2022-09-09 11:40:06 -04:00
b880207f61 remove Oqtane.Server from Oqtane.Maui solution 2022-09-09 11:37:33 -04:00
ee76d02999 Merge pull request #2409 from sbwalker/dev
improvements to run in Android Emulator
2022-09-09 10:27:45 -04:00
804c33a375 improvements to run in Android Emulator 2022-09-09 10:26:13 -04:00
de784714d9 Merge pull request #2408 from sbwalker/dev
fix issue in upgrade logic for making folder paths cross platform
2022-09-08 15:44:34 -04:00
2404e26b61 fix issue in upgrade logic for making folder paths cross platform 2022-09-08 15:43:03 -04:00
b191fdda2c Merge pull request #2407 from sbwalker/dev
prepare for 3.2.0
2022-09-08 15:30:05 -04:00
e8adfd45d2 prepare for 3.2.0 2022-09-08 15:28:25 -04:00
7158595801 Merge pull request #2406 from orionlaw/dev
Make sure Job date times are stored in the database as UTC.
2022-09-08 09:14:31 -04:00
ba97f63338 Make sure Job date times are stored in the database as UTC. This is required if using Postgres or you will get an exception with a message of “Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported.”. 2022-09-07 12:46:24 -06:00
62eca2aedc Merge pull request #2401 from chlupac/BackslashFix
Backslash fix.
2022-09-06 10:53:48 -04:00
b15f6b1fa7 Merge pull request #2402 from chlupac/GitignoreUpdate
Gitignore update
2022-09-06 10:53:05 -04:00
5b22de589c Merge pull request #2403 from sbwalker/dev
Fix #2399 - page paths not being validated for deleted pages
2022-09-06 10:52:34 -04:00
d1f50f12af Fix #2399 - page paths not being validated for deleted pages 2022-09-06 10:50:53 -04:00
d40c1d9b31 Backslash fix. 2022-09-06 09:14:58 +02:00
b69041d4af Gitignore update 2022-09-06 09:14:42 +02:00
64c5d9a09f Merge pull request #2400 from sbwalker/dev
more changes to support Default pane
2022-09-05 15:51:22 -04:00
dd170bb41a more changes to support Default pane 2022-09-05 15:49:38 -04:00
1e6e4033f8 Merge pull request #2398 from sbwalker/dev
changed UrlParameterTemplate name for consistency
2022-09-04 09:48:48 -04:00
01fabc8d9e changed UrlParameterTemplate name for consistency 2022-09-04 09:47:03 -04:00
2ca2539b53 Merge pull request #2397 from sbwalker/dev
fix #2366 - populate new UrlParameters property
2022-09-04 09:37:09 -04:00
51e2e2966f fix #2366 - populate new UrlParameters property 2022-09-04 09:35:18 -04:00
55d02d2db5 Merge pull request #2396 from sbwalker/dev
Fix #2382 - Admin pane improvements
2022-09-02 18:11:51 -04:00
282a0b0c44 Fix #2382 - Admin pane improvements 2022-09-02 18:10:13 -04:00
8432779b23 Merge pull request #2395 from sbwalker/dev
added public Refresh method to FileManager
2022-09-02 09:12:41 -04:00
13b9982461 added public Refresh method to FileManager 2022-09-02 09:11:00 -04:00
d76b8cebdc Merge pull request #2389 from sbwalker/dev
Changes for .NET MAUI on Android
2022-08-31 16:35:53 -04:00
80315ae6d4 Changes for .NET MAUI on Android 2022-08-31 16:34:14 -04:00
95e7344286 Merge pull request #2385 from sbwalker/dev
moved hierarchical ordering logic to server for pages and folders
2022-08-30 07:33:46 -04:00
28f73727b5 moved hierarchical ordering logic to server for pages and folders 2022-08-30 07:31:56 -04:00
68f5bf5759 Merge pull request #2384 from sbwalker/dev
made folder paths cross platform, introduced file handler for abstracting the serving of files, enabled url mapping for broken file links, resolved public folder deletion issue
2022-08-30 07:23:50 -04:00
075748d697 made folder paths cross platform, introduced file handler for abstracting the serving of files, enabled url mapping for broken file links, resolved public folder deletion issue 2022-08-30 07:21:52 -04:00
e8d86f94f2 Merge pull request #2376 from sbwalker/dev
fix rootnamespace
2022-08-19 16:12:32 -04:00
d6bb802892 fix rootnamespace 2022-08-19 16:10:27 -04:00
52680e9002 Merge pull request #2375 from sbwalker/dev
prepare for 3.2.0
2022-08-19 15:59:35 -04:00
d6385d82ae prepare for 3.2.0 2022-08-19 15:57:31 -04:00
d058de067c Merge pull request #2374 from sbwalker/dev
Prepare for 3.2.0 release
2022-08-19 15:56:39 -04:00
32d6d143dd Prepare for 3.2.0 release 2022-08-19 15:54:33 -04:00
b49432802b Merge pull request #2373 from sbwalker/dev
Improvements to richtexteditor to allow file management in raw html editor. Also allow disabling of raw html editor which can be utilized via new setting in Html/Text module.
2022-08-19 15:34:43 -04:00
99d4d75d8e Improvements to richtexteditor to allow file management in raw html editor. Also allow disabling of raw html editor which can be utilized via new setting in Html/Text module. 2022-08-19 15:32:30 -04:00
1f584d57ac Merge pull request #2372 from sbwalker/dev
optimize Url Parameters and implement in Event Log
2022-08-18 16:06:34 -04:00
2c1543aa82 optimize Url Parameters and implement in Event Log 2022-08-18 16:04:30 -04:00
4390cbbfae Update README.md 2022-08-18 08:35:49 -04:00
bbf9e5717e Merge pull request #2370 from sbwalker/dev
improve support for module content editors
2022-08-16 17:27:54 -04:00
c7edc28bd9 improve support for module content editors 2022-08-16 17:25:46 -04:00
6e0de6f7bf Merge pull request #2369 from sbwalker/dev
check for existence of appsettings.json on Maui
2022-08-16 09:42:14 -04:00
3659422165 check for existence of appsettings.json on Maui 2022-08-16 09:40:03 -04:00
1af30da44e Merge pull request #2368 from sbwalker/dev
trim list of pages allowed to be Home Page
2022-08-16 08:44:53 -04:00
56c082cb26 trim list of pages allowed to be Home Page 2022-08-16 08:42:47 -04:00
e8eca582de Merge pull request #2363 from sbwalker/dev
added ability to specify a site home page, updated default template content to include .NET MAUI
2022-08-15 17:03:34 -04:00
4084b352de added ability to specify a site home page, updated default template content to include .NET MAUI 2022-08-15 17:01:20 -04:00
633e4acf0e Merge pull request #2362 from sbwalker/dev
add Site option for specifying a Hosting Model of Blazor Hybrid
2022-08-15 09:32:42 -04:00
468df15d80 add Site option for specifying a Hosting Model of Blazor Hybrid 2022-08-15 09:30:36 -04:00
f4537b4fcb Merge pull request #2361 from sbwalker/dev
optimize site router
2022-08-14 11:24:43 -04:00
8bca345b45 optimize site router 2022-08-14 11:22:39 -04:00
ee80712c77 Merge pull request #2360 from sbwalker/dev
resolve issue with deleted pages and modules caused by refactoring
2022-08-12 18:04:51 -04:00
3cf7153f44 resolve issue with deleted pages and modules caused by refactoring 2022-08-12 18:02:45 -04:00
8e2fc75e48 Update README.md 2022-08-12 17:09:15 -04:00
4aa51c8583 Merge pull request #2359 from sbwalker/dev
performance improvements to reduce http and database interactions
2022-08-12 16:49:59 -04:00
3c6ebd7742 performance improvements to reduce http and database interactions 2022-08-12 16:47:51 -04:00
b85539dc17 Merge pull request #2358 from sbwalker/dev
add ability to dynamically set module title and visible from components
2022-08-12 13:08:14 -04:00
4cae3f02ed add ability to dynamically set module title and visible from components 2022-08-12 13:05:48 -04:00
469b436f10 Merge pull request #2356 from dkoeder/dev
Some methods failing in BaseEntityBuilder if Schema is not null.
2022-08-12 10:45:14 -04:00
66e3e6729b Merge pull request #2357 from sbwalker/dev
add support for preserving state when loading admin components
2022-08-12 10:45:05 -04:00
fc6a794714 add support for preserving state when loading admin components 2022-08-12 10:43:00 -04:00
d75ed3d5ac Update BaseEntityBuilder.cs
Some methods failing in BaseEntityBuilder if Schema is not null.
2022-08-11 16:15:09 -06:00
bd0a218214 Merge pull request #2355 from sbwalker/dev
Blazor Hybrid / .NET MAUI support
2022-08-11 17:11:45 -04:00
f96129fa37 Blazor Hybrid / .NET MAUI support 2022-08-11 17:09:32 -04:00
920418618a Update README.md 2022-08-10 08:30:38 -04:00
29247481a6 Merge pull request #2351 from ajahangard/patch-1
#Bug in passing Lifetime property to GenerateToken
2022-08-09 10:47:28 -04:00
773710aeef #Bug in passing Lifetime property to GenerateToken
Audience is passed to GenerateToken instead of Lifetime.
2022-08-09 15:32:52 +04:30
d0c8ee57e6 Merge pull request #2348 from sbwalker/dev
Fix satellite assembly loading issue when running on WebAssembly
2022-08-08 10:49:42 -04:00
cf2adc7f6a Fix satellite assembly loading issue when running on WebAssembly 2022-08-08 10:47:33 -04:00
b621f24540 Merge pull request #2342 from sbwalker/dev
Fix #2336 - error.png path incorrect
2022-08-06 16:27:45 -04:00
99be638525 Fix #2336 - error.png path incorrect 2022-08-06 16:27:24 -04:00
d35c204e07 Merge pull request #2341 from sbwalker/dev
Fix #2339 - refactor module upgrade logic to  remove requirement on ServerManagerType for modules which have no backend
2022-08-06 16:13:53 -04:00
d8b4267668 Fix #2339 - refactor module upgrade logic to remove requirement on ServerManagerType for modules which have no backend 2022-08-06 16:13:28 -04:00
3c2f3be451 Merge pull request #2338 from chlupac/TruncateAgent
Truncate UserAgent for save to Visitors table #2337
2022-08-05 09:16:54 -04:00
e846cf8672 Truncate UserAgent for save to Visitors table 2022-08-04 16:14:39 +02:00
8804bce6c0 Merge pull request #2331 from sbwalker/dev
add proper translation keys for ActionLink and ActionDialog into RESX for Module Creator template
2022-08-03 08:50:12 -04:00
83acda6d05 add proper translation keys for ActionLink and ActionDialog into RESX for Module Creator template 2022-08-03 08:49:47 -04:00
063719532f Merge pull request #2328 from sbwalker/dev
include ResourceType attribute in Settings component for external module template
2022-08-02 07:55:38 -04:00
7b1b061355 include ResourceType attribute in Settings component for external module template 2022-08-02 07:55:11 -04:00
ed4540887e Merge pull request #2326 from leigh-pointer/Bootstrap5.2
Formating issues with Bootstrap 5.2
2022-08-02 07:51:33 -04:00
6968476ed0 Merge pull request #2327 from leigh-pointer/ScreenProgess
Added Progress Indicator
2022-08-02 07:50:59 -04:00
5d2c7c3058 Added Progress Indicator
When deleting large blocks of Pages, Modules or Notifications there was currently no visual feedback so added the ModuleInstance.ShowProgressIndicator() and ModuleInstance.HideProgressIndicator() calls to these processes.
2022-08-02 10:37:04 +02:00
e6cb90e545 Formating issues with Bootstrap 5.2 2022-08-02 08:55:42 +02:00
ec73f4dbea Merge pull request #2322 from leigh-pointer/Bootstrap5.2
Updated Bootstrap to 5.2
2022-08-01 17:36:10 -04:00
4f41a52ee7 Merge pull request #2325 from sbwalker/dev
fix upgrade issue for framework translations, improvements for managing module translations
2022-08-01 17:05:57 -04:00
c097956fcb fix upgrade issue for framework translations, improvements for managing module translations 2022-08-01 17:05:18 -04:00
8cbc17ed98 Theme Creator updated to Bootstrap 5.2.0 2022-07-28 20:59:52 +02:00
50d89d0f13 Updated Bootstrap to 5.2
Replaced Bootstrap cloudflare versions and Integrity keys to match 5.2.0
2022-07-28 20:52:11 +02:00
7b4d13b73e Merge pull request #2318 from oqtane/master
Merge pull request #2317 from oqtane/dev
2022-07-27 16:16:01 -04:00
2909aa1656 Merge pull request #2317 from oqtane/dev
3.1.4 release
2022-07-27 16:15:43 -04:00
64131b6764 Update README.md 2022-07-27 08:54:48 -04:00
0b78d75a21 Merge pull request #2314 from sbwalker/dev
prepare for 3.1.4 release
2022-07-26 17:22:27 -04:00
b35c342960 prepare for 3.1.4 release 2022-07-26 17:22:06 -04:00
24c858d379 Merge pull request #2313 from sbwalker/dev
support for module translation download/install
2022-07-26 14:44:25 -04:00
b8a31a8be9 support for module translation download/install 2022-07-26 14:44:06 -04:00
02c30d6454 Merge pull request #2312 from sbwalker/dev
add ability to supply connection string in Add Site
2022-07-26 10:13:14 -04:00
985f003e6d add ability to supply connection string in Add Site 2022-07-26 10:12:54 -04:00
98045e1e2e Merge pull request #2311 from sbwalker/dev
introduce ITransientService interface for auto registration of transient services (for DBContexts and Repositories)
2022-07-26 09:42:12 -04:00
5762ce58a4 introduce ITransientService interface for auto registration of transient services (for DBContexts and Repositories) 2022-07-26 09:41:42 -04:00
2787ee71fc Merge pull request #2310 from sbwalker/dev
Allow for entry of raw connection string during installation
2022-07-26 07:48:29 -04:00
e61a6df4d7 Allow for entry of raw connection string during installation 2022-07-26 07:48:04 -04:00
0a5c2ecbf5 Merge pull request #2304 from sbwalker/dev
optimize satellite assembly loading based on the new model where all cultures are available
2022-07-21 16:02:48 -04:00
6bfab696ad optimize satellite assembly loading based on the new model where all cultures are available 2022-07-21 16:02:23 -04:00
928f2dd496 Merge pull request #2302 from chlupac/FileServiceFix
FileService fix
2022-07-20 08:23:41 -04:00
bcf75892f7 FileService fix 2022-07-20 10:39:13 +02:00
594761385f Merge pull request #2301 from sbwalker/dev
add Environment to System Info
2022-07-19 14:34:10 -04:00
d05fba06ec add Environment to System Info 2022-07-19 14:33:51 -04:00
25155b1b38 Merge pull request #2295 from chlupac/AppSettingsFix
Fixed loading of alternative appsettings "appsettings.{env.EnvironmentName}.json"
2022-07-19 14:20:08 -04:00
ded6c9c199 Merge pull request #2299 from chlupac/InstallManFix
Fix - InstallationManager crash when package folders are missing
2022-07-19 13:12:43 -04:00
c62e6c0045 Merge pull request #2300 from sbwalker/dev
performance optimization for permissions
2022-07-19 10:49:52 -04:00
b3feda9fd1 performance optimization for permissions 2022-07-19 10:49:33 -04:00
7ef8e2c8b8 Fix - InstallationManager crash when package folders are missing 2022-07-19 09:42:12 +02:00
d5ff211871 Fixed loading of alternative appsettings "appsettings.{env.EnvironmentName}.json" 2022-07-18 21:34:59 +02:00
51c23e3842 Merge pull request #2294 from sbwalker/dev
use package name as a convention for identifying satellite assemblies
2022-07-18 13:14:53 -04:00
557b30815e use package name as a convention for identifying satellite assemblies 2022-07-18 13:14:34 -04:00
145459bfc3 Merge pull request #2292 from sbwalker/dev
Added version to Language Management, improved framework performance by loading languages into PageState, include all supported cultures and allow Administrator to add any language to a site regardless of translation availability, fix translation upgrade issue
2022-07-16 10:00:20 -04:00
f97a6a2bee Added version to Language Management, improved framework performance by loading languages into PageState, include all supported cultures and allow Administrator to add any language to a site regardless of translation availability, fix translation upgrade issue 2022-07-16 09:59:47 -04:00
1134422891 Merge pull request #2291 from sbwalker/dev
Fix #2282 - dynamically determine framework path when scaffolding project references
2022-07-15 16:00:23 -04:00
6012275c7b Fix #2282 - dynamically determine framework path when scaffolding project references 2022-07-15 15:59:55 -04:00
2f07063375 Merge pull request #2288 from sbwalker/dev
Fix #2285 - handle scenario where the module definition associated to a module instance does not exist
2022-07-14 16:58:43 -04:00
310d1ed485 Fix #2285 - handle scenario where the module definition associated to a module instance does not exist 2022-07-14 16:58:16 -04:00
a48edbb16e Merge pull request #2287 from sbwalker/dev
fixed issue in default site template where MIT License module was being created in invalid pane
2022-07-14 09:11:16 -04:00
d6258409fc fixed issue in default site template where MIT License module was being created in invalid pane 2022-07-14 09:10:51 -04:00
1b94b1247b Merge pull request #2284 from sbwalker/dev
Fix #2280 - add 404 page on upgrade, Fix #2279 add message indicating a restart is required to activate scheduled jobs after installation, add Package Name to Module and Theme management
2022-07-13 15:19:07 -04:00
9ef63ae60e Fix #2280 - add 404 page on upgrade, Fix #2279 add message indicating a restart is required to activate scheduled jobs after installation, add Package Name to Module and Theme management 2022-07-13 15:18:41 -04:00
f99de4be48 Merge pull request #2272 from sbwalker/dev
Support for module editors by exposing Edit Mode in the Control Panel
2022-07-06 17:25:43 -04:00
80fd1820c2 Support for module editors by exposing Edit Mode in the Control Panel 2022-07-06 17:25:08 -04:00
0a4a983d20 Merge pull request #2269 from leigh-pointer/Resx
Added Missing Resource from Oqtane Theme Settings
2022-07-06 08:37:25 -04:00
c2cc830691 Added Missing Resource from Oqtane Theme Settings
Missing Resource string added
2022-07-04 21:37:42 +02:00
4d0490d1c6 Merge pull request #2260 from sbwalker/dev
FIx issue with redirect after site delete and remove tenant if it is empty
2022-06-28 08:17:39 -04:00
02d1838547 FIx issue with redirect after site delete and remove tenant if it is empty 2022-06-28 08:17:06 -04:00
5c6edff778 Update README.md 2022-06-27 16:35:50 -04:00
7cbca32ddd Update README.md 2022-06-27 16:16:58 -04:00
326 changed files with 8384 additions and 3466 deletions

2
.gitignore vendored
View File

@ -10,6 +10,8 @@ msbuild.binlog
*.zip *.zip
*.idea *.idea
_ReSharper.Caches
.DS_Store
Oqtane.Server/appsettings.json Oqtane.Server/appsettings.json
Oqtane.Server/Data Oqtane.Server/Data

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2022 .NET Foundation Copyright (c) 2018-2023 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) public static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services)
{ {
services.AddScoped<SiteState>(); services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>(); services.AddScoped<IInstallationService, InstallationService>();

View File

@ -4,7 +4,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label> <Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) " ResourceKey="Server">Server:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" /> <input id="server" type="text" class="form-control" @bind="@_server" />
</div> </div>
@ -51,7 +51,7 @@
@if (_encryption == "true") @if (_encryption == "true")
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption" ResourceKey="TrustServerCertificate">Certificate:</Label> <Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True." ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate"> <select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option> <option value="true">@Localizer["Self Signed"]</option>

View File

@ -26,22 +26,42 @@
<div class="col-sm-9"> <div class="col-sm-9">
@if (_databases != null) @if (_databases != null)
{ {
<select id="databasetype" class="form-select custom-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))"> <div class="input-group">
@foreach (var database in _databases) <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> {
} <option value="@database.Name">@Localizer[@database.Name]</option>
</select> }
</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>
</div> </div>
@{ @if (!_showConnectionString)
if (_databaseConfigType != null) {
{ if (_databaseConfigType != null)
@DatabaseConfigComponent; {
} @DatabaseConfigComponent
} }
</div> }
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">String:</Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div>
</div>
}
</div>
</div> </div>
<div class="col text-center"> <div class="col text-center">
<h2>@Localizer["ApplicationAdmin"]</h2><br /> <h2>@Localizer["ApplicationAdmin"]</h2><br />
@ -100,13 +120,15 @@
private Type _databaseConfigType; private Type _databaseConfigType;
private object _databaseConfig; private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; } private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
private string _hostUsername = string.Empty; private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty; private string _hostPassword = string.Empty;
private string _passwordType = "password"; private string _passwordType = "password";
private string _confirmPasswordType = "password"; private string _confirmPasswordType = "password";
private string _togglePassword = string.Empty; private string _togglePassword = string.Empty;
private string _toggleConfirmPassword = string.Empty; private string _toggleConfirmPassword = string.Empty;
private string _confirmPassword = string.Empty; private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty; private string _hostEmail = string.Empty;
private bool _register = true; private bool _register = true;
@ -116,7 +138,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglePassword = SharedLocalizer["ShowPassword"]; _togglePassword = SharedLocalizer["ShowPassword"];
_toggleConfirmPassword = SharedLocalizer["ShowPassword"]; _toggleConfirmPassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault)) if (_databases.Exists(item => item.IsDefault))
@ -135,7 +157,7 @@
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
@ -164,17 +186,24 @@
if (firstRender) if (firstRender)
{ {
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/css/bootstrap.min.css", "text/css", "sha512-XWTTruHZEYJsxV3W/lSXG1n3Q39YIWOstqvmFsdNEEQfHoZ6vm6E9GK2OrF6DSJSpIbRbi+Nn0WDPID9O7xB2Q==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head"); await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", "anonymous", "", "head");
} }
} }
private async Task Install() private async Task Install()
{ {
var connectionString = String.Empty; var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_showConnectionString)
{ {
connectionString = databaseConfigControl.GetConnectionString(); connectionString = _connectionString;
}
else
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
} }
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
@ -218,12 +247,12 @@
{ {
_message = Localizer["Message.Password.Invalid"]; _message = Localizer["Message.Password.Invalid"];
} }
} }
else else
{ {
_message = Localizer["Message.Require.DbInfo"]; _message = Localizer["Message.Require.DbInfo"];
} }
} }
private void TogglePassword() private void TogglePassword()
{ {
@ -239,7 +268,7 @@
} }
} }
private void ToggleConfirmPassword() private void ToggleConfirmPassword()
{ {
if (_confirmPasswordType == "password") if (_confirmPasswordType == "password")
{ {
@ -252,4 +281,14 @@
_toggleConfirmPassword = SharedLocalizer["ShowPassword"]; _toggleConfirmPassword = SharedLocalizer["ShowPassword"];
} }
} }
private void ToggleConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionString = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
} }

View File

@ -10,8 +10,8 @@
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{ {
string url = NavigateUrl(p.Path); string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center"> <div class="col-md-2 mx-auto text-center mb-3">
<NavLink class="nav-link" href="@url" Match="NavLinkMatch.All"> <NavLink class="nav-link text-primary" href="@url" Match="NavLinkMatch.All">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>@SharedLocalizer[p.Name] <h2><span class="@p.Icon" aria-hidden="true"></span></h2>@SharedLocalizer[p.Name]
</NavLink> </NavLink>
</div> </div>
@ -20,13 +20,16 @@
</div> </div>
@code { @code {
private List<Page> _pages; private List<Page> _pages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnInitialized() protected override void OnInitialized()
{ {
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin"); var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList(); if (admin != null)
{
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList();
}
} }
} }

View File

@ -11,8 +11,7 @@
Module module = await ModuleService.GetModuleAsync(ModuleState.ModuleId); Module module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
string message = string.Format(Localizer["Error.Module.Load"], module.ModuleDefinitionName); AddModuleMessage(string.Format(Localizer["Error.Module.Load"], module.ModuleDefinitionName), MessageType.Error);
AddModuleMessage(message, MessageType.Error);
} }
await logger.LogCritical("Error Loading Module {Module}", module); await logger.LogCritical("Error Loading Module {Module}", module);

View File

@ -132,22 +132,10 @@
_isEnabled = job.IsEnabled.ToString(); _isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString(); _interval = job.Interval.ToString();
_frequency = job.Frequency; _frequency = job.Frequency;
_startDate = job.StartDate; (_startDate, _startTime) = Utilities.UtcAsLocalDateAndTime(job.StartDate);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0) (_endDate, _endTime) = Utilities.UtcAsLocalDateAndTime(job.EndDate);
{
_startTime = job.StartDate.Value.ToString("HH:mm");
}
_endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{
_endTime = job.EndDate.Value.ToString("HH:mm");
}
_retentionHistory = job.RetentionHistory.ToString(); _retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution; (_nextDate, _nextTime) = Utilities.UtcAsLocalDateAndTime(job.NextExecution);
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{
_nextTime = job.NextExecution.Value.ToString("HH:mm");
}
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
@ -180,50 +168,27 @@
{ {
job.Interval = int.Parse(_interval); job.Interval = int.Parse(_interval);
} }
job.StartDate = _startDate; job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime);
if (job.StartDate != null) job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime);
{ job.RetentionHistory = int.Parse(_retentionHistory);
job.StartDate = job.StartDate.Value.Date; job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
if (!string.IsNullOrEmpty(_startTime))
{ try
job.StartDate = DateTime.Parse(job.StartDate.Value.ToShortDateString() + " " + _startTime); {
} job = await JobService.UpdateJobAsync(job);
} await logger.LogInformation("Job Updated {Job}", job);
job.EndDate = _endDate; NavigationManager.NavigateTo(NavigateUrl());
if (job.EndDate != null) }
{ catch (Exception ex)
job.EndDate = job.EndDate.Value.Date; {
if (!string.IsNullOrEmpty(_endTime)) await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
{ AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
job.EndDate = DateTime.Parse(job.EndDate.Value.ToShortDateString() + " " + _endTime); }
} }
} else
job.RetentionHistory = int.Parse(_retentionHistory); {
job.NextExecution = _nextDate; AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
if (job.NextExecution != null) }
{ }
job.NextExecution = job.NextExecution.Value.Date;
if (!string.IsNullOrEmpty(_nextTime))
{
job.NextExecution = DateTime.Parse(job.NextExecution.Value.ToShortDateString() + " " + _nextTime);
}
}
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
} }

View File

@ -33,7 +33,7 @@ else
<td>@context.Name</td> <td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td> <td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td> <td>@DisplayFrequency(context.Interval, context.Frequency)</td>
<td>@context.NextExecution</td> <td>@context.NextExecution?.ToLocalTime()</td>
<td> <td>
@if (context.IsStarted) @if (context.IsStarted)
{ {
@ -56,6 +56,10 @@ else
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_jobs = await JobService.GetJobsAsync(); _jobs = await JobService.GetJobsAsync();
if (_jobs.Count == 0)
{
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
}
} }
private string DisplayStatus(bool isEnabled, bool isExecuting) private string DisplayStatus(bool isEnabled, bool isExecuting)

View File

@ -9,7 +9,7 @@
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_supportedCultures == null) @if (_cultures == null)
{ {
<p><em>@SharedLocalizer["Loading"]</em></p> <p><em>@SharedLocalizer["Loading"]</em></p>
} }
@ -17,321 +17,132 @@ else
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Manage" ResourceKey="Manage"> <TabPanel Name="Manage" ResourceKey="Manage">
@if (_availableCultures.Count() == 0) <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
{ <div class="container">
<ModuleMessage Type="MessageType.Info" Message="@_message"></ModuleMessage> <div class="row mb-1 align-items-center">
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <Label Class="col-sm-3" For="translated" HelpText="Specify If You Wish To Select Languages That Have Translations Installed" ResourceKey="Translated">Translated?</Label>
} <div class="col-sm-9">
else <select id="translated" class="form-select" value="@_translated" @onchange="(e => TranslatedChanged(e))" required>
{ <option value="True">@SharedLocalizer["Yes"]</option>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <option value="False">@SharedLocalizer["No"]</option>
<div class="container"> </select>
<div class="row mb-1 align-items-center"> </div>
<Label Class="col-sm-3" For="name" HelpText="Name Of The Language" ResourceKey="Name">Name:</Label> </div>
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<select id="_code" class="form-select" @bind="@_code" required> <Label Class="col-sm-3" For="name" HelpText="Name Of The Language" ResourceKey="Name">Name:</Label>
@foreach (var culture in _availableCultures) <div class="col-sm-9">
{ <select id="_code" class="form-select" @bind="@_code" required>
<option value="@culture.Name">@culture.DisplayName</option> <option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
} @foreach (var culture in _cultures)
</select> {
</div> <option value="@culture.Name">@(culture.DisplayName + " (" + culture.Name + ")")</option>
</div> }
<div class="row mb-1 align-items-center"> </select>
<Label Class="col-sm-3" For="default" HelpText="Indicates Whether Or Not This Language Is The Default For The Site" ResourceKey="IsDefault">Default?</Label>
<div class="col-sm-9">
<select id="default" class="form-select" @bind="@_isDefault" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button> <div class="row mb-1 align-items-center">
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <Label Class="col-sm-3" For="default" HelpText="Indicates Whether Or Not This Language Is The Default For The Site" ResourceKey="IsDefault">Default?</Label>
</form> <div class="col-sm-9">
} <select id="default" class="form-select" @bind="@_default" required>
</TabPanel> <option value="True">@SharedLocalizer["Yes"]</option>
<TabPanel Name="Download" ResourceKey="Download" Security="SecurityAccessLevel.Host"> <option value="False">@SharedLocalizer["No"]</option>
<div class="row justify-content-center mb-3"> </select>
<div class="col-sm-6"> </div>
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div> </div>
</div> </div>
</div> <button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (_packages != null) </form>
{
@if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Row>
<td>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;by:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
<button type="button" class="btn btn-success" @onclick="InstallLanguages">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
}
</TabPanel> </TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host"> <TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more translations. Once they are uploaded click Install to complete the installation." ResourceKey="LanguageUpload">Language: </Label> <Label Class="col-sm-3" HelpText="Upload one or more translations. Once they are uploaded click Install to complete the installation." ResourceKey="LanguageUpload">Translation: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div> </div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-success" @onclick="InstallLanguages">@SharedLocalizer["Install"]</button> <button type="button" class="btn btn-success" @onclick="InstallTranslations">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
} }
@if (_productname != "")
{
<div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license))
{
@((MarkupString)_license)
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _code = string.Empty; private string _translated = "True";
private string _isDefault = "False"; private string _code = "-";
private string _message; private string _default = "False";
private IEnumerable<Culture> _supportedCultures; private List<string> _languages;
private IEnumerable<Culture> _availableCultures; private IEnumerable<Culture> _cultures;
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _license = "";
private string _packageid = "";
private string _version = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
var languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); var languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
var languagesCodes = languages.Select(l => l.Code).ToList(); _languages = languages.Select(l => l.Code).ToList();
_supportedCultures = await LocalizationService.GetCulturesAsync(); await LoadCultures();
_availableCultures = _supportedCultures }
.Where(c => !c.Name.Equals(Constants.DefaultCulture) && !languagesCodes.Contains(c.Name));
await LoadTranslations();
if (_supportedCultures.Count() == 1) private async Task LoadCultures()
{ {
_message = Localizer["OnlyEnglish"]; _cultures = await LocalizationService.GetCulturesAsync(bool.Parse(_translated));
} _cultures = _cultures.Where(c => !c.Name.Equals(Constants.DefaultCulture) && !_languages.Contains(c.Name));
else if (_availableCultures.Count() == 0) _code = "-";
{ }
_message = Localizer["AllLanguages"];
}
}
private async Task LoadTranslations() private async void TranslatedChanged(ChangeEventArgs e)
{ {
_packages = await PackageService.GetPackagesAsync("translation", _search, _price, ""); _translated = (string)e.Value;
} await LoadCultures();
StateHasChanged();
}
private async void PriceChanged(ChangeEventArgs e) private async Task SaveLanguage()
{ {
try validated = true;
{ var interop = new Interop(JSRuntime);
_price = (string)e.Value; if (await interop.FormValid(form) && _code != "-")
_search = ""; {
await LoadTranslations(); var language = new Language
StateHasChanged(); {
} SiteId = PageState.Page.SiteId,
catch (Exception ex) Name = CultureInfo.GetCultureInfo(_code).DisplayName,
{ Code = _code,
await logger.LogError(ex, "Error On PriceChanged"); IsDefault = (_default == null ? false : Boolean.Parse(_default))
} };
}
private async Task Search() try
{ {
try language = await LanguageService.AddLanguageAsync(language);
{
await LoadTranslations();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Search");
}
}
private async Task Reset() if (language.IsDefault)
{ {
try await SetCultureAsync(language.Code);
{ }
_search = "";
await LoadTranslations();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Reset");
}
}
private async Task SaveLanguage() await logger.LogInformation("Language Added {Language}", language);
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var language = new Language
{
SiteId = PageState.Page.SiteId,
Name = CultureInfo.GetCultureInfo(_code).DisplayName,
Code = _code,
IsDefault = (_isDefault == null ? false : Boolean.Parse(_isDefault))
};
try NavigationManager.NavigateTo(NavigateUrl());
{ }
language = await LanguageService.AddLanguageAsync(language); catch (Exception ex)
{
if (language.IsDefault) await logger.LogError(ex, "Error Adding Language {Language} {Error}", language, ex.Message);
{ AddModuleMessage(Localizer["Error.Language.Add"], MessageType.Error);
await SetCultureAsync(language.Code); }
} }
await logger.LogInformation("Language Added {Language}", language);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error.Language.Add"], MessageType.Error);
}
}
else else
{ {
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
} }
} }
private async Task InstallTranslations()
private void HideModal()
{
_productname = "";
_license = "";
StateHasChanged();
}
private async Task GetPackage(string packageid, string version)
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null)
{
_productname = package.Name;
if (!string.IsNullOrEmpty(package.License))
{
_license = package.License.Replace("\n", "<br />");
}
_packageid = package.PackageId;
_version = package.Version;
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Language Package {Name} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Language.Download"], MessageType.Success);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Translation {Name} {Version}", _packageid, _version);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
}
}
private async Task InstallLanguages()
{ {
try try
{ {

View File

@ -20,93 +20,197 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th> <th>@Localizer["Code"]</th>
<th>@Localizer["Default"]</th> <th>@Localizer["Default"]</th>
<th style="width: 1px;">&nbsp;</th> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<th style="width: 1px;">@Localizer["Translation"]</th>
<th style="width: 1px;">&nbsp;</th>
}
</Header> </Header>
<Row> <Row>
<td><ActionDialog Header="Delete Language" Message="@string.Format(Localizer["Confirm.Language.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteLanguage(context))" Disabled="@((context.IsDefault && _languages.Count > 2) || context.Code == Constants.DefaultCulture)" ResourceKey="DeleteLanguage" /></td> <td><ActionDialog Header="Delete Language" Message="@string.Format(Localizer["Confirm.Language.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteLanguage(context))" Disabled="@((context.IsDefault && _languages.Count > 2) || context.Code == Constants.DefaultCulture)" ResourceKey="DeleteLanguage" /></td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Code</td> <td>@context.Code</td>
<td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td> <td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td>
<td> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@if (UpgradeAvailable(context.Code)) {
{ <td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadLanguage(context.Code))>@SharedLocalizer["Upgrade"]</button> <td>
} @switch (TranslationAvailable(context.Code, context.Version))
</td> {
case "install":
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(context.Code))>@SharedLocalizer["Download"]</button>
break;
case "upgrade":
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(context.Code))>@SharedLocalizer["Upgrade"]</button>
break;
}
</td>
}
</Row> </Row>
</Pager> </Pager>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _install)
{
<button type="button" class="btn btn-success" @onclick="InstallTranslations">@SharedLocalizer["Install"]</button>
}
}
@if (_package != null)
{
<div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h4 style="display: inline;"><a href="@_package.ProductUrl" target="_new">@_package.Name</a></h4><br />
@SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@_package.OwnerUrl" target="new">@_package.Owner</a></strong><br />
@(_package.Description.Length > 400 ? (_package.Description.Substring(0, 400) + "...") : _package.Description)<br />
<strong>@(String.Format("{0:n0}", _package.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@_package.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@_package.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(_package.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(_package.PackageUrl).Host + "</strong>" : ""))
<br /><br />
@if (!string.IsNullOrEmpty(_package.License))
{
@((MarkupString)_package.License.Replace("\n", "<br />"))
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
} }
@code { @code {
private List<Language> _languages; private List<Language> _languages;
private List<Package> _packages; private List<Package> _packages;
private Package _package;
private bool _install = false;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); await GetLanguages();
var cultures = await LocalizationService.GetCulturesAsync(); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
var culture = cultures.First(c => c.Name.Equals(Constants.DefaultCulture)); {
_packages = await PackageService.GetPackagesAsync("translation");
}
}
// Adds English as default language private async Task GetLanguages()
_languages.Insert(0, new Language { Name = culture.DisplayName, Code = culture.Name, IsDefault = !_languages.Any(l => l.IsDefault) }); {
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId);
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) private async Task DeleteLanguage(Language language)
{ {
_packages = await PackageService.GetPackagesAsync("translation"); try
} {
} await LanguageService.DeleteLanguageAsync(language.LanguageId);
await logger.LogInformation("Language Deleted {Language}", language);
await GetLanguages();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Language {Language} {Error}", language, ex.Message);
private async Task DeleteLanguage(Language language) AddModuleMessage(Localizer["Error.Language.Delete"], MessageType.Error);
{ }
try }
{
await LanguageService.DeleteLanguageAsync(language.LanguageId);
await logger.LogInformation("Language Deleted {Language}", language);
StateHasChanged(); private string TranslationAvailable(string code, string version)
} {
catch (Exception ex) if (_packages != null)
{ {
await logger.LogError(ex, "Error Deleting Language {Language} {Error}", language, ex.Message); var package = _packages.Where(item => item.PackageId == (Constants.PackageId + "." + code)).FirstOrDefault();
if (package != null)
{
// package version needs to match current framework version
if (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0)
{
if (string.IsNullOrEmpty(version))
{
return "install";
}
else
{
if (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return "upgrade";
}
}
}
}
}
return "";
}
AddModuleMessage(Localizer["Error.Language.Delete"], MessageType.Error); private async Task GetPackage(string code)
} {
} try
{
_package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, Constants.Version);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", Constants.PackageId + "." + code, Constants.Version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private bool UpgradeAvailable(string code) private async Task DownloadPackage()
{ {
var upgradeavailable = false; try
if (_packages != null) {
{ await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
var package = _packages.Where(item => item.PackageId == (Constants.PackageId + ".Client." + code)).FirstOrDefault(); await logger.LogInformation("Language Package {Name} {Version} Downloaded Successfully", _package.PackageId, _package.Version);
if (package != null) AddModuleMessage(Localizer["Success.Language.Download"], MessageType.Success);
{ _package = null;
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0); _install = true;
} StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Translation {Name} {Version}", _package.PackageId, _package.Version);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
}
}
} private void HideModal()
return upgradeavailable; {
} _package = null;
StateHasChanged();
}
private async Task DownloadLanguage(string code) private async Task InstallTranslations()
{ {
try try
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) await PackageService.InstallPackagesAsync();
{ AddModuleMessage(string.Format(Localizer["Success.Translation.Install"], NavigateUrl("admin/system")), MessageType.Success);
await PackageService.DownloadPackageAsync(Constants.PackageId + ".Client." + code, Constants.Version, Constants.PackagesFolder); _install = false;
await logger.LogInformation("Translation Downloaded {Code} {Version}", code, Constants.Version); StateHasChanged();
await PackageService.InstallPackagesAsync(); }
AddModuleMessage(string.Format(Localizer["Success.Language.Install"], NavigateUrl("admin/system")), MessageType.Success); catch (Exception ex)
} {
} await logger.LogError(ex, "Error Installing Translations");
catch (Exception ex) }
{ }
await logger.LogError(ex, "Error Downloading Translation {Code} {Version} {Error}", code, Constants.Version, ex.Message);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
}
}
} }

View File

@ -3,7 +3,6 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUserService UserService @inject IUserService UserService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject SiteState SiteState
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -184,11 +183,12 @@
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(login)) if (await interop.FormValid(login))
{ {
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress}; var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (!twofactor) if (!twofactor)
{ {
user = await UserService.LoginUserAsync(user); user = await UserService.LoginUserAsync(user, hybrid, _remember);
} }
else else
{ {
@ -199,10 +199,21 @@
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
// post back to the Login page so that the cookies are set correctly if (hybrid)
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; {
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); // hybrid apps utilize an interactive login
await interop.SubmitForm(url, fields); var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
} }
else else
{ {

View File

@ -130,13 +130,14 @@
private string _properties = string.Empty; private string _properties = string.Empty;
private string _server = string.Empty; private string _server = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override string UrlParametersTemplate => "/{id}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_logId = Int32.Parse(PageState.QueryString["id"]); _logId = Int32.Parse(UrlParameters["id"]);
var log = await LogService.GetLogAsync(_logId); var log = await LogService.GetLogAsync(_logId);
if (log != null) if (log != null)
{ {
@ -191,13 +192,6 @@
private string CloseUrl() private string CloseUrl()
{ {
if (!PageState.QueryString.ContainsKey("level")) return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl();
{
return NavigateUrl();
}
else
{
return NavigateUrl(PageState.Page.Path, "level=" + PageState.QueryString["level"] + "&function=" + PageState.QueryString["function"] + "&rows=" + PageState.QueryString["rows"] + "&page=" + PageState.QueryString["page"]);
}
} }
} }

View File

@ -63,7 +63,7 @@ else
<th>@Localizer["Function"]</th> <th>@Localizer["Function"]</th>
</Header> </Header>
<Row> <Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"id=" + context.LogId.ToString() + "&level=" + _level + "&function=" + _function + "&rows=" + _rows + "&page=" + _page.ToString())" ResourceKey="LogDetails" /></td> <td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td> <td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@context.Level</td> <td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td> <td class="@GetClass(context.Function)">@context.Feature</td>
@ -99,29 +99,32 @@ else
private List<Log> _logs; private List<Log> _logs;
private string _retention = ""; private string _retention = "";
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
// external link to log item will display Details component
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int id)) if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int id))
{ {
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, ModuleState.ModuleId, "Detail", $"id={id}")); NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, ModuleState.ModuleId, "Detail", $"/{id}"));
} }
if (PageState.QueryString.ContainsKey("level"))
if (UrlParameters.ContainsKey("level"))
{ {
_level = PageState.QueryString["level"]; _level = UrlParameters["level"];
} }
if (PageState.QueryString.ContainsKey("function")) if (UrlParameters.ContainsKey("function"))
{ {
_function = PageState.QueryString["function"]; _function = UrlParameters["function"];
} }
if (PageState.QueryString.ContainsKey("rows")) if (UrlParameters.ContainsKey("rows"))
{ {
_rows = PageState.QueryString["rows"]; _rows = UrlParameters["rows"];
} }
if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page)) if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page))
{ {
_page = page; _page = page;
} }

View File

@ -85,7 +85,7 @@ else
private List<Template> _templates; private List<Template> _templates;
private string _template = "-"; private string _template = "-";
private string[] _versions; private string[] _versions;
private string _reference = Constants.Version; private string _reference = "local";
private string _minversion = "2.0.0"; private string _minversion = "2.0.0";
private string _location = string.Empty; private string _location = string.Empty;
@ -131,7 +131,7 @@ else
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition); moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName); settings = SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation(); GetLocation();

View File

@ -91,9 +91,9 @@
<div class="modal-body"> <div class="modal-body">
<p style="height: 200px; overflow-y: scroll;"> <p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3> <h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license)) @if (!string.IsNullOrEmpty(_packagelicense))
{ {
@((MarkupString)_license) @((MarkupString)_packagelicense)
} }
else else
{ {
@ -114,14 +114,18 @@
<button type="button" class="btn btn-success" @onclick="InstallModules">@SharedLocalizer["Install"]</button> <button type="button" class="btn btn-success" @onclick="InstallModules">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code { @code {
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
private string _search = ""; private string _search = "";
private string _productname = ""; private string _productname = "";
private string _license = "";
private string _packageid = ""; private string _packageid = "";
private string _version = ""; private string _packagelicense = "";
private string _packageversion = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -198,7 +202,7 @@
private void HideModal() private void HideModal()
{ {
_productname = ""; _productname = "";
_license = ""; _packagelicense = "";
StateHasChanged(); StateHasChanged();
} }
@ -210,12 +214,12 @@
if (package != null) if (package != null)
{ {
_productname = package.Name; _productname = package.Name;
_packageid = package.PackageId;
if (!string.IsNullOrEmpty(package.License)) if (!string.IsNullOrEmpty(package.License))
{ {
_license = package.License.Replace("\n", "<br />"); _packagelicense = package.License.Replace("\n", "<br />");
} }
_packageid = package.PackageId; _packageversion = package.Version;
_version = package.Version;
} }
StateHasChanged(); StateHasChanged();
} }
@ -230,16 +234,16 @@
{ {
try try
{ {
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(_packageid, _packageversion, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version); await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _packageversion);
AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success); AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success);
_productname = ""; _productname = "";
_license = ""; _packagelicense = "";
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version); await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _packageversion);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
} }
} }
@ -253,7 +257,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Installing Module"); await logger.LogError(ex, "Error Installing Modules");
} }
} }
} }

View File

@ -151,7 +151,7 @@
var template = _templates.FirstOrDefault(item => item.Name == _template); var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version; _minversion = template.Version;
} }
GetLocation(); GetLocation();
} }
private void GetLocation() private void GetLocation()

View File

@ -1,6 +1,10 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions @namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase @inherits ModuleBase
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject IModuleDefinitionService ModuleDefinitionService @inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService
@inject ILanguageService LanguageService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -43,7 +47,13 @@
<input id="version" class="form-control" @bind="@_version" disabled /> <input id="version" class="form-control" @bind="@_version" disabled />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" 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 module" ResourceKey="Owner">Owner: </Label> <Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the module" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled /> <input id="owner" class="form-control" @bind="@_owner" disabled />
@ -74,7 +84,13 @@
</div> </div>
</div> </div>
</div> </div>
</Section> </Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@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>
<TabPanel Name="Permissions" ResourceKey="Permissions"> <TabPanel Name="Permissions" ResourceKey="Permissions">
<div class="container"> <div class="container">
@ -82,107 +98,291 @@
<PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel> </TabPanel>
<TabPanel Name="Translations" ResourceKey="Translations">
@if (_languages != null && _languages.Count > 0)
{
<Pager Items="@_languages">
<Header>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
<th style="width: 1px;">@Localizer["Version"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
<td>@context.Name</td>
<td>@context.Code</td>
<td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<td>
@switch (TranslationAvailable(_packagename + "." + context.Code, context.Version))
{
case "install":
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Download"]</button>
break;
case "upgrade":
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Upgrade"]</button>
break;
}
</td>
</Row>
</Pager>
@if (_install)
{
<button type="button" class="btn btn-success" @onclick="InstallTranslations">@SharedLocalizer["Install"]</button>
}
}
else
{
<br />
<div class="mx-auto text-center">
@if (string.IsNullOrEmpty(_packagename))
{
@Localizer["Search.PackageNameMissing"]
}
else
{
@Localizer["Search.NoResults"]
}
</div>
<br />
}
</TabPanel>
</TabStrip> </TabStrip>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> @if (_package != null)
<br /> {
<br /> <div class="app-actiondialog">
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo> <div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h4 style="display: inline;"><a href="@_package.ProductUrl" target="_new">@_package.Name</a></h4><br />
@SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@_package.OwnerUrl" target="new">@_package.Owner</a></strong><br />
@(_package.Description.Length > 400 ? (_package.Description.Substring(0, 400) + "...") : _package.Description)<br />
<strong>@(String.Format("{0:n0}", _package.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@_package.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@_package.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(_package.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(_package.PackageUrl).Host + "</strong>" : ""))
<br /><br />
@if (!string.IsNullOrEmpty(_package.License))
{
@((MarkupString)_package.License.Replace("\n", "<br />"))
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private int _moduleDefinitionId; private int _moduleDefinitionId;
private string _name; private string _name;
private string _version; private string _description = "";
private string _categories; private string _categories;
private string _moduledefinitionname = ""; private string _moduledefinitionname = "";
private string _description = ""; private string _version;
private string _owner = ""; private string _packagename = "";
private string _url = ""; private string _owner = "";
private string _contact = ""; private string _url = "";
private string _license = ""; private string _contact = "";
private string _runtimes = ""; private string _license = "";
private string _permissions; private string _runtimes = "";
private string _createdby; private string _permissions;
private DateTime _createdon; private string _createdby;
private string _modifiedby; private DateTime _createdon;
private DateTime _modifiedon; private string _modifiedby;
private DateTime _modifiedon;
#pragma warning disable 649 #pragma warning disable 649
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
#pragma warning restore 649 #pragma warning restore 649
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; private List<Package> _packages;
private List<Language> _languages;
private Package _package;
private bool _install = false;
protected override async Task OnInitializedAsync() public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
{
try
{
_moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]);
var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduleDefinition != null)
{
_name = moduleDefinition.Name;
_version = moduleDefinition.Version;
_categories = moduleDefinition.Categories;
_moduledefinitionname = moduleDefinition.ModuleDefinitionName;
_description = moduleDefinition.Description;
_owner = moduleDefinition.Owner;
_url = moduleDefinition.Url;
_contact = moduleDefinition.Contact;
_license = moduleDefinition.License;
_runtimes = moduleDefinition.Runtimes;
_permissions = moduleDefinition.Permissions;
_createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn;
_modifiedby = moduleDefinition.ModifiedBy;
_modifiedon = moduleDefinition.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
private async Task SaveModuleDefinition() protected override async Task OnInitializedAsync()
{ {
validated = true; try
var interop = new Interop(JSRuntime); {
if (await interop.FormValid(form)) _moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]);
{ var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
try if (moduleDefinition != null)
{ {
var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); _name = moduleDefinition.Name;
if (moduledefinition.Name != _name) _description = moduleDefinition.Description;
{ _categories = moduleDefinition.Categories;
moduledefinition.Name = _name; _moduledefinitionname = moduleDefinition.ModuleDefinitionName;
} _version = moduleDefinition.Version;
if (moduledefinition.Description != _description) _packagename = moduleDefinition.PackageName;
{ _owner = moduleDefinition.Owner;
moduledefinition.Description = _description; _url = moduleDefinition.Url;
} _contact = moduleDefinition.Contact;
if (moduledefinition.Categories != _categories) _license = moduleDefinition.License;
{ _runtimes = moduleDefinition.Runtimes;
moduledefinition.Categories = _categories; _permissions = moduleDefinition.Permissions;
} _createdby = moduleDefinition.CreatedBy;
moduledefinition.Permissions = _permissionGrid.GetPermissions(); _createdon = moduleDefinition.CreatedOn;
await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition); _modifiedby = moduleDefinition.ModifiedBy;
await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition); _modifiedon = moduleDefinition.ModifiedOn;
NavigationManager.NavigateTo(NavigateUrl());
} if (!string.IsNullOrEmpty(_packagename))
catch (Exception ex) {
{ _packages = await PackageService.GetPackagesAsync("translation", "", "", _packagename);
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message); _languages = await LanguageService.GetLanguagesAsync(-1, _packagename);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error); foreach (var package in _packages)
} {
} var code = package.PackageId.Split('.').Last();
else if (!_languages.Any(item => item.Code == code))
{ {
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); _languages.Add(new Language { Code = code, Name = CultureInfo.GetCultureInfo(code).DisplayName, Version = package.Version, IsDefault = true });
} }
} }
_languages = _languages.OrderBy(item => item.Name).ToList();
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
private async Task SaveModuleDefinition()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduledefinition.Name != _name)
{
moduledefinition.Name = _name;
}
if (moduledefinition.Description != _description)
{
moduledefinition.Description = _description;
}
if (moduledefinition.Categories != _categories)
{
moduledefinition.Categories = _categories;
}
moduledefinition.Permissions = _permissionGrid.GetPermissions();
await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition);
await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void HideModal()
{
_package = null;
StateHasChanged();
}
private string TranslationAvailable(string packagename, string version)
{
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (string.IsNullOrEmpty(version))
{
return "install";
}
else
{
if (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return "upgrade";
}
}
}
}
return "";
}
private async Task GetPackage(string packagename)
{
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try
{
_package = await PackageService.GetPackageAsync(packagename, version);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packagename, version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _package.PackageId, _package.Version);
AddModuleMessage(Localizer["Success.Translation.Download"], MessageType.Success);
_package = null;
_install = true;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task InstallTranslations()
{
try
{
await PackageService.InstallPackagesAsync();
AddModuleMessage(string.Format(Localizer["Success.Translation.Install"], NavigateUrl("admin/system")), MessageType.Success);
_install = false;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Translations");
}
}
} }

View File

@ -50,7 +50,7 @@ else
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td>
<td> <td>
@if (context.AssemblyName != "Oqtane.Client") @if (context.AssemblyName != Constants.ClientId)
{ {
<ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete", context.Name])" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /> <ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete", context.Name])" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" />
} }
@ -58,7 +58,7 @@ else
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Version</td> <td>@context.Version</td>
<td> <td>
@if(context.AssemblyName == "Oqtane.Client" || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null) @if (context.AssemblyName == Constants.ClientId || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{ {
<span>@SharedLocalizer["Yes"]</span> <span>@SharedLocalizer["Yes"]</span>
} }

View File

@ -19,7 +19,7 @@
@code { @code {
private string _content = string.Empty; private string _content = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content"; public override string Title => "Export Content";
@ -27,7 +27,7 @@
{ {
try try
{ {
_content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId); _content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId);
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -25,7 +25,7 @@
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content"; public override string Title => "Import Content";
private async Task ImportModule() private async Task ImportModule()
@ -38,7 +38,7 @@
{ {
try try
{ {
bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, _content); bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, _content);
if (success) if (success)
{ {
AddModuleMessage(Localizer["Success.Content.Import"], MessageType.Success); AddModuleMessage(Localizer["Success.Content.Import"], MessageType.Success);

View File

@ -124,37 +124,45 @@
_containerType = ModuleState.ContainerType; _containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString(); _allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions; _permissions = ModuleState.Permissions;
_permissionNames = ModuleState.ModuleDefinition.PermissionNames;
_pageId = ModuleState.PageId.ToString(); _pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy; createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn; createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy; modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn; modifiedon = ModuleState.ModifiedOn;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType)) if (ModuleState.ModuleDefinition != null)
{ {
// module settings type explicitly declared in IModule interface _permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
}
else
{
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl;
if (!string.IsNullOrEmpty(moduleobject.Title))
{
_moduleSettingsTitle = moduleobject.Title;
}
ModuleSettingsComponent = builder =>
{
builder.OpenComponent(0, _moduleSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); });
builder.CloseComponent();
};
}
} }
else else
{ {
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module ) AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error);
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl;
if (!string.IsNullOrEmpty(moduleobject.Title))
{
_moduleSettingsTitle = moduleobject.Title;
}
ModuleSettingsComponent = builder =>
{
builder.OpenComponent(0, _moduleSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); });
builder.CloseComponent();
};
} }
var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));

View File

@ -24,7 +24,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<select id="parent" class="form-select" @onchange="(e => ParentChanged(e))" required> <select id="parent" class="form-select" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option> <option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pageList) @foreach (Page page in PageState.Pages)
{ {
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option> <option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
} }
@ -157,7 +157,8 @@
</TabPanel> </TabPanel>
} }
</TabStrip> </TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form> </form>
@ -167,7 +168,6 @@
private List<Theme> _themeList; private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pageList;
private string _name; private string _name;
private string _title; private string _title;
private string _meta; private string _meta;
@ -201,7 +201,6 @@
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_permissions = string.Empty; _permissions = string.Empty;
ThemeSettings(); ThemeSettings();
@ -307,6 +306,10 @@
} }
if (_path.Contains("/")) if (_path.Contains("/"))
{ {
if (_path.EndsWith("/") && _path != "/")
{
_path = _path.Substring(0, _path.Length - 1);
}
_path = _path.Substring(_path.LastIndexOf("/") + 1); _path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
@ -329,15 +332,16 @@
} }
} }
if(PagePathIsDeleted(page.Path, page.SiteId, _pageList)) var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == page.Path))
{ {
AddModuleMessage(string.Format(Localizer["Message.Page.Deleted"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
return; return;
} }
if (!PagePathIsUnique(page.Path, page.SiteId, _pageList)) if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower()))
{ {
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning);
return; return;
} }
@ -421,14 +425,4 @@
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
} }
private static bool PagePathIsUnique(string pagePath, int siteId, List<Page> existingPages)
{
return !existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath);
}
private static bool PagePathIsDeleted(string pagePath, int siteId, List<Page> existingPages)
{
return existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath && page.IsDeleted == true);
}
} }

View File

@ -25,7 +25,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required> <select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option> <option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pageList) @foreach (Page page in PageState.Pages)
{ {
if (page.PageId != _pageId) if (page.PageId != _pageId)
{ {
@ -148,7 +148,8 @@
</div> </div>
</div> </div>
</Section> </Section>
<br /><br /> <br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
} }
</TabPanel> </TabPanel>
@ -189,7 +190,8 @@
<br /> <br />
} }
</TabStrip> </TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form> </form>
@ -201,7 +203,6 @@
private List<Theme> _themeList; private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pageList;
private List<Module> _pageModules; private List<Module> _pageModules;
private int _pageId; private int _pageId;
private string _name; private string _name;
@ -238,7 +239,6 @@
{ {
try try
{ {
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
@ -435,6 +435,10 @@
} }
if (_path.Contains("/")) if (_path.Contains("/"))
{ {
if (_path.EndsWith("/") && _path != "/")
{
_path = _path.Substring(0, _path.Length - 1);
}
_path = _path.Substring(_path.LastIndexOf("/") + 1); _path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
@ -457,12 +461,19 @@
} }
} }
if (!PagePathIsUnique(page.Path, page.SiteId, page.PageId, _pageList)) var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == page.Path && item.PageId != page.PageId))
{ {
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
return; return;
} }
if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower()))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning);
return;
}
if (_insert != "=") if (_insert != "=")
{ {
Page child; Page child;
@ -567,9 +578,4 @@
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
} }
private static bool PagePathIsUnique(string pagePath, int siteId, int pageId, List<Page> existingPages)
{
return !existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath && page.PageId != pageId);
}
} }

View File

@ -104,7 +104,7 @@
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit"; public override string Actions => "Add,Edit";

View File

@ -10,7 +10,7 @@
} }
else else
{ {
<ActionLink Action="Add" Security="SecurityAccessLevel.Admin" Text="Add Profile" ResourceKey="AddProfile" /> <ActionLink Action="Add" Text="Add Profile" Security="SecurityAccessLevel.Edit" ResourceKey="AddProfile" />
<Pager Items="@_profiles"> <Pager Items="@_profiles">
<Header> <Header>
@ -19,8 +19,8 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" ResourceKey="EditProfile" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditProfile" /></td>
<td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td> <td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td>@context.Name</td> <td>@context.Name</td>
</Row> </Row>
</Pager> </Pager>
@ -29,7 +29,7 @@ else
@code { @code {
private List<Profile> _profiles; private List<Profile> _profiles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {

View File

@ -0,0 +1,20 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Profiles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Profiles",
Description = "Manage Profiles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -7,75 +7,77 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip> @if (_pages == null || _modules == null)
<TabPanel Name="Pages" ResourceKey="Pages"> {
@if (_pages == null) <p><em>@SharedLocalizer["Loading"]</em></p>
{ }
<br /> else
<p>@Localizer["NoPage.Deleted"]</p> {
} <TabStrip>
else <TabPanel Name="Pages" ResourceKey="Pages">
{ @if (!_pages.Where(item => item.IsDeleted).Any())
<Pager Items="@_pages"> {
<Header> <br />
<th style="width: 1px;">&nbsp;</th> <p>@Localizer["NoPage.Deleted"]</p>
<th style="width: 1px;">&nbsp;</th> }
<th>@SharedLocalizer["Name"]</th> else
<th>@Localizer["DeletedBy"]</th> {
<th>@Localizer["DeletedOn"]</th> <Pager Items="@_pages.Where(item => item.IsDeleted)">
</Header> <Header>
<Row> <th style="width: 1px;">&nbsp;</th>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">Restore</button></td> <th style="width: 1px;">&nbsp;</th>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td> <th>@SharedLocalizer["Name"]</th>
<td>@context.Name</td> <th>@Localizer["DeletedBy"]</th>
<td>@context.DeletedBy</td> <th>@Localizer["DeletedOn"]</th>
<td>@context.DeletedOn</td> </Header>
</Row> <Row>
</Pager> <td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">Restore</button></td>
@if (_pages.Any()) <td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
{ <td>@context.Name</td>
<br /><ActionDialog Header="Delete All Pages" Message="Are You Sure You Wish To Permanently Delete All Pages?" Action="Delete All Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" /> <td>@context.DeletedBy</td>
} <td>@context.DeletedOn</td>
} </Row>
</TabPanel> </Pager>
<TabPanel Name="Modules" ResourceKey="Modules"> <br />
@if (_modules == null) <ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
{ }
<br /> </TabPanel>
<p>@Localizer["NoModule.Deleted"]</p> <TabPanel Name="Modules" ResourceKey="Modules">
} @if (!_modules.Where(item => item.IsDeleted).Any())
else {
{ <br />
<Pager Items="@_modules"> <p>@Localizer["NoModule.Deleted"]</p>
<Header> }
<th style="width: 1px;">&nbsp;</th> else
<th style="width: 1px;">&nbsp;</th> {
<th>@Localizer["Page"]</th> <Pager Items="@_modules.Where(item => item.IsDeleted)">
<th>@Localizer["Module"]</th> <Header>
<th>@Localizer["DeletedBy"]</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["DeletedOn"]</th> <th style="width: 1px;">&nbsp;</th>
</Header> <th>@Localizer["Page"]</th>
<Row> <th>@Localizer["Module"]</th>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td> <th>@Localizer["DeletedBy"]</th>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td> <th>@Localizer["DeletedOn"]</th>
<td>@PageState.Pages.Find(item => item.PageId == context.PageId).Name</td> </Header>
<td>@context.Title</td> <Row>
<td>@context.DeletedBy</td> <td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td>@context.DeletedOn</td> <td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
</Row> <td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
</Pager> <td>@context.Title</td>
@if (_modules.Any()) <td>@context.DeletedBy</td>
{ <td>@context.DeletedOn</td>
<br /><ActionDialog Header="Delete All Modules" Message="Are You Sure You Wish To Permanently Delete All Modules?" Action="Delete All Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" /> </Row>
} </Pager>
<br />
} <ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
</TabPanel> }
</TabStrip> </TabPanel>
</TabStrip>
}
@code { @code {
private List<Page> _pages; private List<Page> _pages;
private List<Module> _modules; private List<Module> _modules;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -95,10 +97,7 @@
private async Task Load() private async Task Load()
{ {
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pages = _pages.Where(item => item.IsDeleted).ToList();
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); _modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
_modules = _modules.Where(item => item.IsDeleted).ToList();
} }
private async Task RestorePage(Page page) private async Task RestorePage(Page page)
@ -140,7 +139,8 @@
{ {
try try
{ {
foreach (Page page in _pages) ModuleInstance.ShowProgressIndicator();
foreach (Page page in _pages.Where(item => item.IsDeleted))
{ {
await PageService.DeletePageAsync(page.PageId); await PageService.DeletePageAsync(page.PageId);
await logger.LogInformation("Page Permanently Deleted {Page}", page); await logger.LogInformation("Page Permanently Deleted {Page}", page);
@ -148,6 +148,7 @@
await logger.LogInformation("Pages Permanently Deleted"); await logger.LogInformation("Pages Permanently Deleted");
await Load(); await Load();
ModuleInstance.HideProgressIndicator();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -155,6 +156,7 @@
{ {
await logger.LogError(ex, "Error Permanently Deleting Pages {Error}", ex.Message); await logger.LogError(ex, "Error Permanently Deleting Pages {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
ModuleInstance.HideProgressIndicator();
} }
} }
@ -181,9 +183,8 @@
try try
{ {
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
// check if there are any remaining module instances in the site
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
// check if there are any remaining module instances in the site
if (!_modules.Exists(item => item.ModuleId == module.ModuleId)) if (!_modules.Exists(item => item.ModuleId == module.ModuleId))
{ {
await ModuleService.DeleteModuleAsync(module.ModuleId); await ModuleService.DeleteModuleAsync(module.ModuleId);
@ -204,12 +205,12 @@
{ {
try try
{ {
foreach (Module module in _modules) ModuleInstance.ShowProgressIndicator();
foreach (Module module in _modules.Where(item => item.IsDeleted))
{ {
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
// check if there are any remaining module instances in the site
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
// check if there are any remaining module instances in the site
if (!_modules.Exists(item => item.ModuleId == module.ModuleId)) if (!_modules.Exists(item => item.ModuleId == module.ModuleId))
{ {
await ModuleService.DeleteModuleAsync(module.ModuleId); await ModuleService.DeleteModuleAsync(module.ModuleId);
@ -218,12 +219,14 @@
await logger.LogInformation("Modules Permanently Deleted"); await logger.LogInformation("Modules Permanently Deleted");
await Load(); await Load();
ModuleInstance.HideProgressIndicator();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Permanently Deleting Modules {Error}", ex.Message); await logger.LogError(ex, "Error Permanently Deleting Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Modules.Delete"], MessageType.Error); AddModuleMessage(Localizer["Error.Modules.Delete"], MessageType.Error);
ModuleInstance.HideProgressIndicator();
} }
} }
} }

View File

@ -42,7 +42,7 @@
private string _description = string.Empty; private string _description = string.Empty;
private string _isautoassigned = "False"; private string _isautoassigned = "False";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
private async Task SaveRole() private async Task SaveRole()
{ {

View File

@ -49,7 +49,7 @@
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {

View File

@ -10,7 +10,7 @@
} }
else else
{ {
<ActionLink Action="Add" Text="Add Role" ResourceKey="AddRole" /> <ActionLink Action="Add" Text="Add Role" Security="SecurityAccessLevel.Edit" ResourceKey="AddRole" />
<Pager Items="@_roles"> <Pager Items="@_roles">
<Header> <Header>
@ -20,9 +20,9 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td> <td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" ResourceKey="Users" /></td> <td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Users" /></td>
<td>@context.Name</td> <td>@context.Name</td>
</Row> </Row>
</Pager> </Pager>
@ -31,7 +31,7 @@ else
@code { @code {
private List<Role> _roles; private List<Role> _roles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {

View File

@ -0,0 +1,21 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Roles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Roles",
Description = "Manage Roles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}," +
$"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -23,13 +23,7 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label> <Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="user" class="form-select" @bind="@userid" required> <AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["User.Select"]" @ref="user" />
<option value="-1">&lt;@Localizer["User.Select"]&gt;</option>
@foreach (UserRole userrole in users)
{
<option value="@(userrole.UserId)">@userrole.User.DisplayName</option>
}
</select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -64,7 +58,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || PageState.User.Username == UserNames.Host)" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -75,81 +69,96 @@ else
} }
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private int roleid; private int roleid;
private string name = string.Empty; private string name = string.Empty;
private List<UserRole> users; private AutoComplete user;
private int userid = -1; private DateTime? effectivedate = null;
private DateTime? effectivedate = null; private DateTime? expirydate = null;
private DateTime? expirydate = null; private List<UserRole> userroles;
private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
roleid = Int32.Parse(PageState.QueryString["id"]); roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid); Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name; name = role.Name;
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered); await GetUserRoles();
await GetUserRoles(); }
} catch (Exception ex)
catch (Exception ex) {
{ await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error); }
} }
}
private async Task GetUserRoles() private async Task<Dictionary<string, string>> GetUsers(string filter)
{ {
try try
{ {
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name); var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
} return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
catch (Exception ex) .ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
{ }
await logger.LogError(ex, "Error Loading User Roles {RoleId} {Error}", roleid, ex.Message); catch (Exception ex)
AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error); {
} await logger.LogError(ex, "Error Loading Users {filter} {Error}", filter, ex.Message);
} AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
return new Dictionary<string, string>();
}
private async Task SaveUserRole() private async Task GetUserRoles()
{ {
validated = true; try
var interop = new Interop(JSRuntime); {
if (await interop.FormValid(form)) userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name);
{ }
try catch (Exception ex)
{ {
if (userid != -1) await logger.LogError(ex, "Error Loading User Roles {RoleId} {Error}", roleid, ex.Message);
{ AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error);
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault(); }
if (userrole != null) }
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.AddUserRoleAsync(userrole); private async Task SaveUserRole()
} {
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
if (!string.IsNullOrEmpty(user.Key) && int.TryParse(user.Key, out int userid))
{
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await logger.LogInformation("User Assigned To Role {UserRole}", userrole); await UserRoleService.AddUserRoleAsync(userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success); }
await GetUserRoles();
StateHasChanged(); await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success);
await GetUserRoles();
user.Clear();
StateHasChanged();
} }
else else
{ {

View File

@ -70,6 +70,21 @@
} }
</select> </select>
</div> </div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@Localizer["Not Specified"]&gt;</option>
@foreach (Page page in PageState.Pages)
{
if (UserSecurity.ContainsRole(page.Permissions, PermissionNames.View, RoleNames.Everyone))
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
}
</select>
</div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label> <Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
@ -112,7 +127,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmptUsername">Username: </Label> <Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" /> <input id="username" class="form-control" @bind="@_smtpusername" />
</div> </div>
@ -127,12 +142,21 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmptSender">Email Sender: </Label> <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"> <div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" /> <input id="sender" class="form-control" @bind="@_smtpsender" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<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>
</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> <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"> <div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" /> <input id="retention" class="form-control" @bind="@_retention" />
@ -228,6 +252,7 @@
<select id="runtime" class="form-select" @bind="@_runtime" required> <select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option> <option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option> <option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
<option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -295,6 +320,7 @@
private string _themetype = "-"; private string _themetype = "-";
private string _containertype = "-"; private string _containertype = "-";
private string _admincontainertype = "-"; private string _admincontainertype = "-";
private string _homepageid = "-";
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "False";
@ -303,6 +329,7 @@
private string _smtppasswordtype = "password"; private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty; private string _togglesmtppassword = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private string _retention = string.Empty; private string _retention = string.Empty;
private string _pwaisenabled; private string _pwaisenabled;
private int _pwaappiconfileid = -1; private int _pwaappiconfileid = -1;
@ -353,6 +380,11 @@
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
}
_pwaisenabled = site.PwaIsEnabled.ToString(); _pwaisenabled = site.PwaIsEnabled.ToString();
if (site.PwaAppIconFileId != null) if (site.PwaAppIconFileId != null)
{ {
@ -371,6 +403,7 @@
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); _retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -422,7 +455,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message); await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error); AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error);
} }
} }
@ -479,6 +512,7 @@
refresh = true; // needs to be refreshed on client refresh = true; // needs to be refreshed on client
} }
site.AdminContainerType = _admincontainertype; site.AdminContainerType = _admincontainertype;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
if (site.PwaIsEnabled.ToString() != _pwaisenabled) if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{ {
@ -509,6 +543,7 @@
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
@ -557,8 +592,8 @@
await AliasService.DeleteAliasAsync(alias.AliasId); await AliasService.DeleteAliasAsync(alias.AliasId);
} }
aliases = await AliasService.GetAliasesAsync(); var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId);
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + aliases.First().Name, true); NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + redirect.Name, true);
} }
else else
{ {
@ -579,12 +614,12 @@
try try
{ {
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");

View File

@ -29,7 +29,7 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="Enter the aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they can be separated by commas." ResourceKey="Aliases">Aliases: </Label> <Label Class="col-sm-3" For="alias" HelpText="Enter the aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea> <textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div> </div>
@ -89,7 +89,8 @@ else
<select id="runtime" class="form-select" @bind="@_runtime" required> <select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option> <option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option> <option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
</select> <option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -128,18 +129,43 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databaseType" HelpText="Select the database type for the tenant" ResourceKey="DatabaseType">Database Type: </Label> <Label Class="col-sm-3" For="databaseType" HelpText="Select the database type for the tenant" ResourceKey="DatabaseType">Database Type: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required> @if (_databases != null)
@foreach (var database in _databases) {
{ <div class="input-group">
<option value="@database.Name">@Localizer[@database.Name]</option> <select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
} @foreach (var database in _databases)
</select> {
</div> <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> </div>
if (_databaseConfigType != null) @if (!_showConnectionString)
{ {
@DatabaseConfigComponent; 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">String:</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"> <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> <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"> <div class="col-sm-9">
@ -169,6 +195,8 @@ else
private Type _databaseConfigType; private Type _databaseConfigType;
private object _databaseConfig; private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; } private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
private List<Theme> _themeList; private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
@ -218,7 +246,7 @@ else
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
@ -309,15 +337,22 @@ else
user.Username = _hostusername; user.Username = _hostusername;
user.Password = _hostpassword; user.Password = _hostpassword;
user.LastIPAddress = PageState.RemoteIPAddress; user.LastIPAddress = PageState.RemoteIPAddress;
user = await UserService.LoginUserAsync(user); user = await UserService.LoginUserAsync(user, false, false);
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
var connectionString = String.Empty; var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) var connectionString = String.Empty;
{ if (_showConnectionString)
connectionString = databaseConfigControl.GetConnectionString(); {
} connectionString = _connectionString;
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); }
else
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
}
if (connectionString != "") if (connectionString != "")
{ {
@ -398,4 +433,13 @@ else
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
} }
} }
private void ToggleConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionString = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
} }

View File

@ -16,7 +16,7 @@ else
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the tenant for the SQL server" ResourceKey="Tenant">Tenant: </Label> <Label Class="col-sm-3" For="tenant" HelpText="Select the tenant associated with the database server" ResourceKey="Tenant">Tenant: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="tenant" class="form-select" value="@_tenantid" @onchange="(e => TenantChanged(e))"> <select id="tenant" class="form-select" value="@_tenantid" @onchange="(e => TenantChanged(e))">
<option value="-1">&lt;@Localizer["Tenant.Select"]&gt;</option> <option value="-1">&lt;@Localizer["Tenant.Select"]&gt;</option>
@ -38,13 +38,16 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label> <Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="2" readonly></textarea> <div class="input-group">
</div> <input id="connectionstring" type="@_connectionstringtype" class="form-control" @bind="@_connectionstring" readonly />
</div> <button type="button" class="btn btn-secondary" @onclick="@ToggleConnectionString">@_connectionstringtoggle</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sqlQeury" HelpText="Enter the query for the SQL server" ResourceKey="SqlQuery">SQL Query: </Label> <Label Class="col-sm-3" For="sqlQuery" HelpText="Enter the SQL query for the database server" ResourceKey="SqlQuery">SQL Query: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="sqlQeury" class="form-control" @bind="@_sql" rows="3"></textarea> <textarea id="sqlQuery" class="form-control" @bind="@_sql" rows="3"></textarea>
</div> </div>
</div> </div>
} }
@ -85,8 +88,10 @@ else
private List<Tenant> _tenants; private List<Tenant> _tenants;
private string _tenantid = "-1"; private string _tenantid = "-1";
private string _database = string.Empty; private string _database = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
private string _sql = string.Empty; private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty;
private List<Dictionary<string, string>> _results; private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -96,7 +101,8 @@ else
try try
{ {
_tenants = await TenantService.GetTenantsAsync(); _tenants = await TenantService.GetTenantsAsync();
} _connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message); await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
@ -126,7 +132,21 @@ else
} }
} }
private async Task Execute() private void ToggleConnectionString()
{
if (_connectionstringtype == "password")
{
_connectionstringtype = "text";
_connectionstringtoggle = SharedLocalizer["HidePassword"];
}
else
{
_connectionstringtype = "password";
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
}
private async Task Execute()
{ {
try try
{ {

View File

@ -38,7 +38,13 @@
<input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly /> <input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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> <Label Class="col-sm-3" For="contentrootpath" HelpText="Root Path" ResourceKey="ContentRootPath">Root Path: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly /> <input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly />
@ -141,6 +147,16 @@
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp; <a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&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" /> <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>
<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>
</TabPanel>
</TabStrip> </TabStrip>
<br /><br /> <br /><br />
@ -152,6 +168,7 @@
private string _osversion = string.Empty; private string _osversion = string.Empty;
private string _machinename = string.Empty; private string _machinename = string.Empty;
private string _ipaddress = string.Empty; private string _ipaddress = string.Empty;
private string _environment = string.Empty;
private string _contentrootpath = string.Empty; private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty; private string _webrootpath = string.Empty;
private string _servertime = string.Empty; private string _servertime = string.Empty;
@ -165,6 +182,8 @@
private string _swagger = string.Empty; private string _swagger = string.Empty;
private string _packageservice = string.Empty; private string _packageservice = string.Empty;
private string _log = string.Empty;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_version = Constants.Version; _version = Constants.Version;
@ -176,6 +195,7 @@
_osversion = systeminfo["OSVersion"].ToString(); _osversion = systeminfo["OSVersion"].ToString();
_machinename = systeminfo["MachineName"].ToString(); _machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString(); _ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].ToString();
_contentrootpath = systeminfo["ContentRootPath"].ToString(); _contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString(); _webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC"; _servertime = systeminfo["ServerTime"].ToString() + " UTC";
@ -183,19 +203,25 @@
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB"; _workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
} }
systeminfo = await SystemService.GetSystemInfoAsync(); systeminfo = await SystemService.GetSystemInfoAsync("configuration");
if (systeminfo != null) if (systeminfo != null)
{ {
_installationid = systeminfo["InstallationId"].ToString(); _installationid = systeminfo["InstallationId"].ToString();
_detailederrors = systeminfo["DetailedErrors"].ToString(); _detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString(); _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString(); _notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString(); _swagger = systeminfo["UseSwagger"].ToString();
_packageservice = systeminfo["PackageService"].ToString(); _packageservice = systeminfo["PackageService"].ToString();
}
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
{
_log = systeminfo["Log"].ToString();
} }
} }
private async Task SaveConfig() private async Task SaveConfig()
{ {
try try
{ {

View File

@ -114,6 +114,10 @@
<button type="button" class="btn btn-success" @onclick="InstallThemes">@SharedLocalizer["Install"]</button> <button type="button" class="btn btn-success" @onclick="InstallThemes">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code { @code {
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";

View File

@ -29,7 +29,7 @@ else
<Row> <Row>
<td><ActionLink Action="View" Parameters="@($"name=" + WebUtility.UrlEncode(context.ThemeName))" ResourceKey="ViewTheme" /></td> <td><ActionLink Action="View" Parameters="@($"name=" + WebUtility.UrlEncode(context.ThemeName))" ResourceKey="ViewTheme" /></td>
<td> <td>
@if (context.AssemblyName != "Oqtane.Client") @if (context.AssemblyName != Constants.ClientId)
{ {
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" /> <ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
} }

View File

@ -25,7 +25,13 @@
<input id="version" class="form-control" @bind="@_version" disabled /> <input id="version" class="form-control" @bind="@_version" disabled />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" 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> <Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled /> <input id="owner" class="form-control" @bind="@_owner" disabled />
@ -53,28 +59,30 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code { @code {
private string _themeName = ""; private string _themeName = "";
private string _name; private string _name;
private string _version; private string _version;
private string _owner = ""; private string _packagename;
private string _url = ""; private string _owner = "";
private string _contact = ""; private string _url = "";
private string _license = ""; private string _contact = "";
private string _license = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_themeName = WebUtility.UrlDecode(PageState.QueryString["name"]); _themeName = WebUtility.UrlDecode(PageState.QueryString["name"]);
var themes = await ThemeService.GetThemesAsync(); var themes = await ThemeService.GetThemesAsync();
var theme = themes.FirstOrDefault(item => item.ThemeName == _themeName); var theme = themes.FirstOrDefault(item => item.ThemeName == _themeName);
if (theme != null) if (theme != null)
{ {
_name = theme.Name; _name = theme.Name;
_version = theme.Version; _version = theme.Version;
_owner = theme.Owner; _packagename = theme.PackageName;
_owner = theme.Owner;
_url = theme.Url; _url = theme.Url;
_contact = theme.Contact; _contact = theme.Contact;
_license = theme.License; _license = theme.License;

View File

@ -224,126 +224,140 @@ else
<br /><br /> <br /><br />
@code { @code {
private string username = string.Empty; private string username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string confirm = string.Empty;
private bool allowtwofactor = false; private bool allowtwofactor = false;
private string twofactor = "False"; private string twofactor = "False";
private string email = string.Empty; private string email = string.Empty;
private string displayname = string.Empty; private string displayname = string.Empty;
private FileManager filemanager; private FileManager filemanager;
private int folderid = -1; private int folderid = -1;
private int photofileid = -1; private int photofileid = -1;
private File photo = null; private File photo = null;
private List<Profile> profiles; private List<Profile> profiles;
private Dictionary<string, string> settings; private Dictionary<string, string> settings;
private string category = string.Empty; private string category = string.Empty;
private string filter = "to"; private string filter = "to";
private List<Notification> notifications; private List<Notification> notifications;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"])) if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{ {
allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true"); allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
} }
if (PageState.User != null) if (PageState.User != null)
{ {
username = PageState.User.Username; username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString(); twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email; email = PageState.User.Email;
displayname = PageState.User.DisplayName; displayname = PageState.User.DisplayName;
// get user folder // get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
{ {
folderid = folder.FolderId; folderid = folder.FolderId;
} }
if (PageState.User.PhotoFileId != null) if (PageState.User.PhotoFileId != null)
{ {
photofileid = PageState.User.PhotoFileId.Value; photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); photo = await FileService.GetFileAsync(photofileid);
} }
else else
{ {
photofileid = -1; photofileid = -1;
photo = null; photo = null;
} }
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
await LoadNotificationsAsync(); await LoadNotificationsAsync();
} }
else else
{ {
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning); AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message); await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
} }
} }
private async Task LoadNotificationsAsync() private async Task LoadNotificationsAsync()
{ {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task Save() private async Task Save()
{ {
try try
{ {
if (username != string.Empty && email != string.Empty && ValidateProfiles()) if (username != string.Empty && email != string.Empty && ValidateProfiles())
{ {
if (_password == confirm) if (_password == confirm)
{ {
var user = PageState.User; var user = PageState.User;
user.Username = username; user.Username = username;
user.Password = _password; user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor); user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email; user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname); user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId(); user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {
user.PhotoFileId = null; user.PhotoFileId = null;
} }
if (user.PhotoFileId != null) if (user.PhotoFileId != null)
{ {
photofileid = user.PhotoFileId.Value; photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); photo = await FileService.GetFileAsync(photofileid);
} }
else else
{ {
photofileid = -1; photofileid = -1;
photo = null; photo = null;
} }
await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); if (user != null)
await logger.LogInformation("User Profile Saved"); {
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged(); StateHasChanged();
} }
else
{
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
}
else else
{ {
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning); AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
@ -429,6 +443,7 @@ else
{ {
try try
{ {
ModuleInstance.ShowProgressIndicator();
foreach(var Notification in notifications) foreach(var Notification in notifications)
{ {
if (!Notification.IsDeleted) if (!Notification.IsDeleted)
@ -444,12 +459,15 @@ else
} }
await logger.LogInformation("Notifications Permanently Deleted"); await logger.LogInformation("Notifications Permanently Deleted");
await LoadNotificationsAsync(); await LoadNotificationsAsync();
ModuleInstance.HideProgressIndicator();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message); await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
ModuleInstance.HideProgressIndicator();
} }
} }

View File

@ -104,7 +104,7 @@
private Dictionary<string, string> settings; private Dictionary<string, string> settings;
private string category = string.Empty; private string category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -121,8 +121,15 @@
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser() private async Task SaveUser()
{ {

View File

@ -174,7 +174,7 @@ else
private string deletedby; private string deletedby;
private DateTime? deletedon; private DateTime? deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -201,58 +201,71 @@ else
photofileid = -1; photofileid = -1;
photo = null; photo = null;
} }
isdeleted = user.IsDeleted.ToString(); isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress; lastipaddress = user.LastIPAddress;
settings = await SettingService.GetUserSettingsAsync(user.UserId); settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy; createdby = user.CreatedBy;
createdon = user.CreatedOn; createdon = user.CreatedOn;
modifiedby = user.ModifiedBy; modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn; modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy; deletedby = user.DeletedBy;
deletedon = user.DeletedOn; deletedon = user.DeletedOn;
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message); await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser() private async Task SaveUser()
{ {
try try
{ {
if (username != string.Empty && email != string.Empty && ValidateProfiles()) if (username != string.Empty && email != string.Empty && ValidateProfiles())
{ {
if (_password == confirm) if (_password == confirm)
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = username;
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null; user.PhotoFileId = null;
user.PhotoFileId = filemanager.GetFileId(); user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {
user.PhotoFileId = null; user.PhotoFileId = null;
} }
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); if (user != null)
await logger.LogInformation("User Saved {User}", user); {
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
NavigationManager.NavigateTo(NavigateUrl()); await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
} }
else else
{ {

View File

@ -6,7 +6,6 @@
@inject ISiteService SiteService @inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject SiteState SiteState
@if (users == null) @if (users == null)
{ {
@ -21,7 +20,7 @@ else
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-4"> <div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" /> <ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" @bind="@_search" /> <input class="form-control" @bind="@_search" />
@ -42,21 +41,21 @@ else
</Header> </Header>
<Row> <Row>
<td> <td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" /> <ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" /> <ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td> </td>
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td> <td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn)</td> <td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings"> <TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container"> <div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings"> <Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -287,6 +286,15 @@ else
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" /> <input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div> </div>
</div> </div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
</div>
}
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label> <Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -386,6 +394,7 @@ else
private string _redirecturl; private string _redirecturl;
private string _identifierclaimtype; private string _identifierclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _roleclaimtype;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
@ -397,7 +406,7 @@ else
private string _lifetime; private string _lifetime;
private string _token; private string _token;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -437,8 +446,9 @@ else
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"); _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
@ -446,7 +456,8 @@ else
_togglesecret = SharedLocalizer["ShowPassword"]; _togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name); _issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", ""); _audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); } _lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20");
}
} }
private async Task LoadUsersAsync(bool load) private async Task LoadUsersAsync(bool load)
@ -512,7 +523,7 @@ else
private async Task UpdateUserSettingsAsync() private async Task UpdateUserSettingsAsync()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search); settings = SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
} }
@ -556,6 +567,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
@ -591,14 +603,10 @@ else
if (_providertype == AuthenticationProviderTypes.OpenIDConnect) if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{ {
_scopes = "openid,profile,email"; _scopes = "openid,profile,email";
_identifierclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
_emailclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
} }
else else
{ {
_scopes = ""; _scopes = "";
_identifierclaimtype = "sub";
_emailclaimtype = "email";
} }
} }
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;

View File

@ -0,0 +1,21 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Users
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Users",
Description = "Manage Users",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}," +
$"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -63,7 +63,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -79,7 +79,7 @@ else
private string expirydate = string.Empty; private string expirydate = string.Empty;
private List<UserRole> userroles; private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {

View File

@ -128,13 +128,6 @@
private string CloseUrl() private string CloseUrl()
{ {
if (!PageState.QueryString.ContainsKey("type")) return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl();
{
return NavigateUrl();
}
else
{
return NavigateUrl(PageState.Page.Path, "type=" + PageState.QueryString["type"] + "&days=" + PageState.QueryString["days"] + "&page=" + PageState.QueryString["page"]);
}
} }
} }

View File

@ -43,7 +43,7 @@ else
<th>@Localizer["Created"]</th> <th>@Localizer["Created"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Detail" Parameters="@($"id=" + context.VisitorId.ToString() + "&type=" + _type.ToString() + "&days=" + _days.ToString() + "&page=" + _page.ToString())" ResourceKey="Details" /></td> <td><ActionLink Action="Detail" Parameters="@($"id={context.VisitorId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, $"type={_type}&days={_days}&page={_page}"))" ResourceKey="Details" /></td>
<td>@context.IPAddress</td> <td>@context.IPAddress</td>
<td> <td>
@if (context.UserId != null) @if (context.UserId != null)

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Net
@inherits LocalizableComponent @inherits LocalizableComponent
@inject IUserService UserService @inject IUserService UserService
@ -71,6 +72,9 @@
[Parameter] [Parameter]
public bool IconOnly { get; set; } // optional - specifies only icon in link public bool IconOnly { get; set; } // optional - specifies only icon in link
[Parameter]
public string ReturnUrl { get; set; } // optional - used to set a url to redirect to
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
base.OnParametersSet(); base.OnParametersSet();
@ -117,8 +121,12 @@
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions; _permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions;
_text = Localize(nameof(Text), _text); _text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters); _url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
_authorized = IsAuthorized(); if (!string.IsNullOrEmpty(ReturnUrl))
{
_url += ((_url.Contains("?")) ? "&" : "?") + $"returnurl={WebUtility.UrlEncode(ReturnUrl)}";
}
_authorized = IsAuthorized();
} }
private bool IsAuthorized() private bool IsAuthorized()

View File

@ -0,0 +1,153 @@
@namespace Oqtane.Modules.Controls
@inherits LocalizableComponent
<div class="app-autocomplete">
<input class="form-control" value="@Value" @oninput="OnInput" @onkeyup="OnKeyUp" placeholder="@Placeholder" autocomplete="off" />
@if (_results != null)
{
<select class="form-select" style="position: relative;" value="@Value" size="@Rows" @onkeyup="OnKeyUp" @onchange="(e => OnChange(e))">
@if (_results.Any())
{
@foreach (var result in _results)
{
if (result.Value == Value)
{
<option selected>@result.Value</option>
}
else
{
<option>@result.Value</option>
}
}
}
else
{
<option disabled>No Results</option>
}
</select>
}
</div>
@code {
Dictionary<string, string> _results;
[Parameter]
public Func<string, Task<Dictionary<string, string>>> OnSearch { get; set; } // required - an async delegate method which accepts a filter string parameter and returns a dictionary
[Parameter]
public int Characters { get; set; } = 3; // optional - number of characters before search is initiated
[Parameter]
public int Rows { get; set; } = 3; // optional - number of result rows to display
[Parameter]
public string Placeholder { get; set; } // optional - placeholder input text
[Parameter]
public string Value { get; set; } // value of item selected
[Parameter]
public string Key { get; set; } // key of item selected
private async Task OnInput(ChangeEventArgs e)
{
Value = e.Value?.ToString();
if (Value?.Length >= Characters)
{
_results = await OnSearch?.Invoke(Value);
}
else
{
_results = null;
}
SetKey();
}
private async Task OnKeyUp(KeyboardEventArgs e)
{
var index = -1;
switch (e.Key)
{
case "ArrowDown":
if (_results == null)
{
if (Value?.Length >= Characters)
{
_results = await OnSearch?.Invoke(Value);
}
}
else
{
index = GetIndex();
if (index < _results.Count - 1)
{
Value = _results.ElementAt(index + 1).Value;
Key = _results.ElementAt(index + 1).Key;
}
}
break;
case "ArrowUp":
index = GetIndex();
if (index > 0)
{
Value = _results.ElementAt(index - 1).Value;
Key = _results.ElementAt(index - 1).Key;
}
break;
case "ArrowRight":
case "Tab":
_results = null;
break;
case "Enter": // note within a form the enter key submits the entire form
case "NumpadEnter":
_results = null;
break;
case "Escape":
Value = "";
_results = null;
break;
}
}
private void OnChange(ChangeEventArgs e)
{
Value = (string)e.Value;
SetKey();
_results = null;
}
private int GetIndex()
{
if (_results != null)
{
for (int index = 0; index < _results.Count; index++)
{
if (_results.ElementAt(index).Value == Value)
{
return index;
}
}
}
return -1;
}
private void SetKey()
{
var index = GetIndex();
if (index != -1)
{
Key = _results.ElementAt(index).Key;
}
else
{
Key = "";
}
}
public void Clear()
{
Value = "";
Key = "";
_results = null;
}
}

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Threading
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IFolderService FolderService @inject IFolderService FolderService
@inject IFileService FileService @inject IFileService FileService
@ -53,7 +54,7 @@
} }
</div> </div>
<div class="col mt-2 text-end"> <div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1) @if (ShowFiles && GetFileId() != -1)
{ {
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button> <button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
@ -86,7 +87,6 @@
} }
@code { @code {
private string _id;
private List<Folder> _folders; private List<Folder> _folders;
private List<File> _files = new List<File>(); private List<File> _files = new List<File>();
private string _fileinputid = string.Empty; private string _fileinputid = string.Empty;
@ -144,11 +144,6 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!string.IsNullOrEmpty(Id))
{
_id = Id;
}
// packages folder is a framework folder for uploading installable nuget packages // packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder) if (Folder == Constants.PackagesFolder)
{ {
@ -207,9 +202,9 @@
// create unique id for component // create unique id for component
_guid = Guid.NewGuid().ToString("N"); _guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput"; _fileinputid = "FileInput_" + _guid;
_progressinfoid = _guid + "ProgressInfo"; _progressinfoid = "ProgressInfo_" + _guid;
_progressbarid = _guid + "ProgressBar"; _progressbarid = "ProgressBar_" + _guid;
} }
private async Task GetFiles() private async Task GetFiles()
@ -304,17 +299,17 @@
} }
} }
private async Task UploadFile() private async Task UploadFiles()
{ {
_message = string.Empty; _message = string.Empty;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid); var uploads = await interop.GetFiles(_fileinputid);
if (upload.Length > 0) if (uploads.Length > 0)
{ {
string restricted = ""; string restricted = "";
foreach (var file in upload) foreach (var upload in uploads)
{ {
var extension = (file.LastIndexOf(".") != -1) ? file.Substring(file.LastIndexOf(".") + 1) : ""; var extension = (upload.LastIndexOf(".") != -1) ? upload.Substring(upload.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower())) if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{ {
restricted += (restricted == "" ? "" : ",") + extension; restricted += (restricted == "" ? "" : ",") + extension;
@ -324,48 +319,68 @@
{ {
try try
{ {
string result; // upload the files
if (Folder == Constants.PackagesFolder) var posturl = Utilities.TenantUrl(PageState.Alias, "/api/file/upload");
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
// uploading is asynchronous so we need to wait for the uploads to complete
// note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
bool success = false;
int attempts = 0;
while (attempts < 5 && !success)
{ {
result = await FileService.UploadFilesAsync(Folder, upload, _guid); attempts += 1;
} Thread.Sleep(1000 * attempts); // progressive retry
else
{ success = true;
result = await FileService.UploadFilesAsync(FolderId, upload, _guid); List<File> files = await FileService.GetFilesAsync(folder);
if (files.Count > 0)
{
foreach (string upload in uploads)
{
if (!files.Exists(item => item.Name == upload))
{
success = false;
}
}
}
} }
if (result == string.Empty) // reset progress indicators
await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
if (success)
{ {
await logger.LogInformation("File Upload Succeeded {Files}", upload); await logger.LogInformation("File Upload Succeeded {Files}", uploads);
if (ShowSuccess) if (ShowSuccess)
{ {
_message = Localizer["Success.File.Upload"]; _message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
} }
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
StateHasChanged();
} }
else else
{ {
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", ")); await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message); await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
@ -376,42 +391,62 @@
_messagetype = MessageType.Warning; _messagetype = MessageType.Warning;
} }
} }
else else
{ {
_message = Localizer["Message.File.NotSelected"]; _message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning; _messagetype = MessageType.Warning;
} }
} }
private async Task DeleteFile() private async Task DeleteFile()
{ {
_message = string.Empty; _message = string.Empty;
try try
{ {
await FileService.DeleteFileAsync(FileId); await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId); await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId); await OnDelete.InvokeAsync(FileId);
_message = Localizer["Success.File.Delete"]; _message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
await GetFiles(); await GetFiles();
FileId = -1; FileId = -1;
await SetImage(); await SetImage();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message); await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
_message = Localizer["Error.File.Delete"]; _message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error; _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(int fileId)
{
await GetFiles();
if (fileId != -1)
{
var file = _files.Where(item => item.FileId == fileId).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
}
}
StateHasChanged();
}
} }

View File

@ -8,16 +8,16 @@
@if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? "" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
@ -25,27 +25,27 @@
var pager = i; var pager = i;
if (pager == _page) if (pager == _page)
{ {
<li class="page-item active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? "" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
@ -55,43 +55,46 @@
} }
@if (Format == "Table" && Row != null) @if (Format == "Table" && Row != null)
{ {
<div class="table-responsive"> <div class="table-responsive">
<table class="@Class"> <table class="@Class">
<thead> <thead>
<tr class="@RowClass">@Header</tr> <tr class="@RowClass">@Header</tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var item in ItemList) @foreach (var item in ItemList)
{ {
<tr class="@RowClass">@Row(item)</tr> <tr class="@RowClass">@Row(item)</tr>
@if (Detail != null) @if (Detail != null)
{ {
<tr>@Detail(item)</tr> <tr>@Detail(item)</tr>
} }
} }
</tbody> </tbody>
</table> <tfoot>
</div> <tr class="@RowClass">@Footer</tr>
</tfoot>
</table>
</div>
} }
@if (Format == "Grid" && Row != null) @if (Format == "Grid" && Row != null)
{ {
int count = 0; int count = 0;
int rows = 0; int rows = 0;
int cols = 0; int cols = 0;
if (ItemList != null) if (ItemList != null)
{ {
if (_columns == 0) if (_columns == 0)
{ {
count = ItemList.Count(); count = ItemList.Count();
rows = 1; rows = 1;
cols = count; cols = count;
} }
else else
{ {
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns; count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns;
rows = count / _columns; rows = count / _columns;
cols = _columns; cols = _columns;
} }
} }
<div class="@Class"> <div class="@Class">
@for (int row = 0; row < rows; row++) @for (int row = 0; row < rows; row++)
@ -113,19 +116,19 @@
} }
</div> </div>
} }
@if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? "" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
@ -133,97 +136,100 @@
var pager = i; var pager = i;
if (pager == _page) if (pager == _page)
{ {
<li class="page-item active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? "" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a> <a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
} }
@code { @code {
private IStringLocalizer Localizer; private IStringLocalizer Localizer;
private int _pages = 0; private int _pages = 0;
private int _page = 1; private int _page = 1;
private int _maxItems = 10; private int _maxItems = 10;
private int _displayPages = 5; private int _displayPages = 5;
private int _startPage = 0; private int _startPage = 0;
private int _endPage = 0; private int _endPage = 0;
private int _columns = 0; private int _columns = 0;
[Parameter] [Parameter]
public string Format { get; set; } // Table or Grid public string Format { get; set; } // Table or Grid
[Parameter] [Parameter]
public string Toolbar { get; set; } // Top, Bottom or Both public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter] [Parameter]
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter] [Parameter]
public RenderFragment<TableItem> Row { get; set; } = null; // required public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter] [Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts public RenderFragment Footer { get; set; } = null; // only applicable to Table layouts
[Parameter] [Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter] [Parameter]
public string PageSize { get; set; } // number of items to display on a page public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter] [Parameter]
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior public string PageSize { get; set; } // number of items to display on a page
[Parameter] [Parameter]
public string CurrentPage { get; set; } // sets the initial page to display public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter] [Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection public string CurrentPage { get; set; } // sets the initial page to display
[Parameter] [Parameter]
public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter] [Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
[Parameter] [Parameter]
public string ColumnClass { get; set; } // class for column element - only applicable to Grid format public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
[Parameter] [Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes public string ColumnClass { get; set; } // class for column element - only applicable to Grid format
private IEnumerable<TableItem> ItemList { get; set; } [Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes
protected override void OnInitialized() private IEnumerable<TableItem> ItemList { get; set; }
{
Localizer = LocalizerFactory.Create(GetType().FullName);
}
protected override void OnParametersSet() protected override void OnInitialized()
{ {
Localizer = LocalizerFactory.Create(GetType().FullName);
}
protected override void OnParametersSet()
{
if (string.IsNullOrEmpty(Format)) if (string.IsNullOrEmpty(Format))
{ {
Format = "Table"; Format = "Table";
@ -270,7 +276,7 @@
} }
} }
if (!string.IsNullOrEmpty(PageSize)) if (!string.IsNullOrEmpty(PageSize))
{ {
_maxItems = int.Parse(PageSize); _maxItems = int.Parse(PageSize);
} }
@ -293,6 +299,7 @@
{ {
_page = 1; _page = 1;
} }
if (_page < 1) _page = 1;
_startPage = 0; _startPage = 0;
_endPage = 0; _endPage = 0;
@ -304,7 +311,6 @@
{ {
_page = _pages; _page = _pages;
} }
ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
SetPagerSize(); SetPagerSize();
} }
} }
@ -317,13 +323,13 @@
{ {
_endPage = _pages; _endPage = _pages;
} }
OnPageChange?.Invoke(_page); ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
StateHasChanged(); StateHasChanged();
} OnPageChange?.Invoke(_page);
}
public void UpdateList(int page) public void UpdateList(int page)
{ {
ItemList = Items.Skip((page - 1) * _maxItems).Take(_maxItems);
_page = page; _page = page;
SetPagerSize(); SetPagerSize();
} }

View File

@ -2,6 +2,7 @@
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IRoleService RoleService @inject IRoleService RoleService
@inject IUserService UserService @inject IUserService UserService
@inject IUserRoleService UserRoleService
@inject IStringLocalizer<PermissionGrid> Localizer @inject IStringLocalizer<PermissionGrid> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -16,8 +17,8 @@
<th scope="col">@Localizer["Role"]</th> <th scope="col">@Localizer["Role"]</th>
@foreach (PermissionString permission in _permissions) @foreach (PermissionString permission in _permissions)
{ {
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th> <th style="text-align: center; width: 1px;">@((MarkupString)GetPermissionName(permission).Replace(" ", "<br />"))</th>
} }
</tr> </tr>
@foreach (Role role in _roles) @foreach (Role role in _roles)
{ {
@ -27,7 +28,7 @@
{ {
var p = permission; var p = permission;
<td style="text-align: center;"> <td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled=@GetPermissionDisabled(role.Name) OnChange="@(e => PermissionChanged(e, p.PermissionName, role.Name))" /> <TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled="@GetPermissionDisabled(p.EntityName, p.PermissionName, role.Name)" OnChange="@(e => PermissionChanged(e, p.EntityName, p.PermissionName, role.Name))" />
</td> </td>
} }
</tr> </tr>
@ -65,7 +66,7 @@
{ {
var p = permission; var p = permission;
<td style="text-align: center; width: 1px;"> <td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled=false OnChange="@(e => PermissionChanged(e, p.PermissionName, userid))" /> <TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled="@GetPermissionDisabled(p.EntityName, p.PermissionName, "")" OnChange="@(e => PermissionChanged(e, p.EntityName, p.PermissionName, userid))" />
</td> </td>
} }
</tr> </tr>
@ -77,23 +78,16 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col-11">
<table class="table table-borderless"> <AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
<tbody> </div>
<tr> <div class="col-1">
<td class="input-group"> <button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
<input type="text" name="Username" class="form-control" placeholder="@Localizer["Username.Enter"]" @bind="@_username" />
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</td>
</tr>
</tbody>
</table>
<br />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<ModuleMessage Type="MessageType.Error" Message="@_message" /> <ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div> </div>
</div> </div>
</div> </div>
@ -104,7 +98,7 @@
private List<Role> _roles; private List<Role> _roles;
private List<PermissionString> _permissions; private List<PermissionString> _permissions;
private List<User> _users = new List<User>(); private List<User> _users = new List<User>();
private string _username = string.Empty; private AutoComplete _user;
private string _message = string.Empty; private string _message = string.Empty;
[Parameter] [Parameter]
@ -135,10 +129,25 @@
_permissions = new List<PermissionString>(); _permissions = new List<PermissionString>();
foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string permissionname in _permissionnames.Split(',', StringSplitOptions.RemoveEmptyEntries))
{ {
// initialize with admin role // permission names can be in the form of "EntityName:PermissionName:Roles"
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin }); if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
if (!segments[2].Contains(RoleNames.Admin))
{
segments[2] = RoleNames.Admin + ";" + segments[2]; // ensure admin access
}
_permissions.Add(new PermissionString { EntityName = segments[0], PermissionName = segments[1], Permissions = segments[2] });
}
}
else
{
_permissions.Add(new PermissionString { EntityName = EntityName, PermissionName = permissionname, Permissions = RoleNames.Admin });
}
} }
if (!string.IsNullOrEmpty(Permissions)) if (!string.IsNullOrEmpty(Permissions))
@ -146,14 +155,15 @@
// populate permissions // populate permissions
foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions)) foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions))
{ {
if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null) int index = _permissions.FindIndex(item => item.EntityName == permissionstring.EntityName && item.PermissionName == permissionstring.PermissionName);
if (index != -1)
{ {
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions; _permissions[index].Permissions = permissionstring.Permissions;
} }
if (permissionstring.Permissions.Contains("[")) if (permissionstring.Permissions.Contains("["))
{ {
foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string user in permissionstring.Permissions.Split('[', StringSplitOptions.RemoveEmptyEntries))
{ {
if (user.Contains("]")) if (user.Contains("]"))
{ {
@ -169,6 +179,16 @@
} }
} }
private string GetPermissionName(PermissionString permission)
{
var permissionname = Localizer[permission.PermissionName].ToString();
if (!string.IsNullOrEmpty(EntityName))
{
permissionname += " " + Localizer[permission.EntityName].ToString();
}
return permissionname;
}
private bool? GetPermissionValue(string permissions, string securityKey) private bool? GetPermissionValue(string permissions, string securityKey)
{ {
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";")) if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
@ -188,38 +208,58 @@
} }
} }
private bool GetPermissionDisabled(string roleName) private bool GetPermissionDisabled(string entityName, string permissionName, string roleName)
=> (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) ? true : false; {
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (entityName != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
}
private async Task AddUser() private async Task AddUser()
{ {
if (_users.Where(item => item.Username == _username).FirstOrDefault() == null) if (!string.IsNullOrEmpty(_user.Key))
{ {
try var user = await UserService.GetUserAsync(int.Parse(_user.Key), ModuleState.SiteId);
if (user != null && !_users.Any(item => item.UserId == user.UserId))
{ {
var user = await UserService.GetUserAsync(_username, ModuleState.SiteId); _users.Add(user);
if (user != null)
{
_users.Add(user);
}
}
catch
{
_message = Localizer["Message.Username.DontExist"];
} }
} }
else
_username = string.Empty; {
_message = Localizer["Message.Username.DontExist"];
}
_user.Clear();
} }
private void PermissionChanged(bool? value, string permissionName, string securityId) private void PermissionChanged(bool? value, string entityName, string permissionName, string securityId)
{ {
var selected = value; var selected = value;
var permission = _permissions.Find(item => item.PermissionName == permissionName); int index = _permissions.FindIndex(item => item.EntityName == entityName && item.PermissionName == permissionName);
if (permission != null) if (index != -1)
{ {
var ids = permission.Permissions.Split(';').ToList(); var permission = _permissions[index];
var ids = permission.Permissions.Split(';').ToList();
ids.Remove(securityId); // remove grant permission ids.Remove(securityId); // remove grant permission
ids.Remove("!" + securityId); // remove deny permission ids.Remove("!" + securityId); // remove deny permission
@ -235,7 +275,7 @@
break; // permission not specified break; // permission not specified
} }
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionName)].Permissions = string.Join(";", ids.ToArray()); _permissions[index].Permissions = string.Join(";", ids.ToArray());
} }
} }
@ -248,9 +288,9 @@
private void ValidatePermissions() private void ValidatePermissions()
{ {
PermissionString permission; PermissionString permission;
for (int i = 0; i < _permissions.Count; i++) for (int index = 0; index < _permissions.Count; index++)
{ {
permission = _permissions[i]; permission = _permissions[index];
List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList(); List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Unauthenticated); // remove deny unauthenticated ids.Remove("!" + RoleNames.Unauthenticated); // remove deny unauthenticated
@ -266,7 +306,7 @@
} }
} }
permission.Permissions = string.Join(";", ids.ToArray()); permission.Permissions = string.Join(";", ids.ToArray());
_permissions[i] = permission; _permissions[index] = permission;
} }
} }
} }

View File

@ -5,79 +5,100 @@
<div class="row" style="margin-bottom: 50px;"> <div class="row" style="margin-bottom: 50px;">
<div class="col"> <div class="col">
<TabStrip> <TabStrip>
<TabPanel Name="Rich" Heading="Rich Text Editor"> <TabPanel Name="Rich" Heading="Rich Text Editor">
@if (AllowFileManagement) @if (_richfilemanager)
{ {
@if (_filemanagervisible) <FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
{ <ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" /> <br />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage> }
<br /> <div class="d-flex justify-content-center mb-2">
} @if (AllowRawHtml)
<div class="d-flex justify-content-center mb-2"> {
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>&nbsp;&nbsp; <button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-primary" @onclick="InsertImage">@Localizer["InsertImage"]</button> }
@if (_filemanagervisible) @if (AllowFileManagement)
{ {
@((MarkupString)"&nbsp;&nbsp;") <button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
<button type="button" class="btn btn-secondary" @onclick="CloseFileManager">@Localizer["Close"]</button> }
} @if (_richfilemanager)
</div> {
} @((MarkupString)"&nbsp;&nbsp;")
<div class="row"> <button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
<div class="col"> }
<div @ref="@_toolBar"> </div>
@if (ToolbarContent != null) <div class="row">
{ <div class="col">
@ToolbarContent <div @ref="@_toolBar">
} @if (ToolbarContent != null)
else {
{ @ToolbarContent
<select class="ql-header"> }
<option selected=""></option> else
<option value="1"></option> {
<option value="2"></option> <select class="ql-header">
<option value="3"></option> <option selected=""></option>
<option value="4"></option> <option value="1"></option>
<option value="5"></option> <option value="2"></option>
</select> <option value="3"></option>
<span class="ql-formats"> <option value="4"></option>
<button class="ql-bold"></button> <option value="5"></option>
<button class="ql-italic"></button> </select>
<button class="ql-underline"></button> <span class="ql-formats">
<button class="ql-strike"></button> <button class="ql-bold"></button>
</span> <button class="ql-italic"></button>
<span class="ql-formats"> <button class="ql-underline"></button>
<select class="ql-color"></select> <button class="ql-strike"></button>
<select class="ql-background"></select> </span>
</span> <span class="ql-formats">
<span class="ql-formats"> <select class="ql-color"></select>
<button class="ql-list" value="ordered"></button> <select class="ql-background"></select>
<button class="ql-list" value="bullet"></button> </span>
</span> <span class="ql-formats">
<span class="ql-formats"> <button class="ql-list" value="ordered"></button>
<button class="ql-link"></button> <button class="ql-list" value="bullet"></button>
</span> </span>
} <span class="ql-formats">
</div> <button class="ql-link"></button>
<div @ref="@_editorElement"> </span>
</div> }
</div> </div>
</div> <div @ref="@_editorElement">
</TabPanel> </div>
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor"> </div>
<div class="d-flex justify-content-center mb-2"> </div>
<button type="button" class="btn btn-secondary" @onclick="RefreshRawHtml">@Localizer["SynchronizeContent"]</button> </TabPanel>
</div> @if (AllowRawHtml)
@if (ReadOnly) {
{ <TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
<textarea class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea> @if (_rawfilemanager)
} {
else <FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
{ <ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<textarea class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea> <br />
} }
</TabPanel> <div class="d-flex justify-content-center mb-2">
<button type="button" class="btn btn-secondary" @onclick="RefreshRawHtml">@Localizer["SynchronizeContent"]</button>&nbsp;&nbsp;
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
</TabStrip> </TabStrip>
</div> </div>
</div> </div>
@ -85,10 +106,11 @@
@code { @code {
private ElementReference _editorElement; private ElementReference _editorElement;
private ElementReference _toolBar; private ElementReference _toolBar;
private bool _filemanagervisible = false; private bool _richfilemanager = false;
private FileManager _fileManager; private FileManager _fileManager;
private string _richhtml = string.Empty; private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty; private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtml = string.Empty; private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty; private string _originalrawhtml = string.Empty;
private string _message = string.Empty; private string _message = string.Empty;
@ -102,6 +124,12 @@
[Parameter] [Parameter]
public string Placeholder { get; set; } = "Enter Your Content..."; public string Placeholder { get; set; } = "Enter Your Content...";
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
public bool AllowRawHtml { get; set; } = true;
// parameters only applicable to rich text editor // parameters only applicable to rich text editor
[Parameter] [Parameter]
public RenderFragment ToolbarContent { get; set; } public RenderFragment ToolbarContent { get; set; }
@ -112,9 +140,6 @@
[Parameter] [Parameter]
public string DebugLevel { get; set; } = "info"; public string DebugLevel { get; set; } = "info";
[Parameter]
public bool AllowFileManagement { get; set; } = true;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" },
@ -152,9 +177,16 @@
} }
} }
public void CloseFileManager() public void CloseRichFileManager()
{ {
_filemanagervisible = false; _richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty; _message = string.Empty;
StateHasChanged(); StateHasChanged();
} }
@ -194,29 +226,55 @@
return _originalrawhtml; return _originalrawhtml;
} }
} }
} }
public async Task InsertImage() public async Task InsertRichImage()
{ {
_message = string.Empty; _message = string.Empty;
if (_filemanagervisible) if (_richfilemanager)
{ {
var file = _fileManager.GetFile(); var file = _fileManager.GetFile();
if (file != null) if (file != null)
{ {
var interop = new RichTextEditorInterop(JSRuntime); var interop = new RichTextEditorInterop(JSRuntime);
await interop.InsertImage(_editorElement, file.Url, file.Name); await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name));
_filemanagervisible = false; _richfilemanager = false;
} }
else else
{ {
_message = Localizer["Message.Require.Image"]; _message = Localizer["Message.Require.Image"];
} }
} }
else else
{ {
_filemanagervisible = true; _richfilemanager = true;
} }
StateHasChanged(); StateHasChanged();
} }
public async Task InsertRawImage()
{
_message = string.Empty;
if (_rawfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new Interop(JSRuntime);
int pos = await interop.GetCaretPosition("rawhtmleditor");
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\" class=\"img-fluid\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos);
_rawfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
} }

View File

@ -3,13 +3,13 @@
@if (Name == Parent.ActiveTab) @if (Name == Parent.ActiveTab)
{ {
<div id="@Name" class="tab-pane fade show active" role="tabpanel"> <div id="@(Parent.Id + Name)" class="tab-pane fade show active" role="tabpanel">
@ChildContent @ChildContent
</div> </div>
} }
else else
{ {
<div id="@Name" class="tab-pane fade" role="tabpanel"> <div id="@(Parent.Id + Name)" class="tab-pane fade" role="tabpanel">
@ChildContent @ChildContent
</div> </div>
} }

View File

@ -10,13 +10,13 @@
<li class="nav-item" @key="tabPanel.Name"> <li class="nav-item" @key="tabPanel.Name">
@if (tabPanel.Name == ActiveTab) @if (tabPanel.Name == ActiveTab)
{ {
<a class="nav-link active" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true"> <a class="nav-link active" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading() @tabPanel.DisplayHeading()
</a> </a>
} }
else else
{ {
<a class="nav-link" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true"> <a class="nav-link" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading() @tabPanel.DisplayHeading()
</a> </a>
} }
@ -32,18 +32,31 @@
</CascadingValue> </CascadingValue>
@code { @code {
private List<TabPanel> _tabPanels; private List<TabPanel> _tabPanels;
private string _tabpanelid = string.Empty;
[Parameter] [Parameter]
public RenderFragment ChildContent { get; set; } // contains the TabPanels public RenderFragment ChildContent { get; set; } // contains the TabPanels
[Parameter] [Parameter]
public string ActiveTab { get; set; } // optional - defaults to first TabPanel if not specified. Can also be set using a "tab=" querystring parameter. public string ActiveTab { get; set; } // optional - defaults to first TabPanel if not specified. Can also be set using a "tab=" querystring parameter.
[Parameter] [Parameter]
public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used. public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used.
protected override void OnParametersSet() [Parameter]
public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided)
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Id))
{
// create unique id for component
Id = "TabStrip_" + Guid.NewGuid().ToString("N") + "_" ;
}
}
protected override void OnParametersSet()
{ {
if (PageState.QueryString.ContainsKey("tab")) if (PageState.QueryString.ContainsKey("tab"))
{ {

View File

@ -13,7 +13,7 @@
<TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit"> <TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
@if (_content != null) @if (_content != null)
{ {
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" @ref="@RichTextEditorHtml"></RichTextEditor> <RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" AllowRawHtml="@_allowrawhtml" @ref="@RichTextEditorHtml"></RichTextEditor>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@ -60,6 +60,7 @@
private RichTextEditor RichTextEditorHtml; private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement; private bool _allowfilemanagement;
private bool _allowrawhtml;
private string _content = null; private string _content = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@ -73,6 +74,7 @@
try try
{ {
_allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true")); _allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"));
_allowrawhtml = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true"));
await LoadContent(); await LoadContent();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -15,18 +15,29 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="files" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify If Editors Can Enter Raw HTML">Allow Raw HTML: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowrawhtml">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div> </div>
@code { @code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement; private string _allowfilemanagement;
private string _allowrawhtml;
protected override void OnInitialized() protected override void OnInitialized()
{ {
try try
{ {
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"); _allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
} _allowrawhtml = SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true");
}
catch (Exception ex) catch (Exception ex)
{ {
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error); ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
@ -39,7 +50,8 @@
{ {
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement); settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); settings = SettingService.SetSetting(settings, "AllowRawHtml", _allowrawhtml);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -15,6 +15,8 @@ namespace Oqtane.Modules
public abstract class ModuleBase : ComponentBase, IModuleControl public abstract class ModuleBase : ComponentBase, IModuleControl
{ {
private Logger _logger; private Logger _logger;
private string _urlparametersstate;
private Dictionary<string, string> _urlparameters;
protected Logger logger => _logger ?? (_logger = new Logger(this)); protected Logger logger => _logger ?? (_logger = new Logger(this));
@ -24,6 +26,9 @@ namespace Oqtane.Modules
[Inject] [Inject]
protected IJSRuntime JSRuntime { get; set; } protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected SiteState SiteState { get; set; }
[CascadingParameter] [CascadingParameter]
protected PageState PageState { get; set; } protected PageState PageState { get; set; }
@ -44,6 +49,21 @@ namespace Oqtane.Modules
public virtual List<Resource> Resources { get; set; } public virtual List<Resource> Resources { get; set; }
// url parameters
public virtual string UrlParametersTemplate { get; set; }
public Dictionary<string, string> UrlParameters {
get
{
if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters)
{
_urlparametersstate = PageState.UrlParameters;
_urlparameters = GetUrlParameters(UrlParametersTemplate);
}
return _urlparameters;
}
}
// base lifecycle method for handling JSInterop script registration // base lifecycle method for handling JSInterop script registration
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
@ -52,14 +72,24 @@ namespace Oqtane.Modules
{ {
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var interop = new Interop(JSRuntime);
var scripts = new List<object>(); var scripts = new List<object>();
var inline = 0;
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{ {
scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); if (!string.IsNullOrEmpty(resource.Url))
{
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url;
scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
}
else
{
inline += 1;
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower());
}
} }
if (scripts.Any()) if (scripts.Any())
{ {
var interop = new Interop(JSRuntime);
await interop.IncludeScripts(scripts.ToArray()); await interop.IncludeScripts(scripts.ToArray());
} }
} }
@ -70,7 +100,7 @@ namespace Oqtane.Modules
public string ModulePath() public string ModulePath()
{ {
return "Modules/" + GetType().Namespace + "/"; return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
} }
// url methods // url methods
@ -124,14 +154,23 @@ namespace Oqtane.Modules
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters); return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters);
} }
public string ContentUrl(int fileid) public string FileUrl(string folderpath, string filename)
{ {
return ContentUrl(fileid, false); return FileUrl(folderpath, filename, false);
} }
public string ContentUrl(int fileid, bool asAttachment) public string FileUrl(string folderpath, string filename, bool download)
{ {
return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment); return Utilities.FileUrl(PageState.Alias, folderpath, filename, download);
}
public string FileUrl(int fileid)
{
return FileUrl(fileid, false);
}
public string FileUrl(int fileid, bool download)
{
return Utilities.FileUrl(PageState.Alias, fileid, download);
} }
public string ImageUrl(int fileid, int width, int height) public string ImageUrl(int fileid, int width, int height)
@ -149,15 +188,26 @@ namespace Oqtane.Modules
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode, position, background, rotate, recreate); return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode, position, background, rotate, recreate);
} }
public virtual Dictionary<string, string> GetUrlParameters(string parametersTemplate = "") public string AddUrlParameters(params object[] parameters)
{
var url = "";
for (var i = 0; i < parameters.Length; i++)
{
url += "/" + parameters[i].ToString();
}
return url;
}
// template is in the form of a standard route template ie. "/{id}/{name}" and produces dictionary of key/value pairs
// if url parameters belong to a specific module you should embed a unique key into the route (ie. /!/blog/1) and validate the url parameter key in the module
public virtual Dictionary<string, string> GetUrlParameters(string template = "")
{ {
var urlParameters = new Dictionary<string, string>(); var urlParameters = new Dictionary<string, string>();
string[] templateSegments; var parameters = _urlparametersstate.Split('/', StringSplitOptions.RemoveEmptyEntries);
var parameters = PageState.UrlParameters.Split('/', StringSplitOptions.RemoveEmptyEntries);
var parameterId = 0;
if (string.IsNullOrEmpty(parametersTemplate)) if (string.IsNullOrEmpty(template))
{ {
// no template will populate dictionary with generic "parameter#" keys
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
urlParameters.TryAdd("parameter" + i, parameters[i]); urlParameters.TryAdd("parameter" + i, parameters[i]);
@ -165,39 +215,37 @@ namespace Oqtane.Modules
} }
else else
{ {
templateSegments = parametersTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries); var segments = template.Split('/', StringSplitOptions.RemoveEmptyEntries);
string key;
if (parameters.Length == templateSegments.Length) for (int i = 0; i < parameters.Length; i++)
{ {
for (int i = 0; i < parameters.Length; i++) if (i < segments.Length)
{ {
if (parameters.Length > i) key = segments[i];
if (key.StartsWith("{") && key.EndsWith("}"))
{ {
if (templateSegments[i] == parameters[i]) // dynamic segment
{ key = key.Substring(1, key.Length - 2);
urlParameters.TryAdd("parameter" + parameterId, parameters[i]); }
parameterId++; else
} {
else if (templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}")) // static segments use generic "parameter#" keys
{ key = "parameter" + i.ToString();
var key = templateSegments[i].Replace("{", "");
key = key.Replace("}", "");
urlParameters.TryAdd(key, parameters[i]);
}
else
{
i = parameters.Length;
urlParameters.Clear();
}
} }
} }
else // unspecified segments use generic "parameter#" keys
{
key = "parameter" + i.ToString();
}
urlParameters.TryAdd(key, parameters[i]);
} }
} }
return urlParameters; return urlParameters;
} }
// user feedback methods // UI methods
public void AddModuleMessage(string message, MessageType type) public void AddModuleMessage(string message, MessageType type)
{ {
ModuleInstance.AddModuleMessage(message, type); ModuleInstance.AddModuleMessage(message, type);
@ -218,6 +266,18 @@ namespace Oqtane.Modules
ModuleInstance.HideProgressIndicator(); ModuleInstance.HideProgressIndicator();
} }
public void SetModuleTitle(string title)
{
var obj = new { PageModuleId = ModuleState.PageModuleId, Title = title };
SiteState.Properties.ModuleTitle = obj;
}
public void SetModuleVisibility(bool visible)
{
var obj = new { PageModuleId = ModuleState.PageModuleId, Visible = visible };
SiteState.Properties.ModuleVisibility = obj;
}
// logging methods // logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args) public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{ {
@ -255,15 +315,10 @@ namespace Oqtane.Modules
{ {
int pageId = ModuleState.PageId; int pageId = ModuleState.PageId;
int moduleId = ModuleState.ModuleId; int moduleId = ModuleState.ModuleId;
int? userId = null;
if (PageState.User != null)
{
userId = PageState.User.UserId;
}
string category = GetType().AssemblyQualifiedName; string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1); string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(alias, pageId, moduleId, userId, category, feature, function, level, exception, message, args); await LoggingService.Log(alias, pageId, moduleId, PageState.User?.UserId, category, feature, function, level, exception, message, args);
} }
public class Logger public class Logger
@ -365,5 +420,17 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Critical, "", exception, message, args); await _moduleBase.Log(null, LogLevel.Critical, "", exception, message, args);
} }
} }
[Obsolete("ContentUrl(int fileId) is deprecated. Use FileUrl(int fileId) instead.", false)]
public string ContentUrl(int fileid)
{
return ContentUrl(fileid, false);
}
[Obsolete("ContentUrl(int fileId, bool asAttachment) is deprecated. Use FileUrl(int fileId, bool download) instead.", false)]
public string ContentUrl(int fileid, bool asAttachment)
{
return Utilities.FileUrl(PageState.Alias, fileid, asAttachment);
}
} }
} }

View File

@ -5,15 +5,15 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion> <RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.1.3</Version> <Version>3.3.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
<Description>Modular Application Framework for Blazor</Description> <Description>Modular Application Framework for Blazor and MAUI</Description>
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.3.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -35,13 +35,8 @@
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" /> <ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="System.Runtime" />
<TrimmerRootAssembly Include="System.Linq.Parallel" />
<TrimmerRootAssembly Include="System.Runtime.CompilerServices.VisualC" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression> <BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup> </PropertyGroup>

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
@ -33,7 +34,7 @@ namespace Oqtane.Client
builder.Services.AddOptions(); builder.Services.AddOptions();
// Register localization services // register localization services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
// register auth services // register auth services
@ -42,7 +43,9 @@ namespace Oqtane.Client
// register scoped core services // register scoped core services
builder.Services.AddOqtaneScopedServices(); builder.Services.AddOqtaneScopedServices();
await LoadClientAssemblies(httpClient); var serviceProvider = builder.Services.BuildServiceProvider();
await LoadClientAssemblies(httpClient, serviceProvider);
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies) foreach (var assembly in assemblies)
@ -58,33 +61,106 @@ namespace Oqtane.Client
await SetCultureFromLocalizationCookie(host.Services); await SetCultureFromLocalizationCookie(host.Services);
ServiceActivator.Configure(host.Services);
await host.RunAsync(); await host.RunAsync();
} }
private static async Task LoadClientAssemblies(HttpClient http) private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
{ {
// get list of loaded assemblies on the client var dlls = new Dictionary<string, byte[]>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); var pdbs = new Dictionary<string, byte[]>();
var list = new List<string>();
// get assemblies from server and load into client app domain var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
var zip = await http.GetByteArrayAsync($"/api/Installation/load"); var interop = new Interop(jsRuntime);
var files = await interop.GetIndexedDBKeys(".dll");
// asemblies and debug symbols are packaged in a zip file if (files.Count() != 0)
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{ {
var dlls = new Dictionary<string, byte[]>(); // get list of assemblies from server
var pdbs = new Dictionary<string, byte[]>(); var json = await http.GetStringAsync("/api/Installation/list");
var assemblies = JsonSerializer.Deserialize<List<string>>(json);
foreach (ZipArchiveEntry entry in archive.Entries) // determine which assemblies need to be downloaded
foreach (var assembly in assemblies)
{ {
if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName))) var file = files.FirstOrDefault(item => item.Contains(assembly));
if (file == null)
{
list.Add(assembly);
}
else
{
// check if newer version available
if (GetFileDate(assembly) > GetFileDate(file))
{
list.Add(assembly);
}
}
}
// get assemblies already downloaded
foreach (var file in files)
{
if (assemblies.Contains(file) && !list.Contains(file))
{
try
{
dlls.Add(file, await interop.GetIndexedDBItem<byte[]>(file));
var pdb = file.Replace(".dll", ".pdb");
if (files.Contains(pdb))
{
pdbs.Add(pdb, await interop.GetIndexedDBItem<byte[]>(pdb));
}
}
catch
{
// ignore
}
}
else // file is deprecated
{
try
{
await interop.RemoveIndexedDBItem(file);
await interop.RemoveIndexedDBItem(file.Replace(".dll", ".pdb"));
}
catch
{
// ignore
}
}
}
}
else
{
list.Add("*");
}
if (list.Count != 0)
{
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", list));
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{ {
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
entry.Open().CopyTo(memoryStream); entry.Open().CopyTo(memoryStream);
byte[] file = memoryStream.ToArray(); byte[] file = memoryStream.ToArray();
// save assembly to indexeddb
try
{
await interop.SetIndexedDBItem(entry.FullName, file);
}
catch
{
// ignore
}
switch (Path.GetExtension(entry.FullName)) switch (Path.GetExtension(entry.FullName))
{ {
case ".dll": case ".dll":
@ -97,23 +173,31 @@ namespace Oqtane.Client
} }
} }
} }
}
foreach (var item in dlls) // load assemblies into app domain
foreach (var item in dlls)
{
if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb")))
{ {
if (pdbs.ContainsKey(item.Key)) AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")]));
{ }
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key])); else
} {
else AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value));
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value));
}
} }
} }
} }
private static DateTime GetFileDate(string filepath)
{
var segments = filepath.Split('.');
return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
}
private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) private static void RegisterModuleServices(Assembly assembly, IServiceCollection services)
{ {
// dynamically register module scoped services
var implementationTypes = assembly.GetInterfaces<IService>(); var implementationTypes = assembly.GetInterfaces<IService>();
foreach (var implementationType in implementationTypes) foreach (var implementationType in implementationTypes)
{ {
@ -141,7 +225,7 @@ namespace Oqtane.Client
var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName); var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName);
var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value; var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value;
var localizationService = serviceProvider.GetRequiredService<ILocalizationService>(); var localizationService = serviceProvider.GetRequiredService<ILocalizationService>();
var cultures = await localizationService.GetCulturesAsync(); var cultures = await localizationService.GetCulturesAsync(false);
if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))) if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase)))
{ {

View File

@ -121,7 +121,7 @@
<value>Server:</value> <value>Server:</value>
</data> </data>
<data name="Server.HelpText" xml:space="preserve"> <data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value> <value>Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) </value>
</data> </data>
<data name="Database.Text" xml:space="preserve"> <data name="Database.Text" xml:space="preserve">
<value>Database:</value> <value>Database:</value>
@ -133,7 +133,7 @@
<value>Integrated Security:</value> <value>Integrated Security:</value>
</data> </data>
<data name="IntegratedSecurity.HelpText" xml:space="preserve"> <data name="IntegratedSecurity.HelpText" xml:space="preserve">
<value>Select if you want integrated security or not</value> <value>Select if you are using integrated security</value>
</data> </data>
<data name="Uid.Text" xml:space="preserve"> <data name="Uid.Text" xml:space="preserve">
<value>User Id:</value> <value>User Id:</value>
@ -163,7 +163,7 @@
<value>Self Signed</value> <value>Self Signed</value>
</data> </data>
<data name="TrustServerCertificate.HelpText" xml:space="preserve"> <data name="TrustServerCertificate.HelpText" xml:space="preserve">
<value>Specify the type of certificate you are using for encryption</value> <value>Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True.</value>
</data> </data>
<data name="TrustServerCertificate.Text" xml:space="preserve"> <data name="TrustServerCertificate.Text" xml:space="preserve">
<value>Trust Server Certificate:</value> <value>Trust Server Certificate:</value>

View File

@ -135,10 +135,10 @@
<data name="Message.Require.DbInfo" xml:space="preserve"> <data name="Message.Require.DbInfo" xml:space="preserve">
<value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value> <value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value>
</data> </data>
<data name="Message.Password.Invalid" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements.</value> <value>The Password Provided Does Not Meet The Complexity Policy. Passwords Must Be At Least 6 Characters In Length And Contain Uppercase, Lowercase, Numeric, And Punctuation Characters.</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">
@ -168,4 +168,16 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
<data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value>
</data>
<data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value>
</data>
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
</root> </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="EntityName.HelpText" xml:space="preserve">
<value>The Name Of The Entity</value>
</data>
<data name="EntityName.Text" xml:space="preserve">
<value>Entity:</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="Entity" xml:space="preserve">
<value>Entity</value>
</data>
<data name="Permissions" xml:space="preserve">
<value>Permissions</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -118,6 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Error.Module.Load" xml:space="preserve"> <data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}</value> <value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data> </data>
</root> </root>

View File

@ -192,4 +192,7 @@
<data name="Once" xml:space="preserve"> <data name="Once" xml:space="preserve">
<value>Execute Once</value> <value>Execute Once</value>
</data> </data>
<data name="Message.NoJobs" xml:space="preserve">
<value>Please Note That After An Initial Installation You Must &amp;lt;a href={0}&amp;gt;Restart&amp;lt;/a&amp;gt; The Application In Order To Activate The Default Scheduled Jobs.</value>
</data>
</root> </root>

View File

@ -117,50 +117,35 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="The Only Supported Culture That Has Been Defined Is English" xml:space="preserve">
<value>The Only Supported Culture That Has Been Defined Is English</value>
</data>
<data name="Error.Language.Add" xml:space="preserve"> <data name="Error.Language.Add" xml:space="preserve">
<value>Error Adding Language</value> <value>Error Adding Language</value>
</data> </data>
<data name="Translated.HelpText" xml:space="preserve">
<value>Specify If You Wish To Select Languages That Have Translations Installed</value>
</data>
<data name="Name.HelpText" xml:space="preserve"> <data name="Name.HelpText" xml:space="preserve">
<value>Name Of The Langauage</value> <value>Name Of The Langauage</value>
</data> </data>
<data name="IsDefault.HelpText" xml:space="preserve"> <data name="IsDefault.HelpText" xml:space="preserve">
<value>Indicates Whether Or Not This Language Is The Default For The Site</value> <value>Indicates Whether Or Not This Language Is The Default For The Site</value>
</data> </data>
<data name="Translated.Text" xml:space="preserve">
<value>Translated?</value>
</data>
<data name="Name.Text" xml:space="preserve"> <data name="Name.Text" xml:space="preserve">
<value>Name:</value> <value>Name:</value>
</data> </data>
<data name="IsDefault.Text" xml:space="preserve"> <data name="IsDefault.Text" xml:space="preserve">
<value>Default?</value> <value>Default?</value>
</data> </data>
<data name="AllLanguages" xml:space="preserve">
<value>All The Installed Languages Have Been Added.</value>
</data>
<data name="Error.Language.Download" xml:space="preserve">
<value>Error Downloading Translation</value>
</data>
<data name="OnlyEnglish" xml:space="preserve">
<value>The Only Installed Language Is English</value>
</data>
<data name="Success.Language.Download" xml:space="preserve">
<value>Translation Downloaded Successfully. Click Install To Complete Installation.</value>
</data>
<data name="Success.Language.Install" xml:space="preserve"> <data name="Success.Language.Install" xml:space="preserve">
<value>Translations Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value> <value>Translations Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data> </data>
<data name="Search.NoResults" xml:space="preserve">
<value>No Translations Match The Criteria Provided Or Package Service Is Disabled</value>
</data>
<data name="Download.Heading" xml:space="preserve">
<value>Download</value>
</data>
<data name="LanguageUpload.HelpText" xml:space="preserve"> <data name="LanguageUpload.HelpText" xml:space="preserve">
<value>Upload one or more translations. Once they are uploaded click Install to complete the installation.</value> <value>Upload one or more translation packages. Once they are uploaded click Install to complete the installation.</value>
</data> </data>
<data name="LanguageUpload.Text" xml:space="preserve"> <data name="LanguageUpload.Text" xml:space="preserve">
<value>Upload Language</value> <value>Translation</value>
</data> </data>
<data name="Manage.Heading" xml:space="preserve"> <data name="Manage.Heading" xml:space="preserve">
<value>Manage</value> <value>Manage</value>

View File

@ -144,4 +144,7 @@
<data name="DeleteLanguage.Text" xml:space="preserve"> <data name="DeleteLanguage.Text" xml:space="preserve">
<value>Delete</value> <value>Delete</value>
</data> </data>
<data name="Translation" xml:space="preserve">
<value>Translation</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -195,4 +195,28 @@
<data name="Information.Text" xml:space="preserve"> <data name="Information.Text" xml:space="preserve">
<value>Information</value> <value>Information</value>
</data> </data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
</data>
<data name="Error.Translation.Download" xml:space="preserve">
<value>Error Downloading Translation</value>
</data>
<data name="Search.PackageNameMissing" xml:space="preserve">
<value>A Package Name Was Not Provided For The Module</value>
</data>
<data name="Search.NoResults" xml:space="preserve">
<value>No Translations Exist For This Module Or Package Service Is Disabled</value>
</data>
<data name="Success.Translation.Download" xml:space="preserve">
<value>Translation Downloaded Successfully. Click Install To Complete Installation.</value>
</data>
<data name="Success.Translation.Install" xml:space="preserve">
<value>Translation Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data>
<data name="Translations.Heading" xml:space="preserve">
<value>Translations</value>
</data>
</root> </root>

View File

@ -147,4 +147,7 @@
<data name="Message.Required.Title" xml:space="preserve"> <data name="Message.Required.Title" xml:space="preserve">
<value>You Must Provide A Title For The Module</value> <value>You Must Provide A Title For The Module</value>
</data> </data>
<data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -160,7 +160,7 @@
<value>Error Loading Pane Layouts For Theme</value> <value>Error Loading Pane Layouts For Theme</value>
</data> </data>
<data name="Message.Page.Exists" xml:space="preserve"> <data name="Message.Page.Exists" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page. The page path needs to be unique for the selected parent.</value> <value>A page with path '{0}' already exists for this site. Page paths must be unique. You may need to check if a page with this path exists in the Recycle Bin.</value>
</data> </data>
<data name="Message.Required.PageInfo" xml:space="preserve"> <data name="Message.Required.PageInfo" xml:space="preserve">
<value>You Must Provide Page Name, Theme, and Container</value> <value>You Must Provide Page Name, Theme, and Container</value>
@ -228,13 +228,13 @@
<data name="Appearance.Name" xml:space="preserve"> <data name="Appearance.Name" xml:space="preserve">
<value>Appearance</value> <value>Appearance</value>
</data> </data>
<data name="Message.Page.Deleted" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page in the Recycle Bin. Either recover the page or remove from the Recycle Bin and create it again.</value>
</data>
<data name="Meta.HelpText" xml:space="preserve"> <data name="Meta.HelpText" xml:space="preserve">
<value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value> <value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value>
</data> </data>
<data name="Meta.Text" xml:space="preserve"> <data name="Meta.Text" xml:space="preserve">
<value>Meta:</value> <value>Meta:</value>
</data> </data>
<data name="Message.Page.Reserved" xml:space="preserve">
<value>The page name {0} is reserved. Please enter a different name for your page.</value>
</data>
</root> </root>

View File

@ -151,7 +151,7 @@
<value>Error Loading Pane Layouts For Theme</value> <value>Error Loading Pane Layouts For Theme</value>
</data> </data>
<data name="Mesage.Page.PathExists" xml:space="preserve"> <data name="Mesage.Page.PathExists" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page. The page path needs to be unique for the selected parent.</value> <value>A page with path '{0}' already exists for this site. Page paths must be unique. You may need to check if a page with this path exists in the Recycle Bin.</value>
</data> </data>
<data name="Message.Required.PageInfo" xml:space="preserve"> <data name="Message.Required.PageInfo" xml:space="preserve">
<value>You Must Provide Page Name, Theme, and Container</value> <value>You Must Provide Page Name, Theme, and Container</value>
@ -270,4 +270,7 @@
<data name="Meta.Text" xml:space="preserve"> <data name="Meta.Text" xml:space="preserve">
<value>Meta:</value> <value>Meta:</value>
</data> </data>
<data name="Message.Page.Reserved" xml:space="preserve">
<value>The page name {0} is reserved. Please enter a different name for your page.</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -166,16 +166,22 @@
<value>Error Permanently Deleting Modules</value> <value>Error Permanently Deleting Modules</value>
</data> </data>
<data name="DeleteAllPages.Header" xml:space="preserve"> <data name="DeleteAllPages.Header" xml:space="preserve">
<value>Delete All Pages</value> <value>Remove All Deleted Pages</value>
</data> </data>
<data name="DeleteAllPages.Message" xml:space="preserve"> <data name="DeleteAllPages.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete All Pages?</value> <value>Are You Sure You Wish To Permanently Remove All Deleted Pages?</value>
</data>
<data name="DeleteAllPages.Text" xml:space="preserve">
<value>Remove All Deleted Pages</value>
</data> </data>
<data name="DeleteAllModules.Header" xml:space="preserve"> <data name="DeleteAllModules.Header" xml:space="preserve">
<value>Delete All Modules</value> <value>Remove All Deleted Modules</value>
</data> </data>
<data name="DeleteAllModules.Message" xml:space="preserve"> <data name="DeleteAllModules.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete All Modules?</value> <value>Are You Sure You Wish To Permanently Remove All Deleted Modules?</value>
</data>
<data name="DeleteAllModules.Text" xml:space="preserve">
<value>Remove All Deleted Modules</value>
</data> </data>
<data name="Pages.Heading" xml:space="preserve"> <data name="Pages.Heading" xml:space="preserve">
<value>Pages</value> <value>Pages</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -121,7 +121,7 @@
<value>User: </value> <value>User: </value>
</data> </data>
<data name="User.Select" xml:space="preserve"> <data name="User.Select" xml:space="preserve">
<value>Select User</value> <value>Enter User's Name</value>
</data> </data>
<data name="Users" xml:space="preserve"> <data name="Users" xml:space="preserve">
<value>Users</value> <value>Users</value>
@ -129,9 +129,6 @@
<data name="Error.User.Load" xml:space="preserve"> <data name="Error.User.Load" xml:space="preserve">
<value>Error Loading Users</value> <value>Error Loading Users</value>
</data> </data>
<data name="Error.User.LoadRole" xml:space="preserve">
<value>Error Loading User Roles</value>
</data>
<data name="Success.User.AssignedRole" xml:space="preserve"> <data name="Success.User.AssignedRole" xml:space="preserve">
<value>User Assigned To Role</value> <value>User Assigned To Role</value>
</data> </data>
@ -151,7 +148,7 @@
<value>The role you are assigning users to</value> <value>The role you are assigning users to</value>
</data> </data>
<data name="User.HelpText" xml:space="preserve"> <data name="User.HelpText" xml:space="preserve">
<value>Select a user</value> <value>Enter the name of a user</value>
</data> </data>
<data name="EffectiveDate.HelpText" xml:space="preserve"> <data name="EffectiveDate.HelpText" xml:space="preserve">
<value>The date that this role assignment is active</value> <value>The date that this role assignment is active</value>

View File

@ -166,7 +166,7 @@
<value>Enter the tenant for the site</value> <value>Enter the tenant for the site</value>
</data> </data>
<data name="Aliases.HelpText" xml:space="preserve"> <data name="Aliases.HelpText" xml:space="preserve">
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas.</value> <value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder).</value>
</data> </data>
<data name="IsDeleted.HelpText" xml:space="preserve"> <data name="IsDeleted.HelpText" xml:space="preserve">
<value>Is this site deleted?</value> <value>Is this site deleted?</value>
@ -195,13 +195,13 @@
<data name="UseSsl.HelpText" xml:space="preserve"> <data name="UseSsl.HelpText" xml:space="preserve">
<value>Specify if SSL is required for your SMTP server</value> <value>Specify if SSL is required for your SMTP server</value>
</data> </data>
<data name="SmptUsername.HelpText" xml:space="preserve"> <data name="SmtpUsername.HelpText" xml:space="preserve">
<value>Enter the username for your SMTP account</value> <value>Enter the username for your SMTP account</value>
</data> </data>
<data name="SmtpPassword.HelpText" xml:space="preserve"> <data name="SmtpPassword.HelpText" xml:space="preserve">
<value>Enter the password for your SMTP account</value> <value>Enter the password for your SMTP account</value>
</data> </data>
<data name="SmptSender.HelpText" xml:space="preserve"> <data name="SmtpSender.HelpText" xml:space="preserve">
<value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value> <value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value>
</data> </data>
<data name="EnablePWA.HelpText" xml:space="preserve"> <data name="EnablePWA.HelpText" xml:space="preserve">
@ -243,13 +243,13 @@
<data name="UseSsl.Text" xml:space="preserve"> <data name="UseSsl.Text" xml:space="preserve">
<value>SSL Enabled: </value> <value>SSL Enabled: </value>
</data> </data>
<data name="SmptUsername.Text" xml:space="preserve"> <data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value> <value>Username: </value>
</data> </data>
<data name="SmtpPassword.Text" xml:space="preserve"> <data name="SmtpPassword.Text" xml:space="preserve">
<value>Password: </value> <value>Password: </value>
</data> </data>
<data name="SmptSender.Text" xml:space="preserve"> <data name="SmtpSender.Text" xml:space="preserve">
<value>Email Sender: </value> <value>Email Sender: </value>
</data> </data>
<data name="EnablePWA.Text" xml:space="preserve"> <data name="EnablePWA.Text" xml:space="preserve">
@ -333,4 +333,16 @@
<data name="Confirm.Alias.Delete" xml:space="preserve"> <data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value> <value>Are You Sure You Wish To Delete {0}?</value>
</data> </data>
<data name="HomePage.HelpText" xml:space="preserve">
<value>Select the home page for the site (to be used if there is no page with a path of '/')</value>
</data>
<data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value>
</data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<value>Relay Configured?</value>
</data>
</root> </root>

View File

@ -270,4 +270,16 @@
<data name="Runtime.Text" xml:space="preserve"> <data name="Runtime.Text" xml:space="preserve">
<value>Runtime: </value> <value>Runtime: </value>
</data> </data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
<data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value>
</data>
<data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value>
</data>
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -133,10 +133,10 @@
<value>No Results Returned</value> <value>No Results Returned</value>
</data> </data>
<data name="Tenant.HelpText" xml:space="preserve"> <data name="Tenant.HelpText" xml:space="preserve">
<value>Select the tenant for the SQL server</value> <value>Select the tenant associated with the database server</value>
</data> </data>
<data name="SqlQuery.HelpText" xml:space="preserve"> <data name="SqlQuery.HelpText" xml:space="preserve">
<value>Enter the query for the SQL server</value> <value>Enter the SQL query for the database server</value>
</data> </data>
<data name="SqlQuery.Text" xml:space="preserve"> <data name="SqlQuery.Text" xml:space="preserve">
<value>SQL Query: </value> <value>SQL Query: </value>

View File

@ -210,7 +210,10 @@
<data name="Options.Heading" xml:space="preserve"> <data name="Options.Heading" xml:space="preserve">
<value>Options</value> <value>Options</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Log.Heading" xml:space="preserve">
<value>Log</value>
</data>
<data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
</data> </data>
<data name="Success.Register" xml:space="preserve"> <data name="Success.Register" xml:space="preserve">
@ -270,4 +273,16 @@
<data name="WorkingSet.Text" xml:space="preserve"> <data name="WorkingSet.Text" xml:space="preserve">
<value>Memory Allocation:</value> <value>Memory Allocation:</value>
</data> </data>
<data name="Environment.HelpText" xml:space="preserve">
<value>Environment Name</value>
</data>
<data name="Environment.Text" xml:space="preserve">
<value>Environment:</value>
</data>
<data name="Log.Text" xml:space="preserve">
<value>Log:</value>
</data>
<data name="Log.HelpText" xml:space="preserve">
<value>System log information for current day</value>
</data>
</root> </root>

View File

@ -162,4 +162,10 @@
<data name="License.HelpText" xml:space="preserve"> <data name="License.HelpText" xml:space="preserve">
<value>The license of the theme</value> <value>The license of the theme</value>
</data> </data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
</data>
</root> </root>

View File

@ -120,6 +120,9 @@
<data name="Message.Password.Invalid" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>Passwords Entered Do Not Match</value> <value>Passwords Entered Do Not Match</value>
</data> </data>
<data name="Message.Password.Complexity" xml:space="preserve">
<value>Password Provided Does Not Meet The Complexity Policy</value>
</data>
<data name="From" xml:space="preserve"> <data name="From" xml:space="preserve">
<value>From</value> <value>From</value>
</data> </data>

View File

@ -120,6 +120,9 @@
<data name="Message.Password.NoMatch" xml:space="preserve"> <data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value> <value>Passwords Entered Do Not Match</value>
</data> </data>
<data name="Message.Password.Complexity" xml:space="preserve">
<value>Password Provided Does Not Meet The Complexity Policy</value>
</data>
<data name="Identity.Name" xml:space="preserve"> <data name="Identity.Name" xml:space="preserve">
<value>Identity</value> <value>Identity</value>
</data> </data>

View File

@ -211,25 +211,25 @@
<value>Allow Login?</value> <value>Allow Login?</value>
</data> </data>
<data name="Authority.HelpText" xml:space="preserve"> <data name="Authority.HelpText" xml:space="preserve">
<value>The Authority Url or Issuer Url associated with the OpenID Connect provider</value> <value>The authority url or issuer url associated with the identity provider</value>
</data> </data>
<data name="Authority.Text" xml:space="preserve"> <data name="Authority.Text" xml:space="preserve">
<value>Authority:</value> <value>Authority:</value>
</data> </data>
<data name="AuthorizationUrl.HelpText" xml:space="preserve"> <data name="AuthorizationUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Authorization Code</value> <value>The endpoint for obtaining an authorization code</value>
</data> </data>
<data name="AuthorizationUrl.Text" xml:space="preserve"> <data name="AuthorizationUrl.Text" xml:space="preserve">
<value>Authorization Url:</value> <value>Authorization Url:</value>
</data> </data>
<data name="ClientID.HelpText" xml:space="preserve"> <data name="ClientID.HelpText" xml:space="preserve">
<value>The Client ID from the provider</value> <value>The client id for the identity provider</value>
</data> </data>
<data name="ClientID.Text" xml:space="preserve"> <data name="ClientID.Text" xml:space="preserve">
<value>Client ID:</value> <value>Client ID:</value>
</data> </data>
<data name="ClientSecret.HelpText" xml:space="preserve"> <data name="ClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret from the provider</value> <value>The client secret for the identity provider</value>
</data> </data>
<data name="ClientSecret.Text" xml:space="preserve"> <data name="ClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value> <value>Client Secret:</value>
@ -247,7 +247,7 @@
<value>Domain Filter:</value> <value>Domain Filter:</value>
</data> </data>
<data name="EmailClaimType.HelpText" xml:space="preserve"> <data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The name of the email address claim provided by the provider</value> <value>The name of the email address claim provided by the identity provider</value>
</data> </data>
<data name="EmailClaimType.Text" xml:space="preserve"> <data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim:</value> <value>Email Claim:</value>
@ -259,7 +259,7 @@
<value>Lockout Settings</value> <value>Lockout Settings</value>
</data> </data>
<data name="MetadataUrl.HelpText" xml:space="preserve"> <data name="MetadataUrl.HelpText" xml:space="preserve">
<value>The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)</value> <value>The discovery endpoint for obtaining metadata for this identity provider. Only specify if the identity provider does not use the standard approach (ie. /.well-known/openid-configuration)</value>
</data> </data>
<data name="MetadataUrl.Text" xml:space="preserve"> <data name="MetadataUrl.Text" xml:space="preserve">
<value>Metadata Url:</value> <value>Metadata Url:</value>
@ -268,7 +268,7 @@
<value>Password Settings</value> <value>Password Settings</value>
</data> </data>
<data name="PKCE.HelpText" xml:space="preserve"> <data name="PKCE.HelpText" xml:space="preserve">
<value>Indicate if the provider supports Proof Key for Code Exchange (PKCE)</value> <value>Indicate if the identity provider supports proof key for code exchange (PKCE)</value>
</data> </data>
<data name="PKCE.Text" xml:space="preserve"> <data name="PKCE.Text" xml:space="preserve">
<value>Use PKCE?</value> <value>Use PKCE?</value>
@ -286,25 +286,25 @@
<value>Provider Type:</value> <value>Provider Type:</value>
</data> </data>
<data name="RedirectUrl.HelpText" xml:space="preserve"> <data name="RedirectUrl.HelpText" xml:space="preserve">
<value>The Redirect Url (or Callback Url) which usually needs to be registered with the provider</value> <value>The redirect url (or callback url) which usually needs to be registered with the identity provider</value>
</data> </data>
<data name="RedirectUrl.Text" xml:space="preserve"> <data name="RedirectUrl.Text" xml:space="preserve">
<value>Redirect Url:</value> <value>Redirect Url:</value>
</data> </data>
<data name="Scopes.HelpText" xml:space="preserve"> <data name="Scopes.HelpText" xml:space="preserve">
<value>A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default.</value> <value>A list of scopes to request from the identity provider (separated by commas). If none are specified, standard Scopes will be used by default.</value>
</data> </data>
<data name="Scopes.Text" xml:space="preserve"> <data name="Scopes.Text" xml:space="preserve">
<value>Scopes:</value> <value>Scopes:</value>
</data> </data>
<data name="TokenUrl.HelpText" xml:space="preserve"> <data name="TokenUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Auth Token</value> <value>The endpoint for obtaining an auth token</value>
</data> </data>
<data name="TokenUrl.Text" xml:space="preserve"> <data name="TokenUrl.Text" xml:space="preserve">
<value>Token Url:</value> <value>Token Url:</value>
</data> </data>
<data name="UserInfoUrl.HelpText" xml:space="preserve"> <data name="UserInfoUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address.</value> <value>The endpoint for obtaining user information. This should be an API endpoint or page url which contains the users email address.</value>
</data> </data>
<data name="UserInfoUrl.Text" xml:space="preserve"> <data name="UserInfoUrl.Text" xml:space="preserve">
<value>User Info Url:</value> <value>User Info Url:</value>
@ -373,15 +373,21 @@
<value>Last Login</value> <value>Last Login</value>
</data> </data>
<data name="IdentifierClaimType.HelpText" xml:space="preserve"> <data name="IdentifierClaimType.HelpText" xml:space="preserve">
<value>The name of the unique user identifier claim provided by the provider</value> <value>The name of the unique user identifier claim provided by the identity provider</value>
</data> </data>
<data name="IdentifierClaimType.Text" xml:space="preserve"> <data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value> <value>Identifier Claim:</value>
</data> </data>
<data name="Parameters.HelpText" xml:space="preserve"> <data name="Parameters.HelpText" xml:space="preserve">
<value>Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple).</value> <value>Optionally specify any additional parameters as name/value pairs to send to the identity provider (separated by commas if there are multiple).</value>
</data> </data>
<data name="Parameters.Text" xml:space="preserve"> <data name="Parameters.Text" xml:space="preserve">
<value>Parameters:</value> <value>Parameters:</value>
</data> </data>
<data name="RoleClaimType.HelpText" xml:space="preserve">
<value>Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
</data>
<data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim Type:</value>
</data>
</root> </root>

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value> <value>Error Loading Files</value>
</data> </data>
<data name="Error.File.Upload" xml:space="preserve"> <data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed</value> <value>File Upload Failed Or Is Still In Progress</value>
</data> </data>
<data name="Message.File.NotSelected" xml:space="preserve"> <data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value> <value>You Have Not Selected A File To Upload</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -124,9 +124,9 @@
<value>User</value> <value>User</value>
</data> </data>
<data name="Username.Enter" xml:space="preserve"> <data name="Username.Enter" xml:space="preserve">
<value>Enter Username</value> <value>Enter User's Name</value>
</data> </data>
<data name="Message.Username.DontExist" xml:space="preserve"> <data name="Message.Username.DontExist" xml:space="preserve">
<value>Username Does Not Exist</value> <value>User Does Not Exist With Name Specified</value>
</data> </data>
</root> </root>

View File

@ -123,4 +123,10 @@
<data name="AllowFileManagement.Text" xml:space="preserve"> <data name="AllowFileManagement.Text" xml:space="preserve">
<value>Allow File Management: </value> <value>Allow File Management: </value>
</data> </data>
<data name="AllowRawHtml.HelpText" xml:space="preserve">
<value>Specify If Editors Can Enter Raw HTML</value>
</data>
<data name="AllowRawHtml.Text" xml:space="preserve">
<value>Allow Raw HTML:</value>
</data>
</root> </root>

View File

@ -318,6 +318,9 @@
<data name="BlazorWebAssembly" xml:space="preserve"> <data name="BlazorWebAssembly" xml:space="preserve">
<value>Blazor WebAssembly</value> <value>Blazor WebAssembly</value>
</data> </data>
<data name="BlazorHybrid" xml:space="preserve">
<value>Blazor Hybrid</value>
</data>
<data name="Settings" xml:space="preserve"> <data name="Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
@ -336,4 +339,7 @@
<data name="Visitor Management" xml:space="preserve"> <data name="Visitor Management" xml:space="preserve">
<value>Visitor Management</value> <value>Visitor Management</value>
</data> </data>
<data name="Oqtane.Marketplace" xml:space="preserve">
<value>Please note that the third party extensions displayed above have been registered in the &lt;a href="https://www.oqtane.net" target="_new"&gt;Oqtane Marketplace&lt;/a&gt; which enables them to be seamlessly downloaded and installed into the framework.</value>
</data>
</root> </root>

View File

@ -138,6 +138,12 @@
<data name="Register.Text" xml:space="preserve"> <data name="Register.Text" xml:space="preserve">
<value>Show Register?</value> <value>Show Register?</value>
</data> </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>
<data name="Site" xml:space="preserve"> <data name="Site" xml:space="preserve">
<value>Site</value> <value>Site</value>
</data> </data>

View File

@ -2,27 +2,17 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Services namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class FileService : ServiceBase, IFileService public class FileService : ServiceBase, IFileService
{ {
private readonly SiteState _siteState; public FileService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{
_siteState = siteState;
_jsRuntime = jsRuntime;
}
private string Apiurl => CreateApiUrl("File"); private string Apiurl => CreateApiUrl("File");
@ -47,7 +37,7 @@ namespace Oqtane.Services
var path = WebUtility.UrlEncode(folderPath); var path = WebUtility.UrlEncode(folderPath);
List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}"); List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}");
return files.OrderBy(item => item.Name).ToList(); return files?.OrderBy(item => item.Name).ToList();
} }
public async Task<File> GetFileAsync(int fileId) public async Task<File> GetFileAsync(int fileId)
@ -75,54 +65,6 @@ namespace Oqtane.Services
return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}"); return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}");
} }
public async Task<string> UploadFilesAsync(int folderId, string[] files, string id)
{
return await UploadFilesAsync(folderId.ToString(), files, id);
}
public async Task<string> UploadFilesAsync(string folder, string[] files, string id)
{
string result = "";
var interop = new Interop(_jsRuntime);
await interop.UploadFiles($"{Apiurl}/upload", folder, id, _siteState.AntiForgeryToken);
// uploading files is asynchronous so we need to wait for the upload to complete
bool success = false;
int attempts = 0;
while (attempts < 5 && success == false)
{
Thread.Sleep(2000); // wait 2 seconds
result = "";
List<File> fileList = await GetFilesAsync(folder);
if (fileList.Count > 0)
{
success = true;
foreach (string file in files)
{
if (!fileList.Exists(item => item.Name == file))
{
success = false;
result += file + ",";
}
}
}
attempts += 1;
}
await interop.SetElementAttribute(id + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(id + "ProgressBar", "style", "display: none;");
if (!success)
{
result = result.Substring(0, result.Length - 1);
}
return result;
}
public async Task<byte[]> DownloadFileAsync(int fileId) public async Task<byte[]> DownloadFileAsync(int fileId)
{ {
return await GetByteArrayAsync($"{Apiurl}/download/{fileId}"); return await GetByteArrayAsync($"{Apiurl}/download/{fileId}");

View File

@ -1,10 +1,8 @@
using Oqtane.Models; using Oqtane.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Shared; using Oqtane.Shared;
using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using Oqtane.Documentation; using Oqtane.Documentation;
@ -20,9 +18,7 @@ namespace Oqtane.Services
public async Task<List<Folder>> GetFoldersAsync(int siteId) public async Task<List<Folder>> GetFoldersAsync(int siteId)
{ {
List<Folder> folders = await GetJsonAsync<List<Folder>>($"{ApiUrl}?siteid={siteId}"); return await GetJsonAsync<List<Folder>>($"{ApiUrl}?siteid={siteId}");
folders = GetFoldersHierarchy(folders);
return folders;
} }
public async Task<Folder> GetFolderAsync(int folderId) public async Task<Folder> GetFolderAsync(int folderId)
@ -58,48 +54,5 @@ namespace Oqtane.Services
{ {
await DeleteAsync($"{ApiUrl}/{folderId}"); await DeleteAsync($"{ApiUrl}/{folderId}");
} }
private static List<Folder> GetFoldersHierarchy(List<Folder> folders)
{
List<Folder> hierarchy = new List<Folder>();
Action<List<Folder>, Folder> getPath = null;
var folders1 = folders;
getPath = (folderList, folder) =>
{
IEnumerable<Folder> children;
int level;
if (folder == null)
{
level = -1;
children = folders1.Where(item => item.ParentId == null);
}
else
{
level = folder.Level;
children = folders1.Where(item => item.ParentId == folder.FolderId);
}
foreach (Folder child in children)
{
child.Level = level + 1;
child.HasChildren = folders1.Any(item => item.ParentId == child.FolderId);
hierarchy.Add(child);
if (getPath != null) getPath(folderList, child);
}
};
folders = folders.OrderBy(item => item.Order).ToList();
getPath(folders, null);
// add any non-hierarchical items to the end of the list
foreach (Folder folder in folders)
{
if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null)
{
hierarchy.Add(folder);
}
}
return hierarchy;
}
} }
} }

View File

@ -66,27 +66,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<File> UploadFileAsync(string url, int folderId, string name); Task<File> UploadFileAsync(string url, int folderId, string name);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folderId">Target <see cref="Folder"/></param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(int folderId, string[] files, string fileUploadName);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folder">Target <see cref="Folder"/>
/// TODO: todoc verify exactly from where the folder path must start
/// </param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(string folder, string[] files, string fileUploadName);
/// <summary> /// <summary>
/// Get / download a file (the body). /// Get / download a file (the body).
/// </summary> /// </summary>

View File

@ -17,6 +17,14 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<List<Language>> GetLanguagesAsync(int siteId); Task<List<Language>> GetLanguagesAsync(int siteId);
/// <summary>
/// Returns a list of all available languages for the given <see cref="Site" /> and package
/// </summary>
/// <param name="siteId"></param>
/// <param name="packageName"></param>
/// <returns></returns>
Task<List<Language>> GetLanguagesAsync(int siteId, string packageName);
/// <summary> /// <summary>
/// Returns the given language /// Returns the given language
/// </summary> /// </summary>

View File

@ -13,6 +13,6 @@ namespace Oqtane.Services
/// Returns a collection of supported cultures /// Returns a collection of supported cultures
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<Culture>> GetCulturesAsync(); Task<IEnumerable<Culture>> GetCulturesAsync(bool installed);
} }
} }

View File

@ -50,13 +50,13 @@ namespace Oqtane.Services
/// <param name="moduleId"></param> /// <param name="moduleId"></param>
/// <param name="content">module in JSON format</param> /// <param name="content">module in JSON format</param>
/// <returns></returns> /// <returns></returns>
Task<bool> ImportModuleAsync(int moduleId, string content); Task<bool> ImportModuleAsync(int moduleId, int pageId, string content);
/// <summary> /// <summary>
/// Exports a given module /// Exports a given module
/// </summary> /// </summary>
/// <param name="moduleId"></param> /// <param name="moduleId"></param>
/// <returns>module in JSON</returns> /// <returns>module in JSON</returns>
Task<string> ExportModuleAsync(int moduleId); Task<string> ExportModuleAsync(int moduleId, int pageId);
} }
} }

View File

@ -164,6 +164,24 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId); Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId);
/// <summary>
/// Returns a specific setting
/// </summary>
/// <param name="entityName"></param>
/// <param name="entityId"></param>
/// <param name="settingName"></param>
/// <returns></returns>
Task DeleteSettingAsync(string entityName, int entityId, string settingName);
/// <summary>
/// Returns a specific setting
/// </summary>
/// <param name="entityName"></param>
/// <param name="entityId"></param>
/// <param name="settingName"></param>
/// <returns></returns>
Task<List<Setting>> GetSettingsAsync(string entityName, int entityId, string settingName);
/// <summary> /// <summary>
/// Returns a specific setting /// Returns a specific setting
/// </summary> /// </summary>

View File

@ -54,8 +54,10 @@ namespace Oqtane.Services
/// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled. /// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled.
/// </summary> /// </summary>
/// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param> /// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param>
/// <param name="setCookie">Determines if the login cookie should be set (only relevant for Hybrid scenarios)</param>
/// <param name="isPersistent">Determines if the login cookie should be persisted for a long time.</param>
/// <returns></returns> /// <returns></returns>
Task<User> LoginUserAsync(User user); Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent);
/// <summary> /// <summary>
/// Logout a <see cref="User"/> /// Logout a <see cref="User"/>

View File

@ -17,18 +17,27 @@ namespace Oqtane.Services
public async Task<List<Language>> GetLanguagesAsync(int siteId) public async Task<List<Language>> GetLanguagesAsync(int siteId)
{ {
var languages = await GetJsonAsync<List<Language>>($"{Apiurl}?siteid={siteId}"); return await GetLanguagesAsync(siteId, "");
}
return languages?.OrderBy(l => l.Name).ToList() ?? Enumerable.Empty<Language>().ToList(); public async Task<List<Language>> GetLanguagesAsync(int siteId, string packageName)
{
return await GetJsonAsync<List<Language>>($"{Apiurl}?siteid={siteId}&packagename={packageName}");
} }
public async Task<Language> GetLanguageAsync(int languageId) public async Task<Language> GetLanguageAsync(int languageId)
=> await GetJsonAsync<Language>($"{Apiurl}/{languageId}"); {
return await GetJsonAsync<Language>($"{Apiurl}/{languageId}");
}
public async Task<Language> AddLanguageAsync(Language language) public async Task<Language> AddLanguageAsync(Language language)
=> await PostJsonAsync<Language>(Apiurl, language); {
return await PostJsonAsync<Language>(Apiurl, language);
}
public async Task DeleteLanguageAsync(int languageId) public async Task DeleteLanguageAsync(int languageId)
=> await DeleteAsync($"{Apiurl}/{languageId}"); {
await DeleteAsync($"{Apiurl}/{languageId}");
}
} }
} }

View File

@ -14,6 +14,6 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("Localization"); private string Apiurl => CreateApiUrl("Localization");
public async Task<IEnumerable<Culture>> GetCulturesAsync() => await GetJsonAsync<IEnumerable<Culture>>(Apiurl); public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed) => await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
} }
} }

View File

@ -5,6 +5,7 @@ using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.Modules.Controls;
namespace Oqtane.Services namespace Oqtane.Services
{ {
@ -44,14 +45,14 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{moduleId.ToString()}"); await DeleteAsync($"{Apiurl}/{moduleId.ToString()}");
} }
public async Task<bool> ImportModuleAsync(int moduleId, string content) public async Task<bool> ImportModuleAsync(int moduleId, int pageId, string content)
{ {
return await PostJsonAsync<string,bool>($"{Apiurl}/import?moduleid={moduleId}", content); return await PostJsonAsync<string,bool>($"{Apiurl}/import?moduleid={moduleId}&pageid={pageId}", content);
} }
public async Task<string> ExportModuleAsync(int moduleId) public async Task<string> ExportModuleAsync(int moduleId, int pageId)
{ {
return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}"); return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}");
} }
} }
} }

View File

@ -1,10 +1,8 @@
using Oqtane.Models; using Oqtane.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Shared; using Oqtane.Shared;
using System;
using System.Net; using System.Net;
using Oqtane.Documentation; using Oqtane.Documentation;
@ -19,9 +17,7 @@ namespace Oqtane.Services
public async Task<List<Page>> GetPagesAsync(int siteId) public async Task<List<Page>> GetPagesAsync(int siteId)
{ {
List<Page> pages = await GetJsonAsync<List<Page>>($"{Apiurl}?siteid={siteId}"); return await GetJsonAsync<List<Page>>($"{Apiurl}?siteid={siteId}");
pages = GetPagesHierarchy(pages);
return pages;
} }
public async Task<Page> GetPageAsync(int pageId) public async Task<Page> GetPageAsync(int pageId)
@ -73,45 +69,5 @@ namespace Oqtane.Services
{ {
await DeleteAsync($"{Apiurl}/{pageId}"); await DeleteAsync($"{Apiurl}/{pageId}");
} }
private static List<Page> GetPagesHierarchy(List<Page> pages)
{
List<Page> hierarchy = new List<Page>();
Action<List<Page>, Page> getPath = null;
getPath = (pageList, page) =>
{
IEnumerable<Page> children;
int level;
if (page == null)
{
level = -1;
children = pages.Where(item => item.ParentId == null);
}
else
{
level = page.Level;
children = pages.Where(item => item.ParentId == page.PageId);
}
foreach (Page child in children)
{
child.Level = level + 1;
child.HasChildren = pages.Any(item => item.ParentId == child.PageId);
hierarchy.Add(child);
getPath(pageList, child);
}
};
pages = pages.OrderBy(item => item.Order).ToList();
getPath(pages, null);
// add any non-hierarchical items to the end of the list
foreach (Page page in pages)
{
if (hierarchy.Find(item => item.PageId == page.PageId) == null)
{
hierarchy.Add(page);
}
}
return hierarchy;
}
} }
} }

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