1
0

feat: More stable right now.

This commit is contained in:
Ca2didi 2025-04-02 11:33:13 +08:00
parent 91097940fc
commit ab1ee9925d
27 changed files with 613 additions and 152 deletions

View File

@ -16,6 +16,7 @@
<entry key="Flawless.Client/Views/MainView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/AddRepositoryMemberDialogueView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/CreateRepositoryDialog.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />

View File

@ -32,6 +32,7 @@
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa\1.10.0\lib\net8.0\Ursa.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa.themes.semi\1.10.0\lib\netstandard2.0\Ursa.Themes.Semi.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Cardi\.nuget\packages\reactiveui\20.1.1\lib\net8.0\ReactiveUI.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;

View File

@ -47,6 +47,8 @@ public partial class RepositoryModel : ReactiveModel
[Reactive] private string _username;
[Reactive] private RepositoryRole _role;
[Reactive] private bool _canEdit;
}
public partial class Commit : ReactiveModel

View File

@ -5,20 +5,20 @@ namespace Flawless.Client;
public static class PathUtility
{
public static string GetWorkspacePath(string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo);
public static string GetWorkspacePath(string login, string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo);
public static string GetWorkspaceManagerPath(string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo, AppDefaultValues.RepoLocalStorageManagerFolder);
public static string GetWorkspaceManagerPath(string login, string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo, AppDefaultValues.RepoLocalStorageManagerFolder);
public static string GetWorkspaceDbPath(string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
public static string GetWorkspaceDbPath(string login, string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo,
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDatabaseFile);
public static string GetWorkspaceDepotCachePath(string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
public static string GetWorkspaceDepotCachePath(string login, string owner, string repo)
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo,
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
}

View File

@ -64,7 +64,7 @@ public class LocalFileTreeAccessor
{
_repo = repo;
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
_rootDirectory = PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
}
public void SetBaseline(IEnumerable<WorkspaceFile> baselines)

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Flawless.Client.Models;
#nullable enable annotations
@ -101,16 +102,6 @@ namespace Flawless.Client.Remote
[Get("/api/repo/{userName}/{repositoryName}/get_users")]
Task<RepoUserRoleListingResponse> GetUsers(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/update_user")]
Task UpdateUser(string repositoryName, string userName, [Body] RepoUserRole body);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_user")]
Task DeleteUser(string repositoryName, string userName, [Body] RepoUserRole body);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
@ -170,6 +161,16 @@ namespace Flawless.Client.Remote
[Post("/api/repo_create")]
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName, [Query] string description);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/update_user")]
Task UpdateUser([Query] string repositoryName, [Query] string modUser, [Query] RepositoryModel.RepositoryRole role);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/delete_user")]
Task DeleteUser([Query] string repositoryName, [Query] string delUser);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_info")]

View File

@ -135,9 +135,12 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
if (_cached.TryGetValue(workPath, out var r))
{
using var ws = new FileStream(destinationPath, FileMode.OpenOrCreate, FileAccess.Write);
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
baseStream.CopyTo(ws);
using (var ws = new FileStream(destinationPath, FileMode.OpenOrCreate, FileAccess.Write))
{
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
baseStream.CopyTo(ws);
}
File.SetLastWriteTimeUtc(destinationPath, r.FileInfo.ModifyTime);
return true;
}

View File

@ -7,10 +7,13 @@ using System.Net;
using System.Threading.Tasks;
using Flawless.Abstraction;
using Flawless.Client.Models;
using Flawless.Client.Remote;
using Flawless.Core.BinaryDataFormat;
using Flawless.Core.Modal;
using Newtonsoft.Json;
using Refit;
using CommitManifest = Flawless.Core.Modal.CommitManifest;
using DepotLabel = Flawless.Core.Modal.DepotLabel;
using WorkspaceFile = Flawless.Core.Modal.WorkspaceFile;
namespace Flawless.Client.Service;
@ -26,12 +29,12 @@ public class RepositoryService : BaseService<RepositoryService>
private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo)
{
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
if (File.Exists(dbPath)) return false;
// Get directories
var localRepoDb = GetRepositoryLocalDatabase(repo);
var folderPath = PathUtility.GetWorkspaceManagerPath(repo.OwnerName, repo.Name);
var folderPath = PathUtility.GetWorkspaceManagerPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
// Create initial data.
Directory.CreateDirectory(folderPath);
@ -47,7 +50,7 @@ public class RepositoryService : BaseService<RepositoryService>
var localRepo = GetRepositoryLocalDatabase(repo);
localRepo.LastOprationTime = DateTime.Now;
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.OpenOrCreate));
@ -60,7 +63,7 @@ public class RepositoryService : BaseService<RepositoryService>
{
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
{
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
if (File.Exists(dbPath))
{
// Use existed target
@ -177,13 +180,82 @@ public class RepositoryService : BaseService<RepositoryService>
{
foreach (var repo in _repositories)
{
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
repo.IsDownloaded = isFolderExists && isDbFileExists;
}
return true;
}
public async ValueTask<bool> DeleteMemberFromServerAsync(RepositoryModel repo, RepositoryModel.Member member)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.DeleteUser(repo.OwnerName, member.Username);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> ModifyMemberFromServerAsync(RepositoryModel repo, RepositoryModel.Member member)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.UpdateUser(repo.Name, member.Username, member.Role);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> AddMemberFromServerAsync(RepositoryModel repo, string grantedTo, RepositoryModel.RepositoryRole role)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.UpdateUser(repo.Name, grantedTo, role);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
{
@ -200,6 +272,7 @@ public class RepositoryService : BaseService<RepositoryService>
// Update existed
var dict = members.Result.ToDictionary(m => m.Username);
dict.Add(repo.OwnerName, new RepoUserRole{ Username = repo.OwnerName, Role = RepositoryRole._3});
for (var i = 0; i < repo.Members.Count; i++)
{
var ele = repo.Members[i];
@ -212,15 +285,18 @@ public class RepositoryService : BaseService<RepositoryService>
ele.Username = role.Username;
ele.Role = (RepositoryModel.RepositoryRole) role.Role;
ele.CanEdit = ele.Role != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser;
}
// Add missing
foreach (var role in dict.Values)
{
var rl = (RepositoryModel.RepositoryRole)role.Role;
var r = new RepositoryModel.Member
{
Username = role.Username,
Role = (RepositoryModel.RepositoryRole) role.Role
Role = rl,
CanEdit = rl != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser
};
repo.Members.Add(r);
@ -261,7 +337,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
{
if (!await UpdateRepositoriesDownloadedStatusFromDiskAsync() || repo.IsDownloaded == false) return false;
if (!await UpdateCommitsFromServerAsync(repo)) return false;
if (!await UpdateCommitsHistoryFromServerAsync(repo)) return false;
var ls = GetRepositoryLocalDatabase(repo);
if (ls.CurrentCommit != null)
@ -296,7 +372,7 @@ public class RepositoryService : BaseService<RepositoryService>
// Create basic structures.
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
if (!await UpdateCommitsFromServerAsync(repo)) return false;
if (!await UpdateCommitsHistoryFromServerAsync(repo)) return false;
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
if (peekCommit == null) return false; // Should not use this function!
@ -336,6 +412,70 @@ public class RepositoryService : BaseService<RepositoryService>
await DeleteFromDiskAsync(repo);
return false;
}
SaveRepositoryLocalDatabaseChanges(repo);
repo.IsDownloaded = true;
_openedRepos.Add(repo);
return true;
}
public async ValueTask<bool> ResetCommitPointerToTargetAndMergeDepotsIntoRepositoryFromRemoteAsync
(RepositoryModel repo, Guid commitId)
{
// Try Download base repo info
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
if (accessor == null)
{
await DeleteFromDiskAsync(repo);
return false;
};
// Remember to cache accessor everytime it will being used.
await accessor.CreateCacheAsync();
var ls = GetRepositoryLocalDatabase(repo);
var oldAcceesor = ls.RepoAccessor;
ls.CurrentCommit = commitId;
ls.RepoAccessor = accessor;
ls.LocalAccessor.SetBaseline(accessor);
try
{
foreach (var f in accessor.Manifest.FilePaths)
{
var pfs = WorkPath.ToPlatformPath(f.WorkPath, ls.LocalAccessor.WorkingDirectory);
var directory = Path.GetDirectoryName(pfs);
// Write into fs
if (directory != null) Directory.CreateDirectory(directory);
// todo Check if we need merge at here and add logic to handle that...
if (!accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
throw new InvalidDataException($"Can not write {f.WorkPath} into repository.");
}
}
catch (Exception e)
{
// Revert baseline
try
{
ls.RepoAccessor = oldAcceesor;
if (oldAcceesor != null) ls.LocalAccessor.SetBaseline(oldAcceesor);
else ls.LocalAccessor.SetBaseline([]);
}
catch (Exception exception) { Console.WriteLine(exception); }
UIHelper.NotifyError(e);
Console.WriteLine(e);
await DeleteFromDiskAsync(repo);
return false;
}
// Dispose old RepoAccessor
try { if (oldAcceesor != null) await oldAcceesor.DisposeAsync(); }
catch (Exception exception) { Console.WriteLine(exception); }
SaveRepositoryLocalDatabaseChanges(repo);
repo.IsDownloaded = true;
@ -343,7 +483,7 @@ public class RepositoryService : BaseService<RepositoryService>
return true;
}
public async ValueTask<bool> ShouldUpdateLocalCommitsCacheFromServerAsync(RepositoryModel repo)
public async ValueTask<bool?> IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
@ -358,10 +498,11 @@ public class RepositoryService : BaseService<RepositoryService>
var emptyRepo = repo.Commits.Count == 0;
// If they both empty
if ((rsp.Result == Guid.Empty) == emptyRepo) return false;
if ((rsp.Result == Guid.Empty) && emptyRepo) return false;
if (emptyRepo) return true;
return rsp.Result == repo.Commits.MaxBy(cm => cm.CommittedOn)!.CommitId;
var ptr = GetRepositoryLocalDatabase(repo).CurrentCommit;
return rsp.Result != ptr;
}
catch (Exception e)
{
@ -371,7 +512,7 @@ public class RepositoryService : BaseService<RepositoryService>
}
}
public async ValueTask<bool> UpdateCommitsFromServerAsync(RepositoryModel repo)
public async ValueTask<bool> UpdateCommitsHistoryFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
@ -441,7 +582,7 @@ public class RepositoryService : BaseService<RepositoryService>
if (manifest == null) return null;
// Prepare folders
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
Directory.CreateDirectory(depotsRoot);
// Generate download depots list
@ -502,7 +643,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
(RepositoryModel repo, IEnumerable<(Guid id, Stream stream)> depots)
{
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
foreach (var d in depots)
{
var dst = Path.Combine(depotsRoot, d.id.ToString());
@ -581,7 +722,7 @@ public class RepositoryService : BaseService<RepositoryService>
{
try
{
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
var path = PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
if (Directory.Exists(path)) Directory.Delete(path, true);
}
catch (Exception e)
@ -598,9 +739,20 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
(RepositoryModel repo, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, string message)
{
// Check if current version is the latest
var api = Api.C;
var requireUpdate = await IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(repo);
if (!requireUpdate.HasValue) return null;
if (requireUpdate.Value)
{
await UIHelper.SimpleAlert("Can not commit workspace at this time! Please pull from remote server first...");
return null;
}
var localDb = GetRepositoryLocalDatabase(repo);
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
var api = Api.C;
try
{
@ -629,7 +781,7 @@ public class RepositoryService : BaseService<RepositoryService>
new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, null!, null!);
// Move depot file to destination
var depotsPath = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
var depotsPath = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
var finalPath = Path.Combine(depotsPath, rsp.MainDepotId.ToString());
Directory.CreateDirectory(depotsPath);
File.Move(tempDepotPath, finalPath, true);
@ -738,7 +890,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<string?> CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable<WorkspaceFile> depotFiles)
{
var repoWs = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
var repoWs = PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
var commitTempFolder = Directory.CreateTempSubdirectory("FlawlessDepot_");
var depotFile = Path.Combine(commitTempFolder.FullName, "depot.bin");

View File

@ -1,7 +1,9 @@
using System;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.VisualTree;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using Ursa.Controls;
@ -14,6 +16,10 @@ public static class UIHelper
{
private static WindowNotificationManager _notificationManager = null!;
private static LoadingContainer _loadingContainer = null!;
private static int _loadingReferenceCount = 0;
public static WindowNotificationManager Notify
{
@ -29,6 +35,40 @@ public static class UIHelper
return _notificationManager!;
}
}
public static LoadingContainer LoadingContainer
{
get
{
if (_loadingContainer != null) return _loadingContainer!;
var lf = ((IClassicDesktopStyleApplicationLifetime)App.Current.ApplicationLifetime);
_loadingContainer = lf.MainWindow!.FindDescendantOfType<LoadingContainer>();
if (_loadingContainer == null)
throw new Exception("Can not get loading mask");
return _loadingContainer!;
}
}
public static void UpdateLoadingStatus()
{
LoadingContainer.IsLoading = _loadingReferenceCount > 0;
}
public static IDisposable MakeLoading(string? msg)
{
_loadingReferenceCount++;
LoadingContainer.LoadingMessage = msg;
UpdateLoadingStatus();
return Disposable.Create(() =>
{
_loadingReferenceCount--;
UpdateLoadingStatus();
});
}
public static void NotifyError(Exception ex)
@ -58,7 +98,20 @@ public static class UIHelper
Console.WriteLine($"Can not notify error to users: {title} - {content}, {e}");
}
}
public static OverlayDialogOptions DefaultOverlayDialogOptions()
{
return new OverlayDialogOptions
{
FullScreen = false,
Buttons = DialogButton.YesNo,
CanResize = false,
CanDragMove = false,
IsCloseButtonVisible = true,
CanLightDismiss = true,
Mode = DialogMode.None
};
}
public static Task<DialogResult> SimpleAskAsync(string content, DialogMode mode = DialogMode.None)
{

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.ReactiveUI;
using Flawless.Client.Models;
using Flawless.Client.Service;
@ -23,11 +25,14 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
[Reactive] private string? _serverFriendlyName;
[Reactive] private string _username;
public ObservableCollection<RepositoryModel> Repos => RepositoryService.C.Repositories;
public HomeViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
Username = Api.C.Username.Value!;
Api.C.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance)
.Subscribe(v => ServerFriendlyName = v ?? "Unknown Server");
@ -37,6 +42,7 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
[ReactiveCommand]
private async Task RefreshRepositoriesAsync()
{
using var l = UIHelper.MakeLoading("Refresh repositories...");
await RepositoryService.C.UpdateRepositoriesFromServerAsync();
await RepositoryService.C.UpdateRepositoriesDownloadedStatusFromDiskAsync();
}
@ -61,6 +67,7 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
if (mr == DialogResult.OK)
{
using var l = UIHelper.MakeLoading("Create repository...");
var repo = await RepositoryService.C.CreateRepositoryOnServerAsync(form.RepositoryName, form.Description);
if (repo == null) return;
if (!await RepositoryService.C.CreateNewRepositoryOnStorageAsync(repo)) return;
@ -81,6 +88,9 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
private async Task DownloadRepositoryAsync()
{
if (_selectedRepository == null) return;
using var l = UIHelper.MakeLoading("Downloading repository...");
if (!await RepositoryService.C.CloneRepositoryFromRemoteAsync(_selectedRepository)) return;
await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(_selectedRepository, HostScreen));
}
@ -131,7 +141,7 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
var mr = await OverlayDialog
.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
if (mr == DialogResult.Yes) Api.C.ClearGateway();
if (mr == DialogResult.Yes) Process.GetCurrentProcess().Kill();
}
[ReactiveCommand]

View File

@ -48,6 +48,7 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
{
try
{
using var l = UIHelper.MakeLoading("Login...");
await Api.C.LoginAsync(Username, Password);
await RepositoryService.C.UpdateRepositoriesFromServerAsync();
}

View File

@ -0,0 +1,27 @@
using System;
using Flawless.Client.Models;
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels.ModalBox;
public partial class EditRepositoryMemberDialogViewModel: ViewModelBase
{
[Reactive] private string _username;
[Reactive] private string _role = RepositoryModel.RepositoryRole.Guest.ToString();
[Reactive] private bool _lockUsername;
public RepositoryModel.RepositoryRole SafeRole
{
get
{
if (Enum.TryParse<RepositoryModel.RepositoryRole>(Role, out var r)) return r;
return RepositoryModel.RepositoryRole.Guest;
}
set
{
Role = value.ToString();
}
}
}

View File

@ -34,6 +34,7 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel
{
try
{
using var l = UIHelper.MakeLoading("Registering...");
await Api.C.Gateway.Register(new RegisterRequest
{
Email = _email,

View File

@ -13,9 +13,12 @@ using DynamicData.Binding;
using Flawless.Abstraction;
using Flawless.Client.Models;
using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using Flawless.Core.Modal;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Ursa.Controls;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels;
@ -69,7 +72,7 @@ public class LocalChangesNode
public class CommitTransitNode
{
public required string Guid { get; set; }
public required string CommitId { get; set; }
public required string Author { get; set; }
@ -77,6 +80,12 @@ public class CommitTransitNode
public required DateTime? CommitAt { get; set; }
public required string FullCommitId { get; set; }
public required string FullMessage { get; set; }
public required string MainDepotId { get; set; }
public static CommitTransitNode FromCommit(RepositoryModel.Commit cm)
{
string msg;
@ -88,17 +97,21 @@ public class CommitTransitNode
{
msg = cm.Message;
}
return new CommitTransitNode
{
Guid = cm.CommitId.ToString(),
CommitId = cm.CommitId.ToString(),
Author = cm.Author,
CommitAt = cm.CommittedOn.ToLocalTime(),
Message = msg,
FullCommitId = cm.CommitId.ToString(),
FullMessage = cm.Message,
MainDepotId = cm.DepotId.ToString(),
};
}
}
public partial class RepositoryViewModel : RoutableViewModelBase
{
public RepositoryModel Repository { get; }
@ -126,10 +139,6 @@ public partial class RepositoryViewModel : RoutableViewModelBase
Repository = repo;
LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo);
User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!;
// Setup repository permission change watcher
RefreshRepositoryRoleInfo();
Repository.Members.ObserveCollectionChanges().Subscribe(_ => RefreshRepositoryRoleInfo());
// Setup local change set
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
@ -167,7 +176,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
Columns =
{
new TextColumn<CommitTransitNode, string>(
string.Empty, n => n.Guid == LocalDatabase.CurrentCommit.ToString() ? "*" : String.Empty),
string.Empty, n => n.CommitId == LocalDatabase.CurrentCommit.ToString() ? "*" : String.Empty),
new TextColumn<CommitTransitNode, string>(
"Message", x => x.Message),
@ -179,7 +188,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
"Time", x => x.CommitAt!.Value),
new TextColumn<CommitTransitNode, string>(
"Id", x => x.Guid.Substring(0, 13)),
"Id", x => x.CommitId.Substring(0, 13)),
}
};
@ -213,6 +222,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
}
private async ValueTask RendererFileTreeAsync()
@ -282,6 +292,19 @@ public partial class RepositoryViewModel : RoutableViewModelBase
}
});
}
private void UpdatePermissionOfRepository()
{
var isOwner = Repository.OwnerName == User.Username;
var role = isOwner ?
RepositoryModel.RepositoryRole.Owner :
Repository.Members.First(p => p.Username == User.Username).Role;
if (role >= RepositoryModel.RepositoryRole.Owner) IsOwnerRole = true;
if (role >= RepositoryModel.RepositoryRole.Developer) IsDeveloperRole = true;
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
}
[ReactiveCommand]
private async Task CommitSelectedChangesAsync()
@ -300,7 +323,8 @@ public partial class RepositoryViewModel : RoutableViewModelBase
await UIHelper.SimpleAlert("Commit message can not be empty!");
return;
}
using var l = UIHelper.MakeLoading("Committing changes...");
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return;
@ -310,30 +334,133 @@ public partial class RepositoryViewModel : RoutableViewModelBase
await RendererFileTreeAsync();
}
[ReactiveCommand]
private async Task PullLatestRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Pulling latest changes...");
var mayUpdate = await RepositoryService.C.IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(Repository);
if (!mayUpdate.HasValue) return;
if (mayUpdate.Value == false)
{
await UIHelper.SimpleAskAsync("Everything is new, no needs to pull.");
return;
}
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var kid = Repository.Commits.MaxBy(k => k.CommittedOn)!.CommitId;
await RepositoryService.C.ResetCommitPointerToTargetAndMergeDepotsIntoRepositoryFromRemoteAsync(Repository, kid);
}
[ReactiveCommand]
private async Task CloseRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Save changes...");
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[ReactiveCommand]
private void RefreshRepositoryRoleInfo()
private async ValueTask RefreshRepositoryRoleInfoAsync()
{
var isOwner = Repository.OwnerName == User.Username;
var role = isOwner ?
RepositoryModel.RepositoryRole.Owner :
Repository.Members.First(p => p.Username == User.Username).Role;
using var l = UIHelper.MakeLoading("Refreshing member info...");
await RepositoryService.C.UpdateMembersFromServerAsync(Repository);
UpdatePermissionOfRepository();
}
if (role >= RepositoryModel.RepositoryRole.Owner) IsOwnerRole = true;
if (role >= RepositoryModel.RepositoryRole.Developer) IsDeveloperRole = true;
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
[ReactiveCommand]
private async ValueTask DeleteMemberFromServerAsync(RepositoryModel.Member member)
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var result = await UIHelper.SimpleAskAsync(
$"Do you really want to delete this member: \n{member.Username} ({member.Role})", DialogMode.Warning);
if (result == DialogResult.Yes)
{
using var l = UIHelper.MakeLoading("Delete member...");
await RepositoryService.C.DeleteMemberFromServerAsync(Repository, member);
}
}
[ReactiveCommand]
private async ValueTask ModifyMemberFromServerAsync(RepositoryModel.Member member)
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var style = UIHelper.DefaultOverlayDialogOptions();
var vm = new EditRepositoryMemberDialogViewModel();
vm.LockUsername = true;
vm.Username = member.Username;
vm.SafeRole = member.Role;
var result = await OverlayDialog.ShowModal<EditRepositoryMemberDialogueView,EditRepositoryMemberDialogViewModel>
(vm, AppDefaultValues.HostId, style);
if (result == DialogResult.Yes)
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
return;
}
if (vm.SafeRole == member.Role)
{
UIHelper.NotifyError("Modification issue", "No modification yet.");
return;
}
using var l = UIHelper.MakeLoading("Modify member...");
member.Role = vm.SafeRole;
await RepositoryService.C.ModifyMemberFromServerAsync(Repository, member);
}
}
[ReactiveCommand]
private async ValueTask AddMemberFromServerAsync()
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var style = UIHelper.DefaultOverlayDialogOptions();
var vm = new EditRepositoryMemberDialogViewModel();
var result = await OverlayDialog.ShowModal<EditRepositoryMemberDialogueView,EditRepositoryMemberDialogViewModel>
(vm, AppDefaultValues.HostId, style);
if (result == DialogResult.Yes)
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
return;
}
vm.Username = vm.Username.Trim();
if (string.IsNullOrEmpty(vm.Username) || vm.Username.Length < 3)
{
UIHelper.NotifyError("Parameter error", "Not a valid username!");
return;
}
using var l = UIHelper.MakeLoading("Add member...");
await RepositoryService.C.AddMemberFromServerAsync(Repository, vm.Username, vm.SafeRole);
}
}
[ReactiveCommand]
private async ValueTask DetectLocalChangesAsync()
{
using var l = UIHelper.MakeLoading("Refreshing local changes...");
var ns = await Task.Run(async () =>
{
LocalDatabase.LocalAccessor.Refresh();

View File

@ -31,6 +31,7 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase
{
try
{
using var l = UIHelper.MakeLoading("Contacting to server...");
await Api.C.SetGatewayAsync(Host);
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
}

View File

@ -17,10 +17,11 @@
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconQuit}"
Command="{Binding QuitLoginCommand}"
Content="{Binding Username}"/>
<u:IconButton Icon="{StaticResource SemiIconSetting}"
Command="{Binding OpenGlobalSettingCommand}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconQuit}"
Command="{Binding QuitLoginCommand}"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
@ -84,7 +85,7 @@
<ScrollViewer IsVisible="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Vertical" Spacing="8">
<Label Content="{Binding SelectedRepository.Description, FallbackValue='Description as below.'}"/>
<u:Divider Content="Recent Activities"/>
<!-- <u:Divider Content="Recent Activities"/> -->
</StackPanel>
</ScrollViewer>
</StackPanel>

View File

@ -20,6 +20,7 @@
<Panel>
<rxui:RoutedViewHost Router="{Binding Router}"/>
<ursa:LoadingContainer/>
<ursa:OverlayDialogHost HostId="Overlay"/>
<ursa:WindowNotificationManager/>
</Panel>

View File

@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
x:DataType="vm:EditRepositoryMemberDialogViewModel"
d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"
MinWidth="400"
x:Class="Flawless.Client.Views.ModalBox.EditRepositoryMemberDialogueView">
<u:Form HorizontalAlignment="Stretch" LabelPosition="Top">
<u:Form.ItemsPanel>
<ItemsPanelTemplate>
<Grid ColumnDefinitions="*, 10, 160"/>
</ItemsPanelTemplate>
</u:Form.ItemsPanel>
<u:FormItem Grid.Column="0" Label="Username">
<TextBox Text="{Binding Username}"
IsEnabled="{Binding !LockUsername, Mode=TwoWay}"/>
</u:FormItem>
<u:FormItem Grid.Column="2" Label="Role">
<ComboBox HorizontalAlignment="Stretch" SelectedIndex="{Binding Role, Mode=TwoWay}">
<ComboBoxItem>Guest</ComboBoxItem>
<ComboBoxItem>Reporter</ComboBoxItem>
<ComboBoxItem>Developer</ComboBoxItem>
</ComboBox>
</u:FormItem>
</u:Form>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Flawless.Client.Views.ModalBox;
public partial class EditRepositoryMemberDialogueView : UserControl
{
public EditRepositoryMemberDialogueView()
{
InitializeComponent();
}
}

View File

@ -9,9 +9,13 @@
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}"/>
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
<ScrollViewer>
<StackPanel Spacing="4">
<Label Content="Commit Message"/>
<ScrollViewer IsVisible="{Binding !!Commits.RowSelection.SelectedItem}">
<StackPanel Spacing="8">
<Label FontWeight="600" FontSize="18" Content="Commit Details"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.FullCommitId, FallbackValue='00000000-0000', StringFormat='Id: {0}'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.Author, FallbackValue='Author', StringFormat='Author: {0}'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.CommitAt, FallbackValue='At Time', StringFormat='Time: {0}'}"/>
<Label FontWeight="400" FontSize="14" Content="{Binding Commits.RowSelection.SelectedItem.FullMessage, FallbackValue='Commit messages.'}"/>
</StackPanel>
</ScrollViewer>
</Border>

View File

@ -2,28 +2,58 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Flawless.Client.ViewModels"
x:DataType="vm:RepositoryViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
<TabControl TabStripPlacement="Left">
<TabItem Header="Members">
<StackPanel Width="400" HorizontalAlignment="Stretch">
</StackPanel>
<StackPanel Width="600" Orientation="Vertical" HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Add User"
IsVisible="{Binding Repository.OwnByCurrentUser}"
Command="{Binding AddMemberFromServerAsyncCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"
Command="{Binding RefreshRepositoryRoleInfoAsyncCommand}" />
</StackPanel>
<ScrollViewer HorizontalAlignment="Stretch">
<ItemsControl ItemsSource="{Binding Repository.Members}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto, *, Auto" Margin="0 16" VerticalAlignment="Center">
<StackPanel Grid.Column="0" Spacing="6" Orientation="Horizontal">
<Label FontSize="16" VerticalContentAlignment="Center" Content="{Binding Username}"/>
<Label VerticalContentAlignment="Center" Classes="Blue"
Theme="{StaticResource TagLabel}" Content="{Binding Role}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="8" IsVisible="{Binding CanEdit}" Orientation="Horizontal">
<u:IconButton Icon="{StaticResource SemiIconEdit}"
Command="{Binding $parent[ItemsControl].((vm:RepositoryViewModel)DataContext).ModifyMemberFromServerAsyncCommand}"
CommandParameter="{Binding .}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconExit}"
Command="{Binding $parent[ItemsControl].((vm:RepositoryViewModel)DataContext).DeleteMemberFromServerAsyncCommand}"
CommandParameter="{Binding .}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</TabItem>
<TabItem Header="Statics" IsVisible="{Binding IsDeveloperRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch">
<StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel>
</TabItem>
<TabItem Header="Admin Area" IsVisible="{Binding IsOwnerRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch">
<StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel>
</TabItem>
<TabItem Header="Hooks" IsVisible="{Binding IsOwnerRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch">
<StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel>
</TabItem>

View File

@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Flawless.Client.ViewModels;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;

View File

@ -11,9 +11,12 @@
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Spacing="8" Margin="6" Orientation="Horizontal">
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Detect"
Command="{Binding DetectLocalChangesAsyncCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull"/>
<u:IconButton Icon="{StaticResource SemiIconCheckList}" Content="Select"/>
<u:IconButton Icon="{StaticResource SemiIconList}" Content="Deselect"/>
<u:IconButton Icon="{StaticResource SemiIconCheckList}" Content="All"
Command="{Binding SelectAllChangesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconList}" Content="None"
Command="{Binding DeselectAllChangesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull"
Command="{Binding PullLatestRepositoryCommand}"/>
<!-- <ToggleButton Content="Auto Refresh" IsChecked="{Binding AutoDetectChanges}"/> -->
</StackPanel>
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>

View File

@ -13,7 +13,7 @@
<DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<u:IconButton Height="4" Icon="{StaticResource SemiIconArrowLeft}"
Command="{Binding GoBackCommand}" Content="All Repositories"/>
Command="{Binding CloseRepositoryCommand}" Content="All Repositories"/>
<Label FontWeight="400" FontSize="28" Content="{Binding Repository.StandaloneName}"/>
</StackPanel>
<TabControl TabStripPlacement="Top" Margin="0 20 0 0">

View File

@ -4,5 +4,5 @@ public record RepoUserRole
{
public required string Username { get; set; }
public RepositoryRole? Role { get; set; }
public RepositoryRole Role { get; set; }
}

View File

@ -103,19 +103,16 @@ public class RepositoryInnieController(
}
[HttpGet("get_users")]
public async Task<ActionResult<ListingResponse<RepoUserRole>>> GetUsersAsync(string repositoryName)
public async Task<ActionResult<ListingResponse<RepoUserRole>>> GetUsersAsync(string userName, string repositoryName)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, u, RepositoryRole.Guest);
if (grantIssue is not Repository) return (ActionResult) grantIssue;
var rp = await dbContext.Repositories
.Include(repository => repository.Owner)
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && userName == rp.Owner.UserName);
return Ok(new ListingResponse<RepoUserRole>(rp.Members.Select(pm => new RepoUserRole
{
@ -123,70 +120,7 @@ public class RepositoryInnieController(
Role = pm.Role
}).ToArray()));
}
[HttpPost("update_user")]
public async Task<IActionResult> UpdateUserAsync(string repositoryName, [FromBody] RepoUserRole r)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var tu = await userManager.FindByNameAsync(r.Username);
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
var rp = await dbContext.Repositories
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
var m = rp.Members.FirstOrDefault(m => m.User == tu);
if (m == null)
{
m = new RepositoryMember
{
User = tu,
Role = r.Role ?? RepositoryRole.Guest
};
rp.Members.Add(m);
}
else
{
m.Role = r.Role ?? RepositoryRole.Guest;
}
await dbContext.SaveChangesAsync();
return Ok();
}
[HttpPost("delete_user")]
public async Task<IActionResult> DeleteUserAsync(string repositoryName, [FromBody] RepoUserRole r)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var tu = await userManager.FindByNameAsync(r.Username);
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
var rp = await dbContext.Repositories
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
var m = rp.Members.FirstOrDefault(m => m.User == tu);
if (m == null) return BadRequest(new FailedResponse("User is not being granted to this repository!"));
rp.Members.Remove(m);
await dbContext.SaveChangesAsync();
return Ok();
}
#endregion

View File

@ -67,4 +67,68 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
Role = RepositoryRole.Owner
});
}
[HttpPost("update_user")]
public async Task<IActionResult> UpdateUserAsync(string repositoryName, string modUser, RepositoryRole role)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var tu = await userManager.FindByNameAsync(modUser);
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
var rp = await dbContext.Repositories
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
var m = rp.Members.FirstOrDefault(m => m.User == tu);
if (m == null)
{
m = new RepositoryMember
{
User = tu,
Role = role
};
rp.Members.Add(m);
}
else
{
m.Role = role;
}
await dbContext.SaveChangesAsync();
return Ok();
}
[HttpPost("delete_user")]
public async Task<IActionResult> DeleteUserAsync(string repositoryName, string delUser)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var tu = await userManager.FindByNameAsync(delUser);
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
var rp = await dbContext.Repositories
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
var m = rp.Members.FirstOrDefault(m => m.User == tu);
if (m == null) return BadRequest(new FailedResponse("User is not being granted to this repository!"));
rp.Members.Remove(m);
await dbContext.SaveChangesAsync();
return Ok();
}
}