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/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/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/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/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/CreateRepositoryDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/SimpleMessageDialogView.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; <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\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\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> &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; <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; &lt;TestAncestor&gt;&#xD;

View File

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

View File

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

View File

@ -64,7 +64,7 @@ public class LocalFileTreeAccessor
{ {
_repo = repo; _repo = repo;
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath); _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) public void SetBaseline(IEnumerable<WorkspaceFile> baselines)

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flawless.Client.Models;
#nullable enable annotations #nullable enable annotations
@ -101,16 +102,6 @@ namespace Flawless.Client.Remote
[Get("/api/repo/{userName}/{repositoryName}/get_users")] [Get("/api/repo/{userName}/{repositoryName}/get_users")]
Task<RepoUserRoleListingResponse> GetUsers(string repositoryName, string userName); 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> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
@ -170,6 +161,16 @@ namespace Flawless.Client.Remote
[Post("/api/repo_create")] [Post("/api/repo_create")]
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName, [Query] string description); 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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_info")] [Post("/api/user/update_info")]

View File

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

View File

@ -7,10 +7,13 @@ using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flawless.Abstraction; using Flawless.Abstraction;
using Flawless.Client.Models; using Flawless.Client.Models;
using Flawless.Client.Remote;
using Flawless.Core.BinaryDataFormat; using Flawless.Core.BinaryDataFormat;
using Flawless.Core.Modal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Refit; using Refit;
using CommitManifest = Flawless.Core.Modal.CommitManifest;
using DepotLabel = Flawless.Core.Modal.DepotLabel;
using WorkspaceFile = Flawless.Core.Modal.WorkspaceFile;
namespace Flawless.Client.Service; namespace Flawless.Client.Service;
@ -26,12 +29,12 @@ public class RepositoryService : BaseService<RepositoryService>
private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo) 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; if (File.Exists(dbPath)) return false;
// Get directories // Get directories
var localRepoDb = GetRepositoryLocalDatabase(repo); 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. // Create initial data.
Directory.CreateDirectory(folderPath); Directory.CreateDirectory(folderPath);
@ -47,7 +50,7 @@ public class RepositoryService : BaseService<RepositoryService>
var localRepo = GetRepositoryLocalDatabase(repo); var localRepo = GetRepositoryLocalDatabase(repo);
localRepo.LastOprationTime = DateTime.Now; 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)!); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.OpenOrCreate)); 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)) 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)) if (File.Exists(dbPath))
{ {
// Use existed target // Use existed target
@ -177,14 +180,83 @@ public class RepositoryService : BaseService<RepositoryService>
{ {
foreach (var repo in _repositories) foreach (var repo in _repositories)
{ {
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name)); var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name)); var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
repo.IsDownloaded = isFolderExists && isDbFileExists; repo.IsDownloaded = isFolderExists && isDbFileExists;
} }
return true; 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) public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
{ {
var api = Api.C; var api = Api.C;
@ -200,6 +272,7 @@ public class RepositoryService : BaseService<RepositoryService>
// Update existed // Update existed
var dict = members.Result.ToDictionary(m => m.Username); 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++) for (var i = 0; i < repo.Members.Count; i++)
{ {
var ele = repo.Members[i]; var ele = repo.Members[i];
@ -212,15 +285,18 @@ public class RepositoryService : BaseService<RepositoryService>
ele.Username = role.Username; ele.Username = role.Username;
ele.Role = (RepositoryModel.RepositoryRole) role.Role; ele.Role = (RepositoryModel.RepositoryRole) role.Role;
ele.CanEdit = ele.Role != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser;
} }
// Add missing // Add missing
foreach (var role in dict.Values) foreach (var role in dict.Values)
{ {
var rl = (RepositoryModel.RepositoryRole)role.Role;
var r = new RepositoryModel.Member var r = new RepositoryModel.Member
{ {
Username = role.Username, Username = role.Username,
Role = (RepositoryModel.RepositoryRole) role.Role Role = rl,
CanEdit = rl != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser
}; };
repo.Members.Add(r); repo.Members.Add(r);
@ -261,7 +337,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo) public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
{ {
if (!await UpdateRepositoriesDownloadedStatusFromDiskAsync() || repo.IsDownloaded == false) return false; if (!await UpdateRepositoriesDownloadedStatusFromDiskAsync() || repo.IsDownloaded == false) return false;
if (!await UpdateCommitsFromServerAsync(repo)) return false; if (!await UpdateCommitsHistoryFromServerAsync(repo)) return false;
var ls = GetRepositoryLocalDatabase(repo); var ls = GetRepositoryLocalDatabase(repo);
if (ls.CurrentCommit != null) if (ls.CurrentCommit != null)
@ -296,7 +372,7 @@ public class RepositoryService : BaseService<RepositoryService>
// Create basic structures. // Create basic structures.
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false; 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); var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
if (peekCommit == null) return false; // Should not use this function! if (peekCommit == null) return false; // Should not use this function!
@ -343,7 +419,71 @@ public class RepositoryService : BaseService<RepositoryService>
return true; return true;
} }
public async ValueTask<bool> ShouldUpdateLocalCommitsCacheFromServerAsync(RepositoryModel repo) 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;
_openedRepos.Add(repo);
return true;
}
public async ValueTask<bool?> IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(RepositoryModel repo)
{ {
var api = Api.C; var api = Api.C;
try try
@ -358,10 +498,11 @@ public class RepositoryService : BaseService<RepositoryService>
var emptyRepo = repo.Commits.Count == 0; var emptyRepo = repo.Commits.Count == 0;
// If they both empty // If they both empty
if ((rsp.Result == Guid.Empty) == emptyRepo) return false; if ((rsp.Result == Guid.Empty) && emptyRepo) return false;
if (emptyRepo) return true; 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) 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; var api = Api.C;
try try
@ -441,7 +582,7 @@ public class RepositoryService : BaseService<RepositoryService>
if (manifest == null) return null; if (manifest == null) return null;
// Prepare folders // 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); Directory.CreateDirectory(depotsRoot);
// Generate download depots list // Generate download depots list
@ -502,7 +643,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
(RepositoryModel repo, IEnumerable<(Guid id, Stream stream)> depots) (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) foreach (var d in depots)
{ {
var dst = Path.Combine(depotsRoot, d.id.ToString()); var dst = Path.Combine(depotsRoot, d.id.ToString());
@ -581,7 +722,7 @@ public class RepositoryService : BaseService<RepositoryService>
{ {
try 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); if (Directory.Exists(path)) Directory.Delete(path, true);
} }
catch (Exception e) catch (Exception e)
@ -598,9 +739,20 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
(RepositoryModel repo, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, string message) (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 localDb = GetRepositoryLocalDatabase(repo);
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes); var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
var api = Api.C;
try try
{ {
@ -629,7 +781,7 @@ public class RepositoryService : BaseService<RepositoryService>
new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, null!, null!); new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, null!, null!);
// Move depot file to destination // 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()); var finalPath = Path.Combine(depotsPath, rsp.MainDepotId.ToString());
Directory.CreateDirectory(depotsPath); Directory.CreateDirectory(depotsPath);
File.Move(tempDepotPath, finalPath, true); File.Move(tempDepotPath, finalPath, true);
@ -738,7 +890,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<string?> CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable<WorkspaceFile> depotFiles) 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 commitTempFolder = Directory.CreateTempSubdirectory("FlawlessDepot_");
var depotFile = Path.Combine(commitTempFolder.FullName, "depot.bin"); var depotFile = Path.Combine(commitTempFolder.FullName, "depot.bin");

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.VisualTree;
using Flawless.Client.ViewModels.ModalBox; using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox; using Flawless.Client.Views.ModalBox;
using Ursa.Controls; using Ursa.Controls;
@ -15,6 +17,10 @@ public static class UIHelper
private static WindowNotificationManager _notificationManager = null!; private static WindowNotificationManager _notificationManager = null!;
private static LoadingContainer _loadingContainer = null!;
private static int _loadingReferenceCount = 0;
public static WindowNotificationManager Notify public static WindowNotificationManager Notify
{ {
get get
@ -30,6 +36,40 @@ public static class UIHelper
} }
} }
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) public static void NotifyError(Exception ex)
{ {
@ -59,6 +99,19 @@ public static class UIHelper
} }
} }
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) public static Task<DialogResult> SimpleAskAsync(string content, DialogMode mode = DialogMode.None)
{ {

View File

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

View File

@ -48,6 +48,7 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
{ {
try try
{ {
using var l = UIHelper.MakeLoading("Login...");
await Api.C.LoginAsync(Username, Password); await Api.C.LoginAsync(Username, Password);
await RepositoryService.C.UpdateRepositoriesFromServerAsync(); 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 try
{ {
using var l = UIHelper.MakeLoading("Registering...");
await Api.C.Gateway.Register(new RegisterRequest await Api.C.Gateway.Register(new RegisterRequest
{ {
Email = _email, Email = _email,

View File

@ -13,9 +13,12 @@ using DynamicData.Binding;
using Flawless.Abstraction; using Flawless.Abstraction;
using Flawless.Client.Models; using Flawless.Client.Models;
using Flawless.Client.Service; using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using Flawless.Core.Modal; using Flawless.Core.Modal;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.SourceGenerators; using ReactiveUI.SourceGenerators;
using Ursa.Controls;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType; using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels; namespace Flawless.Client.ViewModels;
@ -69,7 +72,7 @@ public class LocalChangesNode
public class CommitTransitNode public class CommitTransitNode
{ {
public required string Guid { get; set; } public required string CommitId { get; set; }
public required string Author { get; set; } public required string Author { get; set; }
@ -77,6 +80,12 @@ public class CommitTransitNode
public required DateTime? CommitAt { get; set; } 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) public static CommitTransitNode FromCommit(RepositoryModel.Commit cm)
{ {
string msg; string msg;
@ -91,14 +100,18 @@ public class CommitTransitNode
return new CommitTransitNode return new CommitTransitNode
{ {
Guid = cm.CommitId.ToString(), CommitId = cm.CommitId.ToString(),
Author = cm.Author, Author = cm.Author,
CommitAt = cm.CommittedOn.ToLocalTime(), CommitAt = cm.CommittedOn.ToLocalTime(),
Message = msg, Message = msg,
FullCommitId = cm.CommitId.ToString(),
FullMessage = cm.Message,
MainDepotId = cm.DepotId.ToString(),
}; };
} }
} }
public partial class RepositoryViewModel : RoutableViewModelBase public partial class RepositoryViewModel : RoutableViewModelBase
{ {
public RepositoryModel Repository { get; } public RepositoryModel Repository { get; }
@ -127,10 +140,6 @@ public partial class RepositoryViewModel : RoutableViewModelBase
LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo); LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo);
User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!; User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!;
// Setup repository permission change watcher
RefreshRepositoryRoleInfo();
Repository.Members.ObserveCollectionChanges().Subscribe(_ => RefreshRepositoryRoleInfo());
// Setup local change set // Setup local change set
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw) LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
{ {
@ -167,7 +176,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
Columns = Columns =
{ {
new TextColumn<CommitTransitNode, string>( 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>( new TextColumn<CommitTransitNode, string>(
"Message", x => x.Message), "Message", x => x.Message),
@ -179,7 +188,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
"Time", x => x.CommitAt!.Value), "Time", x => x.CommitAt!.Value),
new TextColumn<CommitTransitNode, string>( 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 DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync(); await RendererFileTreeAsync();
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
} }
private async ValueTask RendererFileTreeAsync() private async ValueTask RendererFileTreeAsync()
@ -283,6 +293,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] [ReactiveCommand]
private async Task CommitSelectedChangesAsync() private async Task CommitSelectedChangesAsync()
{ {
@ -301,6 +324,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
return; return;
} }
using var l = UIHelper.MakeLoading("Committing changes...");
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!); var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return; if (manifest == null) return;
@ -310,30 +334,133 @@ public partial class RepositoryViewModel : RoutableViewModelBase
await RendererFileTreeAsync(); 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] [ReactiveCommand]
private async Task CloseRepositoryAsync() private async Task CloseRepositoryAsync()
{ {
using var l = UIHelper.MakeLoading("Save changes...");
await RepositoryService.C.CloseRepositoryAsync(Repository); await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute(); await HostScreen.Router.NavigateBack.Execute();
} }
[ReactiveCommand] [ReactiveCommand]
private void RefreshRepositoryRoleInfo() private async ValueTask RefreshRepositoryRoleInfoAsync()
{ {
var isOwner = Repository.OwnerName == User.Username; using var l = UIHelper.MakeLoading("Refreshing member info...");
var role = isOwner ? await RepositoryService.C.UpdateMembersFromServerAsync(Repository);
RepositoryModel.RepositoryRole.Owner : UpdatePermissionOfRepository();
Repository.Members.First(p => p.Username == User.Username).Role; }
if (role >= RepositoryModel.RepositoryRole.Owner) IsOwnerRole = true; [ReactiveCommand]
if (role >= RepositoryModel.RepositoryRole.Developer) IsDeveloperRole = true; private async ValueTask DeleteMemberFromServerAsync(RepositoryModel.Member member)
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true; {
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true; 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] [ReactiveCommand]
private async ValueTask DetectLocalChangesAsync() private async ValueTask DetectLocalChangesAsync()
{ {
using var l = UIHelper.MakeLoading("Refreshing local changes...");
var ns = await Task.Run(async () => var ns = await Task.Run(async () =>
{ {
LocalDatabase.LocalAccessor.Refresh(); LocalDatabase.LocalAccessor.Refresh();

View File

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

View File

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

View File

@ -20,6 +20,7 @@
<Panel> <Panel>
<rxui:RoutedViewHost Router="{Binding Router}"/> <rxui:RoutedViewHost Router="{Binding Router}"/>
<ursa:LoadingContainer/>
<ursa:OverlayDialogHost HostId="Overlay"/> <ursa:OverlayDialogHost HostId="Overlay"/>
<ursa:WindowNotificationManager/> <ursa:WindowNotificationManager/>
</Panel> </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*, *"> <Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}"/> <TreeDataGrid Grid.Column="0" Source="{Binding Commits}"/>
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}"> <Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
<ScrollViewer> <ScrollViewer IsVisible="{Binding !!Commits.RowSelection.SelectedItem}">
<StackPanel Spacing="4"> <StackPanel Spacing="8">
<Label Content="Commit Message"/> <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> </StackPanel>
</ScrollViewer> </ScrollViewer>
</Border> </Border>

View File

@ -2,28 +2,58 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Flawless.Client.ViewModels" xmlns:vm="using:Flawless.Client.ViewModels"
x:DataType="vm:RepositoryViewModel" x:DataType="vm:RepositoryViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView"> x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
<TabControl TabStripPlacement="Left"> <TabControl TabStripPlacement="Left">
<TabItem Header="Members"> <TabItem Header="Members">
<StackPanel Width="400" HorizontalAlignment="Stretch"> <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> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="Statics" IsVisible="{Binding IsDeveloperRole}"> <TabItem Header="Statics" IsVisible="{Binding IsDeveloperRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch"> <StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="Admin Area" IsVisible="{Binding IsOwnerRole}"> <TabItem Header="Admin Area" IsVisible="{Binding IsOwnerRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch"> <StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="Hooks" IsVisible="{Binding IsOwnerRole}"> <TabItem Header="Hooks" IsVisible="{Binding IsOwnerRole}">
<StackPanel Width="400" HorizontalAlignment="Stretch"> <StackPanel Width="600" HorizontalAlignment="Stretch">
</StackPanel> </StackPanel>
</TabItem> </TabItem>

View File

@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Flawless.Client.ViewModels;
using Ursa.Controls; using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage; 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"> <StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Spacing="8" Margin="6" Orientation="Horizontal">
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Detect" <u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Detect"
Command="{Binding DetectLocalChangesAsyncCommand}"/> Command="{Binding DetectLocalChangesAsyncCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull"/> <u:IconButton Icon="{StaticResource SemiIconCheckList}" Content="All"
<u:IconButton Icon="{StaticResource SemiIconCheckList}" Content="Select"/> Command="{Binding SelectAllChangesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconList}" Content="Deselect"/> <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}"/> --> <!-- <ToggleButton Content="Auto Refresh" IsChecked="{Binding AutoDetectChanges}"/> -->
</StackPanel> </StackPanel>
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/> <TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>

View File

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

View File

@ -4,5 +4,5 @@ public record RepoUserRole
{ {
public required string Username { get; set; } 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")] [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 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 var rp = await dbContext.Repositories
.Include(repository => repository.Owner)
.Include(repository => repository.Members) .Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User) .ThenInclude(repositoryMember => repositoryMember.User)
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner); .FirstOrDefaultAsync(rp => rp.Name == repositoryName && userName == rp.Owner.UserName);
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
return Ok(new ListingResponse<RepoUserRole>(rp.Members.Select(pm => new RepoUserRole return Ok(new ListingResponse<RepoUserRole>(rp.Members.Select(pm => new RepoUserRole
{ {
@ -124,69 +121,6 @@ public class RepositoryInnieController(
}).ToArray())); }).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 #endregion

View File

@ -67,4 +67,68 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
Role = RepositoryRole.Owner 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();
}
} }