Skip to content

Commit 87f7783

Browse files
Invitations + Basic Team Management
* add team context, team invitation and user invitations * add functionality - remove team member, leave team, invite user, remove invitation, accept/decline invitation * add seogemdl2 icons * refactoring (correct messaging usage, useless code ...)
1 parent 68a8bf8 commit 87f7783

File tree

15 files changed

+490
-26
lines changed

15 files changed

+490
-26
lines changed

Frontend/TeamUp.ApiLayer/ApiClient.TeamManagement.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
using RailwayResult;
1+
using System.Collections.Generic;
22

3+
using RailwayResult;
4+
5+
using TeamUp.Contracts.Invitations;
36
using TeamUp.Contracts.Teams;
47

58
namespace TeamUp.ApiLayer;
@@ -15,6 +18,24 @@ public Task<Result<TeamId>> CreateTeamAsync(CreateTeamRequest request, Cancellat
1518
public Task<Result> DeleteTeamAsync(TeamId teamId, CancellationToken ct) =>
1619
SendAsync(HttpMethod.Delete, $"/api/v1/teams/{teamId.Value}", ct);
1720

21+
public Task<Result> RemoveTeamMemberAsync(TeamId teamId, TeamMemberId memberId, CancellationToken ct) =>
22+
SendAsync(HttpMethod.Delete, $"/api/v1/teams/{teamId.Value}/members/{memberId.Value}", ct);
23+
1824
public Task<Result<TeamResponse>> GetTeamAsync(TeamId teamId, CancellationToken ct) =>
1925
SendAsync<TeamResponse>(HttpMethod.Get, $"/api/v1/teams/{teamId.Value}", ct);
26+
27+
public Task<Result<List<InvitationResponse>>> GetMyInvitationsAsync(CancellationToken ct) =>
28+
SendAsync<List<InvitationResponse>>(HttpMethod.Get, "/api/v1/invitations", ct);
29+
30+
public Task<Result<List<TeamInvitationResponse>>> GetTeamInvitationsAsync(TeamId teamId, CancellationToken ct) =>
31+
SendAsync<List<TeamInvitationResponse>>(HttpMethod.Get, $"/api/v1/invitations/teams/{teamId.Value}", ct);
32+
33+
public Task<Result> AcceptInvitationAsync(InvitationId invitationId, CancellationToken ct) =>
34+
SendAsync(HttpMethod.Post, $"/api/v1/invitations/{invitationId.Value}/accept", ct);
35+
36+
public Task<Result> RemoveInvitationAsync(InvitationId invitationId, CancellationToken ct) =>
37+
SendAsync(HttpMethod.Delete, $"/api/v1/invitations/{invitationId.Value}", ct);
38+
39+
public Task<Result> InviteUserAsync(InviteUserRequest request, CancellationToken ct) =>
40+
SendAsync(HttpMethod.Post, "/api/v1/invitations", request, ct);
2041
}

Frontend/TeamUp.ApiLayer/ApiClient.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ private async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(HttpMethod
8080
return await SendRequestAsync<TResponse>(request, ct);
8181
}
8282

83+
private Task<Result> SendAsync<TRequest>(HttpMethod method, string uri, TRequest payload, CancellationToken ct) =>
84+
SendAsync<TRequest>(method, uri, payload, null, ct);
85+
86+
private async Task<Result> SendAsync<TRequest>(HttpMethod method, string uri, TRequest? payload, Action<HttpRequestMessage>? configure, CancellationToken ct)
87+
{
88+
var json = JsonSerializer.Serialize(payload);
89+
var request = new HttpRequestMessage
90+
{
91+
Method = method,
92+
RequestUri = new Uri(uri, UriKind.Relative),
93+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
94+
};
95+
96+
if (configure is not null)
97+
{
98+
configure(request);
99+
}
100+
101+
return await SendRequestAsync(request, ct);
102+
}
103+
83104
private async Task<Result> SendRequestAsync(HttpRequestMessage request, CancellationToken ct)
84105
{
85106
await InjectAuthToken(request, ct);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Security.Claims;
2+
3+
using Microsoft.AspNetCore.Components;
4+
using Microsoft.AspNetCore.Components.Authorization;
5+
using Microsoft.AspNetCore.Components.Rendering;
6+
using Microsoft.Extensions.Logging;
7+
8+
using TeamUp.Contracts.Teams;
9+
using TeamUp.Contracts.Users;
10+
11+
namespace TeamUp.Client.Components;
12+
13+
public sealed class TeamContext : ComponentBase
14+
{
15+
[Parameter]
16+
[EditorRequired]
17+
public TeamResponse Team { get; set; } = null!;
18+
19+
[Parameter]
20+
[EditorRequired]
21+
public Guid TeamGuid { get; set; } = default!;
22+
23+
public TeamId TeamId { get; private set; } = null!;
24+
25+
public TeamRole Role => Member?.Role ?? TeamRole.Member;
26+
27+
public string Nickname => Member?.Nickname ?? "No Nickname";
28+
29+
public TeamMemberResponse? Member { get; private set; }
30+
31+
[Parameter]
32+
public RenderFragment<TeamContext>? ChildContent { get; set; }
33+
34+
[Inject]
35+
private AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!;
36+
37+
protected override async Task OnParametersSetAsync()
38+
{
39+
TeamId = TeamId.FromGuid(TeamGuid);
40+
41+
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
42+
var userIdString = state.User.Claims.First(claim => claim.Type == ClaimTypes.NameIdentifier).Value;
43+
var userId = UserId.FromGuid(new Guid(userIdString));
44+
45+
Member = Team.Members.First(member => member.UserId == userId);
46+
}
47+
48+
protected override void BuildRenderTree(RenderTreeBuilder builder)
49+
{
50+
builder.OpenRegion(GetHashCode());
51+
52+
builder.OpenComponent<CascadingValue<TeamContext>>(0);
53+
builder.AddComponentParameter(1, "Value", this);
54+
builder.AddComponentParameter(2, "ChildContent", ChildContent?.Invoke(this));
55+
builder.CloseComponent();
56+
57+
builder.CloseRegion();
58+
}
59+
}

Frontend/TeamUp.Client/Layout/NavMenu.razor

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,26 @@
5757

5858
protected override async Task OnInitializedAsync()
5959
{
60-
Messenger.Register<TeamCreatedMessage>(this, TeamCreatedHandler);
61-
Messenger.Register<TeamDeletedMessage>(this, TeamDeletedHandler);
60+
Messenger.Register<NavMenu, TeamCreatedMessage>(this, TeamCreatedHandler);
61+
Messenger.Register<NavMenu, TeamDeletedMessage>(this, TeamDeletedHandler);
62+
Messenger.Register<NavMenu, RefreshTeamsMessage>(this, async (sender, _) => await LoadTeamsAsync());
6263

64+
await LoadTeamsAsync();
65+
}
66+
67+
private async Task LoadTeamsAsync()
68+
{
6369
var teamsResult = await ApiClient.GetMyTeamsAsync(CTS.Token);
6470
if (teamsResult.IsSuccess)
6571
{
6672
teams = teamsResult.Value;
6773
}
74+
else if (teams is null)
75+
{
76+
teams = [];
77+
}
78+
79+
StateHasChanged();
6880
}
6981

7082
private string? GetInitials(string? name)
@@ -87,36 +99,36 @@
8799
};
88100
}
89101

90-
private void TeamCreatedHandler(object self, TeamCreatedMessage message)
102+
private void TeamCreatedHandler(NavMenu self, TeamCreatedMessage message)
91103
{
92-
if (self is not NavMenu menu || menu.teams is null)
104+
if (self.teams is null)
93105
{
94106
return;
95107
}
96108

97-
menu.teams.Add(new TeamSlimResponse
109+
self.teams.Add(new TeamSlimResponse
98110
{
99111
TeamId = message.TeamId,
100112
Name = message.Name
101113
});
102114

103-
menu.StateHasChanged();
115+
self.StateHasChanged();
104116
}
105117

106-
private void TeamDeletedHandler(object self, TeamDeletedMessage message)
118+
private void TeamDeletedHandler(NavMenu self, TeamDeletedMessage message)
107119
{
108-
if (self is not NavMenu menu || menu.teams is null)
120+
if (self.teams is null)
109121
{
110122
return;
111123
}
112124

113-
var deletedTeam = menu.teams.Find(team => team.TeamId == message.TeamId);
125+
var deletedTeam = self.teams.Find(team => team.TeamId == message.TeamId);
114126
if (deletedTeam is null)
115127
{
116128
return;
117129
}
118130

119-
menu.teams.Remove(deletedTeam);
120-
menu.StateHasChanged();
131+
self.teams.Remove(deletedTeam);
132+
self.StateHasChanged();
121133
}
122134
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace TeamUp.Client.Messages;
2+
3+
public sealed record RefreshTeamsMessage;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
@rendermode InteractiveAuto
2+
3+
@inherits CancellableComponent
4+
5+
@using TeamUp.Client.Pages.Panels
6+
7+
@inject ApiClient ApiClient
8+
@inject IToastService ToastService
9+
@inject IDialogService DialogService
10+
11+
<div class="d-flex align-center w-100" style="margin-left: 3px">
12+
<h4 class="padding-small no-margin">Invitations</h4>
13+
<FluentButton Appearance="Appearance.Outline" OnClick="LoadInvitationsAsync">
14+
<span class="icon">&#59180;</span>
15+
</FluentButton>
16+
<FluentSpacer />
17+
<FluentButton Appearance="Appearance.Accent" OnClick="InviteUserAsync">Invite User</FluentButton>
18+
</div>
19+
@if (invitations is not null)
20+
{
21+
<FluentDataGrid Items="@(invitations.AsQueryable())">
22+
<PropertyColumn Property="@(p => p.Email)" Title="Email" Class="content-center" />
23+
<PropertyColumn Property="@(p => p.CreatedUtc)" Title="Created At" Class="content-center" />
24+
<TemplateColumn Title="Action" Class="content-center">
25+
<FluentButton IconStart="@(new Icons.Filled.Size20.Delete())" Appearance="Appearance.Accent" OnClick="@(() => DeleteInvitationAsync(context))" />
26+
</TemplateColumn>
27+
</FluentDataGrid>
28+
}
29+
30+
@code
31+
{
32+
[CascadingParameter]
33+
public TeamContext TeamContext { get; set; } = null!;
34+
35+
private List<TeamInvitationResponse>? invitations;
36+
37+
private IDialogReference? _dialog;
38+
39+
private EmailInput emailInput = new()
40+
{
41+
Email = ""
42+
};
43+
44+
protected override async Task OnInitializedAsync()
45+
{
46+
await LoadInvitationsAsync();
47+
}
48+
49+
private async Task LoadInvitationsAsync()
50+
{
51+
var result = await ApiClient.GetTeamInvitationsAsync(TeamContext.TeamId, CTS.Token);
52+
if (result.IsSuccess)
53+
{
54+
invitations = result.Value;
55+
}
56+
else
57+
{
58+
ToastService.ShowError(result.Error.Message);
59+
}
60+
}
61+
62+
public async Task DeleteInvitationAsync(TeamInvitationResponse invitation)
63+
{
64+
var confirm = await DialogService.ShowConfirmationAsync($"Do you want to remove the invitation for {invitation.Email}?");
65+
var confirmResult = await confirm.Result;
66+
if (confirmResult.Cancelled)
67+
{
68+
return;
69+
}
70+
71+
var result = await ApiClient.RemoveInvitationAsync(invitation.Id, CTS.Token);
72+
if (result.IsFailure)
73+
{
74+
ToastService.ShowError(result.Error.Message);
75+
return;
76+
}
77+
78+
invitations!.Remove(invitation);
79+
ToastService.ShowWarning($"Invitation for user {invitation.Email} has been removed.");
80+
}
81+
82+
public async Task InviteUserAsync()
83+
{
84+
_dialog = await DialogService.ShowPanelAsync<InviteUserPanel>(emailInput, new DialogParameters<EmailInput>()
85+
{
86+
Content = emailInput,
87+
Alignment = HorizontalAlignment.Right,
88+
Title = $"Invite User",
89+
PrimaryAction = "Invite",
90+
SecondaryAction = "Cancel",
91+
});
92+
93+
var result = await _dialog.Result;
94+
95+
if (result.Cancelled)
96+
{
97+
return;
98+
}
99+
100+
var invitationResult = await ApiClient.InviteUserAsync(new InviteUserRequest
101+
{
102+
TeamId = TeamContext.TeamId,
103+
Email = emailInput.Email
104+
}, CTS.Token);
105+
106+
if (invitationResult.IsFailure)
107+
{
108+
ToastService.ShowError(invitationResult.Error.Message);
109+
return;
110+
}
111+
112+
ToastService.ShowInfo("The invitation request was accepted for processing, invitation should be created in upcoming minutes.");
113+
await LoadInvitationsAsync();
114+
}
115+
}

Frontend/TeamUp.Client/Pages/CreateTeam.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
return;
5252
}
5353

54-
ToastService.ShowSuccess("Team successfully created.");
54+
ToastService.ShowSuccess("Team has been successfully created.");
5555
Messenger.Send(new TeamCreatedMessage
5656
{
5757
TeamId = result.Value,

0 commit comments

Comments
 (0)