Skip to content

Commit 7b26d4d

Browse files
Team Management (#4)
* add team management UI and logic (change nickname, change ownership, change team role) * change refresh buttons to use Fluent UI icons * fix team contract to include event types * refactoring (component separation, naming, leftover files, etc.)
1 parent 44654dd commit 7b26d4d

27 files changed

+604
-191
lines changed

src/TeamUp.Client/Layout/NavMenu.razor

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
<div class="d-flex align-center" style="margin: 2px">
3131
<span>Teams</span>
3232
<FluentSpacer />
33-
<FluentButton Appearance="Appearance.Outline" OnClick="() => LoadTeamsAsync(true)" Style="margin-left: 4px; height: 29px">
34-
<span class="icon-small">&#59180;</span>
35-
</FluentButton>
33+
<FluentButton IconStart="@(new Icons.Regular.Size16.ArrowClockwise())" Appearance="Appearance.Outline" OnClick="() => LoadTeamsAsync(true)" Style="margin-left: 4px;" />
3634
</div>
3735
</TitleTemplate>
3836
<ChildContent>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@rendermode InteractiveAuto
2+
3+
@inherits CancellableComponent
4+
5+
<h4 class="padding-small no-margin" style="margin-left: 3px">Event Types</h4>
6+
<FluentDataGrid Items="@(TeamContext.Team.EventTypes.AsQueryable())">
7+
<PropertyColumn IsDefaultSortColumn="true" Property="@(p => p.Name)" Title="Name" Class="content-center" />
8+
<PropertyColumn Property="@(p => p.Description)" Title="Description" Class="content-center" />
9+
@if (TeamContext.Role.CanManipulateEventTypes())
10+
{
11+
<TemplateColumn Title="Action" Class="content-center" Align="Align.Center">
12+
<FluentButton IconStart="@(new Icons.Filled.Size20.Delete())" Appearance="Appearance.Accent" />
13+
<FluentButton IconStart="@(new Icons.Filled.Size20.Edit())" Appearance="Appearance.Accent" />
14+
</TemplateColumn>
15+
}
16+
</FluentDataGrid>
17+
18+
@code
19+
{
20+
[CascadingParameter]
21+
public TeamContext TeamContext { get; set; } = null!;
22+
}

src/TeamUp.Client/Pages/Components/TeamInvitationsComponent.razor

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
@rendermode InteractiveAuto
1+
@using TeamUp.Client.Pages.Panels
22

3-
@inherits CancellableComponent
3+
@rendermode InteractiveAuto
44

5-
@using TeamUp.Client.Pages.Panels
5+
@inherits CancellableComponent
66

77
@inject IToastService ToastService
88
@inject IDialogService DialogService
99
@inject InvitationsService InvitationService
1010

1111
<div class="d-flex align-center w-100" style="margin-left: 3px">
1212
<h4 class="padding-small no-margin">Invitations</h4>
13-
<FluentButton Appearance="Appearance.Outline" OnClick="() => LoadInvitationsAsync(true)">
14-
<span class="icon">&#59180;</span>
15-
</FluentButton>
13+
<FluentButton IconStart="@(new Icons.Regular.Size20.ArrowClockwise())" Appearance="Appearance.Outline" OnClick="() => LoadInvitationsAsync(true)" />
1614
<FluentSpacer />
1715
<FluentButton Appearance="Appearance.Accent" OnClick="InviteUserAsync">Invite User</FluentButton>
1816
</div>
@@ -34,7 +32,7 @@
3432

3533
private List<TeamInvitationResponse>? invitations;
3634

37-
private IDialogReference? _dialog;
35+
private IDialogReference? dialog;
3836

3937
private EmailInput emailInput = new()
4038
{
@@ -81,7 +79,7 @@
8179

8280
public async Task InviteUserAsync()
8381
{
84-
_dialog = await DialogService.ShowPanelAsync<InviteUserPanel>(emailInput, new DialogParameters<EmailInput>()
82+
dialog = await DialogService.ShowPanelAsync<InviteUserPanel>(emailInput, new DialogParameters<EmailInput>()
8583
{
8684
Content = emailInput,
8785
Alignment = HorizontalAlignment.Right,
@@ -90,7 +88,7 @@
9088
SecondaryAction = "Cancel",
9189
});
9290

93-
var result = await _dialog.Result;
91+
var result = await dialog.Result;
9492

9593
if (result.Cancelled)
9694
{
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
@using TeamUp.Client.Pages.Panels
2+
3+
@rendermode InteractiveAuto
4+
5+
@inherits CancellableComponent
6+
7+
@inject NavigationManager NavigationManager
8+
@inject IDialogService DialogService
9+
@inject IToastService ToastService
10+
@inject IMessenger Messenger
11+
@inject TeamService TeamService
12+
13+
<div class="d-flex flex-dir-row align-center">
14+
<div>
15+
Nickname:
16+
<strong style="margin-left: 4px;">@TeamContext.Nickname</strong>
17+
</div>
18+
<FluentButton IconStart="@(new Icons.Filled.Size20.Edit())" Appearance="Appearance.Outline" OnClick="@ChangeNicknameAsync" Style="margin-left: 8px;" />
19+
</div>
20+
<div>
21+
Role:
22+
<strong style="margin-left: 4px;">@TeamContext.Role.ToString()</strong>
23+
</div>
24+
25+
<FluentDivider Style="margin-top: 8px; margin-bottom: 8px" />
26+
27+
@if (TeamContext.Role.IsOwner())
28+
{
29+
<FluentStack Style="margin-top: 8px">
30+
<FluentButton IconStart="@(new Icons.Filled.Size20.Delete())" Appearance="Appearance.Accent" OnClick="@DeleteTeamAsync">Delete Team</FluentButton>
31+
<FluentButton Appearance="Appearance.Accent" OnClick="@ChangeOwnershipAsync">Change Ownership</FluentButton>
32+
</FluentStack>
33+
}
34+
else
35+
{
36+
<div style="margin-top: 8px">
37+
<FluentButton Appearance="Appearance.Accent" OnClick="() => LeaveTeamAsync(TeamContext.Member!.Id)">Leave Team</FluentButton>
38+
</div>
39+
}
40+
41+
@code
42+
{
43+
[CascadingParameter]
44+
public TeamContext TeamContext { get; set; } = null!;
45+
46+
private IDialogReference? dialog;
47+
48+
private ChangeNicknameInput nicknameInput = new()
49+
{
50+
Nickname = ""
51+
};
52+
53+
private ChangeOwnershipInput ownershipInput = new();
54+
55+
public async Task LeaveTeamAsync(TeamMemberId memberId)
56+
{
57+
var confirm = await DialogService.ShowConfirmationAsync("Do you want to leave the team?", "Yes", "No");
58+
if ((await confirm.Result).Cancelled == true)
59+
{
60+
return;
61+
}
62+
63+
var result = await TeamService.RemoveTeamMemberAsync(TeamContext.TeamId, memberId, CTS.Token);
64+
if (result.IsFailure)
65+
{
66+
ToastService.ShowError(result.Error.Message);
67+
return;
68+
}
69+
70+
ToastService.ShowSuccess($"You have left the {TeamContext.Team.Name}.");
71+
Messenger.Send(new TeamDeletedMessage
72+
{
73+
TeamId = TeamContext.TeamId
74+
});
75+
NavigationManager.NavigateTo("/events");
76+
}
77+
78+
79+
public async Task DeleteTeamAsync()
80+
{
81+
var confirm = await DialogService.ShowConfirmationAsync("Do you want to delete the team?", "Yes", "No");
82+
if ((await confirm.Result).Cancelled == true)
83+
{
84+
return;
85+
}
86+
87+
var result = await TeamService.DeleteTeamAsync(TeamContext.TeamId, CTS.Token);
88+
if (result.IsFailure)
89+
{
90+
ToastService.ShowError(result.Error.Message);
91+
return;
92+
}
93+
94+
ToastService.ShowSuccess("Team has been successfully deleted.");
95+
NavigationManager.NavigateTo("/events");
96+
}
97+
98+
public async Task ChangeNicknameAsync()
99+
{
100+
dialog = await DialogService.ShowPanelAsync<ChangeNicknamePanel>(nicknameInput, new DialogParameters<ChangeNicknameInput>()
101+
{
102+
Content = nicknameInput,
103+
Alignment = HorizontalAlignment.Right,
104+
Title = $"Change Nickname",
105+
PrimaryAction = "Change",
106+
SecondaryAction = "Cancel",
107+
});
108+
109+
if ((await dialog.Result).Cancelled)
110+
{
111+
nicknameInput.Nickname = "";
112+
nicknameInput.Errors = null;
113+
return;
114+
}
115+
116+
var changeResult = await TeamService.ChangeNicknameAsync(TeamContext.TeamId, new ChangeNicknameRequest
117+
{
118+
Nickname = nicknameInput.Nickname
119+
}, CTS.Token);
120+
121+
if (changeResult.IsFailure)
122+
{
123+
ToastService.ShowError(changeResult.Error.Message);
124+
125+
if (changeResult.Error is ApiValidationError error)
126+
{
127+
nicknameInput.Errors = error.Errors;
128+
}
129+
130+
return;
131+
}
132+
133+
nicknameInput.Nickname = "";
134+
nicknameInput.Errors = null;
135+
136+
ToastService.ShowSuccess("Nickname has been successfully changed.");
137+
}
138+
139+
public async Task ChangeOwnershipAsync()
140+
{
141+
ownershipInput.Members = TeamContext.Team.Members;
142+
143+
dialog = await DialogService.ShowPanelAsync<ChangeOwnershipPanel>(ownershipInput, new DialogParameters<ChangeOwnershipInput>()
144+
{
145+
Content = ownershipInput,
146+
Alignment = HorizontalAlignment.Right,
147+
Title = $"Change Nickname",
148+
PrimaryAction = "Change",
149+
SecondaryAction = "Cancel",
150+
});
151+
152+
if ((await dialog.Result).Cancelled)
153+
{
154+
ownershipInput.SelectedMember = [];
155+
return;
156+
}
157+
158+
if (ownershipInput.SelectedMember.Count() == 0)
159+
{
160+
ToastService.ShowError("Member has to be selected.");
161+
return;
162+
}
163+
164+
var changeResult = await TeamService.ChangeOwnershipAsync(TeamContext.TeamId, ownershipInput.SelectedMember.First().Id, CTS.Token);
165+
166+
if (changeResult.IsFailure)
167+
{
168+
ToastService.ShowError(changeResult.Error.Message);
169+
return;
170+
}
171+
172+
ownershipInput.SelectedMember = [];
173+
ToastService.ShowSuccess("Ownership has been transferred.");
174+
}
175+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
@using TeamUp.Client.Pages.Panels
2+
3+
@rendermode InteractiveAuto
4+
5+
@inherits CancellableComponent
6+
7+
@inject NavigationManager NavigationManager
8+
@inject IDialogService DialogService
9+
@inject IToastService ToastService
10+
@inject IMessenger Messenger
11+
@inject TeamService TeamService
12+
13+
<h4 class="padding-small no-margin" style="margin-left: 3px">Members</h4>
14+
<FluentDataGrid Items="@(TeamContext.Team.Members.AsQueryable())">
15+
<PropertyColumn Sortable="true" Property="@(p => p.Nickname)" Title="Nickname" Class="content-center" />
16+
<PropertyColumn Sortable="true" IsDefaultSortColumn="true" Property="@(p => p.Role.ToString())" Title="Team Role" Class="content-center" />
17+
@if (TeamContext.Role.CanRemoveTeamMembers() || TeamContext.Role.CanUpdateTeamRoles())
18+
{
19+
<TemplateColumn Title="Action" Class="content-center" Align="Align.Center">
20+
@if (TeamContext.Role.CanRemoveTeamMembers() && !context.Role.IsOwner())
21+
{
22+
<FluentButton IconStart="@(new Icons.Filled.Size20.Delete())" Appearance="Appearance.Accent" OnClick="() => RemoveTeamMemberAsync(context)" />
23+
}
24+
25+
@if (TeamContext.Role.CanUpdateTeamRoles() && !context.Role.IsOwner())
26+
{
27+
<FluentButton IconStart="@(new Icons.Filled.Size20.Edit())" Appearance="Appearance.Accent" OnClick="() => ChangeTeamRoleAsync(context)" />
28+
}
29+
</TemplateColumn>
30+
}
31+
</FluentDataGrid>
32+
33+
@code
34+
{
35+
[CascadingParameter]
36+
public TeamContext TeamContext { get; set; } = null!;
37+
38+
private IDialogReference? dialog;
39+
40+
private ChangeTeamRoleInput teamRoleInput = new()
41+
{
42+
Role = TeamRole.Member
43+
};
44+
45+
public async Task RemoveTeamMemberAsync(TeamMemberResponse member)
46+
{
47+
var confirm = await DialogService.ShowConfirmationAsync($"Do you want to remove {member.Nickname} from the team?", "Yes", "No");
48+
if ((await confirm.Result).Cancelled == true)
49+
{
50+
return;
51+
}
52+
53+
var result = await TeamService.RemoveTeamMemberAsync(TeamContext.TeamId, member.Id, CTS.Token);
54+
if (result.IsFailure)
55+
{
56+
ToastService.ShowError(result.Error.Message);
57+
return;
58+
}
59+
60+
ToastService.ShowWarning($"{member.Nickname} has been removed from the team.");
61+
}
62+
63+
public async Task ChangeTeamRoleAsync(TeamMemberResponse member)
64+
{
65+
teamRoleInput.Role = member.Role;
66+
teamRoleInput.TargetMember = member.Nickname;
67+
68+
dialog = await DialogService.ShowPanelAsync<ChangeTeamRolePanel>(teamRoleInput, new DialogParameters<ChangeTeamRoleInput>()
69+
{
70+
Content = teamRoleInput,
71+
Alignment = HorizontalAlignment.Right,
72+
Title = $"Change Team Role",
73+
PrimaryAction = "Change",
74+
SecondaryAction = "Cancel",
75+
});
76+
77+
var result = await dialog.Result;
78+
if (result.Cancelled)
79+
{
80+
teamRoleInput.Reset();
81+
return;
82+
}
83+
84+
var changeResult = await TeamService.ChangeTeamRoleAsync(TeamContext.TeamId, member.Id, new UpdateTeamRoleRequest
85+
{
86+
Role = teamRoleInput.Role
87+
}, CTS.Token);
88+
89+
if (changeResult.IsFailure)
90+
{
91+
ToastService.ShowError(changeResult.Error.Message);
92+
return;
93+
}
94+
95+
teamRoleInput.Reset();
96+
ToastService.ShowSuccess("Team role has been successfully updated.");
97+
}
98+
}

src/TeamUp.Client/Pages/Invitations.razor

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212

1313
<div class="d-flex align-center" style="margin-bottom: 16px">
1414
<h1 class="d-flex no-margin padding-small">Invitations</h1>
15-
<FluentButton Appearance="Appearance.Outline" OnClick="() => LoadInvitationsAsync(true)" Style="margin-top: 4px">
16-
<span class="icon">&#59180;</span>
17-
</FluentButton>
15+
<FluentButton IconStart="@(new Icons.Regular.Size20.ArrowClockwise())" Appearance="Appearance.Outline" OnClick="() => LoadInvitationsAsync(true)" Style="margin-top: 4px" />
1816
</div>
1917

2018
<div class="body-small">
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace TeamUp.Client.Pages.Panels;
4+
5+
public sealed class ChangeNicknameInput
6+
{
7+
[Required]
8+
public required string Nickname { get; set; }
9+
10+
public IDictionary<string, string[]>? Errors { get; set; }
11+
}

0 commit comments

Comments
 (0)