Skip to content

Commit 98e88c2

Browse files
committed
add PrivilegeForm
1 parent 970da9a commit 98e88c2

20 files changed

+2255
-157
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,53 @@ These components automatically:
743743
- Enable/disable based on update permissions
744744
- Show/hide based on read permissions
745745

746+
#### PrivilegeForm Component
747+
748+
The `PrivilegeForm` component extends the standard `EditForm` to provide privilege-aware form functionality. It automatically cascades privilege form state to child components, allowing you to set default privilege settings at the form level while maintaining the ability for individual components to override specific values.
749+
750+
```razor
751+
@* Basic form with default privilege settings *@
752+
<PrivilegeForm Model="@postModel" Subject="Post" ReadAction="read" UpdateAction="update">
753+
<DataAnnotationsValidator />
754+
<ValidationSummary />
755+
756+
@* These inputs inherit the Subject, ReadAction, and UpdateAction from the form *@
757+
<PrivilegeInputText @bind-Value="@postModel.Title" Field="title" />
758+
<PrivilegeInputTextArea @bind-Value="@postModel.Content" Field="content" />
759+
<PrivilegeInputSelect @bind-Value="@postModel.Status" Field="status">
760+
<option value="draft">Draft</option>
761+
<option value="published">Published</option>
762+
</PrivilegeInputSelect>
763+
764+
@* This input overrides the default Subject *@
765+
<PrivilegeInputText @bind-Value="@postModel.AuthorEmail"
766+
Subject="User"
767+
Field="email" />
768+
769+
<button type="submit" class="btn btn-primary">Save Post</button>
770+
</PrivilegeForm>
771+
772+
@* Form with different actions for different operations *@
773+
<PrivilegeForm Model="@userModel" Subject="User" ReadAction="view" UpdateAction="edit">
774+
<PrivilegeInputText @bind-Value="@userModel.FirstName" Field="firstName" />
775+
<PrivilegeInputText @bind-Value="@userModel.LastName" Field="lastName" />
776+
777+
@* Admin-only field with different permissions *@
778+
<PrivilegeInputText @bind-Value="@userModel.Role"
779+
Subject="User"
780+
Field="role"
781+
ReadAction="viewRole"
782+
UpdateAction="editRole" />
783+
</PrivilegeForm>
784+
```
785+
786+
Key benefits of `PrivilegeForm`:
787+
788+
- **Cascading Defaults**: Set privilege parameters once at the form level instead of repeating them on every input
789+
- **Flexible Overrides**: Individual components can override any of the cascaded values when needed
790+
- **Standard EditForm**: Maintains all functionality of the base `EditForm` component including validation
791+
- **Clean Markup**: Reduces repetitive code and makes forms easier to maintain
792+
746793
### PrivilegeInputText HTML Output Example
747794

748795
Below is an example of the `PrivilegeInputText` component and its corresponding HTML output for various states based on the `PrivilegeContext` results:

src/Privileged.Components/PrivilegeButton.cs

Lines changed: 231 additions & 6 deletions
Large diffs are not rendered by default.

src/Privileged.Components/PrivilegeContextView.cs

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,71 @@ namespace Privileged.Components;
1717
/// </para>
1818
/// <para>
1919
/// Content rendering precedence:
20+
/// </para>
2021
/// <list type="number">
21-
/// <item>If the privilege context is <c>null</c> (loading): <see cref="Loading"/> template is rendered.</item>
22-
/// <item>If the privilege context is loaded and <see cref="Loaded"/> is specified: <see cref="Loaded"/> template is rendered.</item>
23-
/// <item>If the privilege context is loaded and <see cref="Loaded"/> is not specified: <see cref="ChildContent"/> is rendered.</item>
22+
/// <item><description>If the privilege context is <c>null</c> (loading): <see cref="Loading"/> template is rendered.</description></item>
23+
/// <item><description>If the privilege context is loaded and <see cref="Loaded"/> is specified: <see cref="Loaded"/> template is rendered.</description></item>
24+
/// <item><description>If the privilege context is loaded and <see cref="Loaded"/> is not specified: <see cref="ChildContent"/> is rendered.</description></item>
2425
/// </list>
25-
/// </para>
2626
/// <para>
2727
/// The component provides the loaded <see cref="PrivilegeContext"/> as a cascading value to all child components,
2828
/// enabling privilege-aware components throughout the component tree.
2929
/// </para>
3030
/// </remarks>
31+
/// <example>
32+
/// <code>
33+
/// &lt;PrivilegeContextView&gt;
34+
/// &lt;Loading&gt;
35+
/// &lt;div class="spinner"&gt;Loading permissions...&lt;/div&gt;
36+
/// &lt;/Loading&gt;
37+
/// &lt;Loaded&gt;
38+
/// &lt;PrivilegeForm Model="userModel" Subject="User"&gt;
39+
/// &lt;PrivilegeInputText @bind-Value="userModel.Name" /&gt;
40+
/// &lt;PrivilegeInputCheckbox @bind-Value="userModel.IsActive" /&gt;
41+
/// &lt;/PrivilegeForm&gt;
42+
/// &lt;/Loaded&gt;
43+
/// &lt;/PrivilegeContextView&gt;
44+
///
45+
/// &lt;!-- Simplified usage with ChildContent --&gt;
46+
/// &lt;PrivilegeContextView&gt;
47+
/// &lt;PrivilegeInputText @bind-Value="model.SecretField" Subject="Document" Field="Secret" /&gt;
48+
/// &lt;/PrivilegeContextView&gt;
49+
/// </code>
50+
/// </example>
3151
/// <seealso cref="PrivilegeContext"/>
3252
/// <seealso cref="IPrivilegeContextProvider"/>
3353
/// <seealso cref="AuthenticationStateProvider"/>
3454
public class PrivilegeContextView : ComponentBase
3555
{
3656
/// <summary>
3757
/// Gets or sets the provider for retrieving the privilege context.
58+
/// This service is automatically injected and must be registered in the dependency injection container.
3859
/// </summary>
3960
/// <value>
4061
/// An <see cref="IPrivilegeContextProvider"/> instance used to asynchronously load the privilege context.
4162
/// This service should be registered in the dependency injection container.
4263
/// </value>
64+
/// <remarks>
65+
/// The provider is responsible for loading user-specific privilege rules and aliases, typically from
66+
/// a database, cache, or external authorization service. The implementation should handle user identity
67+
/// and return appropriate privilege contexts based on the authenticated user's roles and permissions.
68+
/// </remarks>
4369
[Inject]
4470
protected IPrivilegeContextProvider PrivilegeContextProvider { get; set; } = default!;
4571

4672
/// <summary>
47-
/// Gets or sets the authentication state provider.
73+
/// Gets or sets the authentication state provider used to retrieve the current user's authentication information.
74+
/// This service is automatically injected from the Blazor authentication system.
4875
/// </summary>
4976
/// <value>
5077
/// An <see cref="AuthenticationStateProvider"/> instance used to retrieve the current user's authentication state.
5178
/// This is passed to the privilege context provider to load user-specific privileges.
5279
/// </value>
80+
/// <remarks>
81+
/// The authentication state contains the current user's <see cref="System.Security.Claims.ClaimsPrincipal"/>,
82+
/// which is used by the privilege context provider to determine what privileges should be loaded.
83+
/// This enables user-specific privilege evaluation based on identity, roles, and claims.
84+
/// </remarks>
5385
[Inject]
5486
protected AuthenticationStateProvider AuthenticationStateProvider { get; set; } = default!;
5587

@@ -60,16 +92,50 @@ public class PrivilegeContextView : ComponentBase
6092
/// A <see cref="RenderFragment"/> that defines the content to show during the loading state.
6193
/// If not specified, a default "Loading ..." message will be displayed.
6294
/// </value>
95+
/// <remarks>
96+
/// This template is rendered when the component is initializing and waiting for the privilege context
97+
/// to be loaded from the <see cref="IPrivilegeContextProvider"/>. It provides an opportunity to show
98+
/// loading indicators, spinners, or placeholder content to improve user experience during async operations.
99+
/// </remarks>
100+
/// <example>
101+
/// <code>
102+
/// &lt;Loading&gt;
103+
/// &lt;div class="d-flex justify-content-center"&gt;
104+
/// &lt;div class="spinner-border" role="status"&gt;
105+
/// &lt;span class="sr-only"&gt;Loading permissions...&lt;/span&gt;
106+
/// &lt;/div&gt;
107+
/// &lt;/div&gt;
108+
/// &lt;/Loading&gt;
109+
/// </code>
110+
/// </example>
63111
[Parameter]
64112
public RenderFragment? Loading { get; set; } = builder => builder.AddMarkupContent(0, "Loading ...");
65113

66114
/// <summary>
67-
/// Gets or sets the render fragment to display when the privilege context is loaded.
115+
/// Gets or sets the render fragment to display when the privilege context has been successfully loaded.
68116
/// </summary>
69117
/// <value>
70118
/// A <see cref="RenderFragment"/> that defines the content to show when the privilege context
71119
/// has been successfully loaded. This takes precedence over <see cref="ChildContent"/> when specified.
72120
/// </value>
121+
/// <remarks>
122+
/// This template is rendered after the privilege context has been successfully loaded and is available
123+
/// as a cascading value. When both <see cref="Loaded"/> and <see cref="ChildContent"/> are specified,
124+
/// the <see cref="Loaded"/> template takes precedence. This allows for different content structures
125+
/// based on the loading state.
126+
/// </remarks>
127+
/// <example>
128+
/// <code>
129+
/// &lt;Loaded&gt;
130+
/// &lt;div class="privilege-protected-content"&gt;
131+
/// &lt;h2&gt;Secure Dashboard&lt;/h2&gt;
132+
/// &lt;PrivilegeForm Model="dashboardModel"&gt;
133+
/// &lt;!-- Privilege-aware form content --&gt;
134+
/// &lt;/PrivilegeForm&gt;
135+
/// &lt;/div&gt;
136+
/// &lt;/Loaded&gt;
137+
/// </code>
138+
/// </example>
73139
[Parameter]
74140
public RenderFragment? Loaded { get; set; }
75141

@@ -80,6 +146,21 @@ public class PrivilegeContextView : ComponentBase
80146
/// A <see cref="RenderFragment"/> that defines the default content to render when the privilege context
81147
/// is loaded and <see cref="Loaded"/> is not specified.
82148
/// </value>
149+
/// <remarks>
150+
/// This is the default content template that is rendered when the privilege context is successfully loaded
151+
/// and no explicit <see cref="Loaded"/> template is provided. All content within this fragment will have
152+
/// access to the cascaded <see cref="PrivilegeContext"/> for privilege evaluation.
153+
/// </remarks>
154+
/// <example>
155+
/// <code>
156+
/// &lt;PrivilegeContextView&gt;
157+
/// &lt;div class="user-profile"&gt;
158+
/// &lt;PrivilegeInputText @bind-Value="user.Name" Subject="User" Field="Name" /&gt;
159+
/// &lt;PrivilegeInputText @bind-Value="user.Email" Subject="User" Field="Email" /&gt;
160+
/// &lt;/div&gt;
161+
/// &lt;/PrivilegeContextView&gt;
162+
/// </code>
163+
/// </example>
83164
[Parameter]
84165
public RenderFragment? ChildContent { get; set; }
85166

@@ -90,6 +171,12 @@ public class PrivilegeContextView : ComponentBase
90171
/// The loaded <see cref="PrivilegeContext"/> instance, or <c>null</c> if the context is still loading.
91172
/// This value is set asynchronously during component initialization.
92173
/// </value>
174+
/// <remarks>
175+
/// This property holds the privilege context that is loaded from the <see cref="IPrivilegeContextProvider"/>
176+
/// during component initialization. Once loaded, it is cascaded to all child components, enabling
177+
/// privilege-aware behavior throughout the component tree. The loading state can be determined by
178+
/// checking if this property is <c>null</c>.
179+
/// </remarks>
93180
protected PrivilegeContext? PrivilegeContext { get; set; }
94181

95182
/// <summary>
@@ -102,16 +189,16 @@ public class PrivilegeContextView : ComponentBase
102189
/// <remarks>
103190
/// <para>
104191
/// The rendering logic follows this precedence:
192+
/// </para>
105193
/// <list type="number">
106-
/// <item>Wraps all content in a <see cref="CascadingValue{TValue}"/> with the current <see cref="PrivilegeContext"/>.</item>
107-
/// <item>If <see cref="PrivilegeContext"/> is <c>null</c>: Renders <see cref="Loading"/> content.</item>
108-
/// <item>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is specified: Renders <see cref="Loaded"/> content.</item>
109-
/// <item>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is not specified: Renders <see cref="ChildContent"/>.</item>
194+
/// <item><description>Wraps all content in a <see cref="CascadingValue{TValue}"/> with the current <see cref="PrivilegeContext"/>.</description></item>
195+
/// <item><description>If <see cref="PrivilegeContext"/> is <c>null</c>: Renders <see cref="Loading"/> content.</description></item>
196+
/// <item><description>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is specified: Renders <see cref="Loaded"/> content.</description></item>
197+
/// <item><description>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is not specified: Renders <see cref="ChildContent"/>.</description></item>
110198
/// </list>
111-
/// </para>
112199
/// <para>
113200
/// The cascading value enables any child components that declare a <c>[CascadingParameter] PrivilegeContext</c>
114-
/// to automatically receive the loaded privilege context.
201+
/// to automatically receive the loaded privilege context without requiring explicit parameter passing.
115202
/// </para>
116203
/// </remarks>
117204
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -127,6 +214,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
127214
/// This method is called once when the component is first initialized.
128215
/// </summary>
129216
/// <returns>A task that represents the asynchronous initialization operation.</returns>
217+
/// <remarks>
218+
/// <para>
219+
/// This method performs the following operations:
220+
/// </para>
221+
/// <list type="number">
222+
/// <item><description>Retrieves the current authentication state from the <see cref="AuthenticationStateProvider"/></description></item>
223+
/// <item><description>Extracts the <see cref="System.Security.Claims.ClaimsPrincipal"/> from the authentication state</description></item>
224+
/// <item><description>Calls the <see cref="IPrivilegeContextProvider"/> to load the privilege context for the authenticated user</description></item>
225+
/// <item><description>Sets the <see cref="PrivilegeContext"/> property with the loaded context</description></item>
226+
/// <item><description>Triggers a component re-render by calling <see cref="ComponentBase.StateHasChanged"/></description></item>
227+
/// </list>
228+
/// <para>
229+
/// The re-render after loading ensures that the UI transitions from the loading state to the loaded state,
230+
/// displaying the appropriate content template and making the privilege context available to child components.
231+
/// </para>
232+
/// </remarks>
130233
protected override async Task OnInitializedAsync()
131234
{
132235
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
@@ -141,6 +244,20 @@ protected override async Task OnInitializedAsync()
141244
/// A <see cref="RenderFragment"/> that renders the appropriate content based on whether the
142245
/// <see cref="PrivilegeContext"/> is loaded and which templates are available.
143246
/// </returns>
247+
/// <remarks>
248+
/// <para>
249+
/// This method implements the content selection logic for the component:
250+
/// </para>
251+
/// <list type="bullet">
252+
/// <item><description><strong>Loading state:</strong> When <see cref="PrivilegeContext"/> is <c>null</c>, renders the <see cref="Loading"/> template</description></item>
253+
/// <item><description><strong>Loaded with explicit template:</strong> When <see cref="PrivilegeContext"/> is available and <see cref="Loaded"/> is specified, renders the <see cref="Loaded"/> template</description></item>
254+
/// <item><description><strong>Loaded with default content:</strong> When <see cref="PrivilegeContext"/> is available and no <see cref="Loaded"/> template is specified, renders the <see cref="ChildContent"/></description></item>
255+
/// </list>
256+
/// <para>
257+
/// The returned render fragment is executed within the context of the cascading value, ensuring that
258+
/// all rendered content has access to the privilege context for authorization decisions.
259+
/// </para>
260+
/// </remarks>
144261
private RenderFragment BuildChildContent()
145262
{
146263
return childBuilder =>

0 commit comments

Comments
 (0)