feat: More stable right now.
This commit is contained in:
parent
91097940fc
commit
ab1ee9925d
@ -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" />
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa\1.10.0\lib\net8.0\Ursa.dll" />
|
||||
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa.themes.semi\1.10.0\lib\netstandard2.0\Ursa.Themes.Semi.dll" />
|
||||
<Assembly Path="C:\Users\Cardi\.nuget\packages\reactiveui\20.1.1\lib\net8.0\ReactiveUI.dll" />
|
||||
</AssemblyExplorer></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
|
||||
<Panel>
|
||||
<rxui:RoutedViewHost Router="{Binding Router}"/>
|
||||
<ursa:LoadingContainer/>
|
||||
<ursa:OverlayDialogHost HostId="Overlay"/>
|
||||
<ursa:WindowNotificationManager/>
|
||||
</Panel>
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -4,5 +4,5 @@ public record RepoUserRole
|
||||
{
|
||||
public required string Username { get; set; }
|
||||
|
||||
public RepositoryRole? Role { get; set; }
|
||||
public RepositoryRole Role { get; set; }
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user