1
0

feat: Modify visuals and add commit file tree generator

This commit is contained in:
Ca2didi 2025-04-01 19:01:04 +08:00
parent 11d690d22b
commit 738e8308a8
16 changed files with 397 additions and 96 deletions

View File

@ -21,6 +21,7 @@
<entry key="Flawless.Client/Views/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RegisterView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoCommitPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoDashboardPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoFileTreePageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />

View File

@ -8,5 +8,6 @@
<Application.Styles>
<semi:SemiTheme Locale="zh-cn"/>
<ursa:SemiTheme Locale="zh-cn"/>
<StyleInclude Source="avares://Semi.Avalonia.TreeDataGrid/Index.axaml" />
</Application.Styles>
</Application>

View File

@ -36,6 +36,7 @@
</PackageReference>
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Semi.Avalonia" Version="11.2.1.6" />
<PackageReference Include="Semi.Avalonia.TreeDataGrid" Version="11.0.10.2" />
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" />
</ItemGroup>
@ -66,4 +67,8 @@
<ProjectReference Include="..\Flawless.Abstract\Flawless.Abstract.csproj" />
<ProjectReference Include="..\Flawless.Core\Flawless.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Converters\" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using Flawless.Client.Service;
using ReactiveUI.SourceGenerators;
@ -8,14 +9,17 @@ namespace Flawless.Client.Models;
[Serializable]
public partial class RepositoryLocalDatabaseModel : ReactiveModel
{
public required RepositoryModel RootModal { get; init; }
[JsonIgnore]
public required RepositoryModel RootModal { get; set; }
public required LocalFileTreeAccessor LocalAccessor { get; init; }
[JsonIgnore]
public required LocalFileTreeAccessor LocalAccessor { get; set; }
[JsonIgnore]
public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
public ObservableCollection<string> CurrentLockedFiles { get; } = new();
[NonSerialized]
[Reactive] private Guid? _currentCommit;
[Reactive] private string? _commitMessage;
}

View File

@ -34,7 +34,7 @@ public partial class RepositoryModel : ReactiveModel
public ObservableCollection<Lock> Locks { get; } = new();
public enum RepositoryRole
public enum RepositoryRole : byte
{
Guest = 0,
Reporter = 1,

View File

@ -1,6 +1,33 @@
namespace Flawless.Client.Models;
using System;
using ReactiveUI.SourceGenerators;
public record UserModel : ReactiveRecordModel
namespace Flawless.Client.Models;
public partial class UserModel : ReactiveModel
{
public enum SexType : byte
{
Unset = 0,
Male = 1,
Female = 2,
WalmartPlasticBag = 3
}
[Reactive] private string _username;
[Reactive] private string _nickname;
[Reactive] private string _bio;
[Reactive] private SexType _sex;
[Reactive] private bool _emailIsVisible;
[Reactive] private bool _canEdit;
[Reactive] private string _email;
[Reactive] private string _phoneNumber;
[Reactive] private DateTime _joinDate;
}

View File

@ -36,6 +36,8 @@ public class Api : BaseService<Api>
public IObservable<TokenInfo?> Token => _token;
public IReactiveProperty<string?> Username => _username;
private readonly ReactiveProperty<bool> _isLoggedIn = new(false);
private readonly ReactiveProperty<string?> _serverUrl = new(string.Empty);
@ -43,6 +45,8 @@ public class Api : BaseService<Api>
private readonly ReactiveProperty<ServerStatusResponse?> _status = new(null);
private readonly ReactiveProperty<TokenInfo?> _token = new(null);
private readonly ReactiveProperty<string?> _username = new(string.Empty);
#region GatewayConfig
@ -55,6 +59,7 @@ public class Api : BaseService<Api>
public void ClearGateway()
{
_gateway = null;
_username.Value = null;
_isLoggedIn.Value = false;
_status.Value = null;
_serverUrl.Value = null;
@ -82,7 +87,9 @@ public class Api : BaseService<Api>
Password = password
});
_username.Value = username;
_isLoggedIn.Value = true;
await UserService.C.DownloadUserInfoAsync(username);
}
#endregion

View File

@ -1,4 +1,6 @@
namespace Flawless.Client.Service;
using Flawless.Client.ViewModels;
namespace Flawless.Client.Service;
public class BaseService<TService> where TService : class, new()
{

View File

@ -11,6 +11,28 @@ namespace Flawless.Client.Service;
public class LocalFileTreeAccessor
{
public enum ChangeType
{
Folder = 0,
Add,
Remove,
Modify
}
public struct ChangeRecord
{
public ChangeType Type { get; }
public WorkspaceFile File { get; }
public ChangeRecord(ChangeType type, WorkspaceFile file)
{
Type = type;
File = file;
}
}
private static readonly string[] IgnoredDirectories =
{
AppDefaultValues.RepoLocalStorageManagerFolder
@ -22,11 +44,7 @@ public class LocalFileTreeAccessor
private IReadOnlyDictionary<string, WorkspaceFile> _baseline;
private readonly Dictionary<string, WorkspaceFile> _newFiles = new();
private readonly Dictionary<string, WorkspaceFile> _deleteFiles = new();
private readonly Dictionary<string, WorkspaceFile> _modifyFiles = new();
private Dictionary<string, ChangeRecord> _difference;
private object _optLock = new();
@ -34,16 +52,12 @@ public class LocalFileTreeAccessor
public IReadOnlyDictionary<string, WorkspaceFile> BaselineFiles => _baseline;
public IReadOnlyDictionary<string, WorkspaceFile> NewFiles => _newFiles;
public IReadOnlyDictionary<string, WorkspaceFile> DeletedFiles => _deleteFiles;
public IReadOnlyDictionary<string, WorkspaceFile> ModifiedFiles => _modifyFiles;
public IReadOnlyDictionary<string, ChangeRecord> Changes => _difference;
public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable<WorkspaceFile> baselines)
{
_repo = repo;
_difference = new Dictionary<string, ChangeRecord>();
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
}
@ -53,9 +67,7 @@ public class LocalFileTreeAccessor
lock (_optLock)
{
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
_newFiles.Clear();
_deleteFiles.Clear();
_modifyFiles.Clear();
_difference.Clear();
RefreshInternal();
}
@ -65,9 +77,7 @@ public class LocalFileTreeAccessor
{
lock (_optLock)
{
_newFiles.Clear();
_deleteFiles.Clear();
_modifyFiles.Clear();
_difference.Clear();
RefreshInternal();
}
@ -91,8 +101,8 @@ public class LocalFileTreeAccessor
var news = currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
var removed = _baseline.Values.Where(v => !currentFiles.ContainsKey(v.WorkPath));
foreach (var f in changes) _modifyFiles.Add(f.WorkPath, f);
foreach (var f in news) _newFiles.Add(f.WorkPath, f);
foreach (var f in removed) _deleteFiles.Add(f.WorkPath, f);
foreach (var f in changes) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Modify, f));
foreach (var f in news) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Add, f));
foreach (var f in removed) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Remove, f));
}
}

View File

@ -65,8 +65,11 @@ public class RepositoryService : BaseService<RepositoryService>
{
// Use existed target
using var readFs = new StreamReader(new FileStream(dbPath, FileMode.Open));
localRepo = JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
as RepositoryLocalDatabaseModel; // todo add broken test.
localRepo = (JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
as RepositoryLocalDatabaseModel)!; // todo add broken test.
localRepo.RootModal = repo;
localRepo.LocalAccessor = new LocalFileTreeAccessor(repo, []);
}
else
{
@ -572,7 +575,8 @@ public class RepositoryService : BaseService<RepositoryService>
Console.WriteLine(e);
return false;
}
repo.IsDownloaded = false;
return true;
}
}

View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Flawless.Client.Models;
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.Service;
public partial class UserService : BaseService<UserService>
{
private Dictionary<string, UserModel> _cachedUsers = new();
public bool IsUserCached(string username)
=> _cachedUsers.ContainsKey(username);
public UserModel? GetUserInfoAsync(string username)
=> _cachedUsers.ContainsKey(username) ? _cachedUsers[username] : null;
public async ValueTask<UserModel?> GetOrDownloadUserInfoAsync(string username)
{
if (_cachedUsers.TryGetValue(username, out var userModel)) return userModel;
return await DownloadUserInfoAsync(username);
}
public async ValueTask<UserModel?> DownloadUserInfoAsync(string username)
{
var api = Api.C;
UserModel user;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return null;
}
var info = await api.Gateway.GetInfo(username);
user = new UserModel();
user.Username = info.Username;
user.Nickname = info.NickName;
user.Bio = info.Bio;
user.Sex = (UserModel.SexType) info.Gender;
user.EmailIsVisible = info.PublicEmail ?? false;
user.CanEdit = info.Authorized;
user.PhoneNumber = info.Phone;
user.Email = info.Email;
_cachedUsers.Add(username, user);
}
catch (Exception e)
{
Console.WriteLine(e);
return null;
}
return user;
}
public async ValueTask<bool> RefreshUserInfoAsync(UserModel user)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var info = await api.Gateway.GetInfo(user.Username);
user.Nickname = info.NickName;
user.Bio = info.Bio;
user.Sex = (UserModel.SexType) info.Gender;
user.EmailIsVisible = info.PublicEmail ?? false;
user.CanEdit = info.Authorized;
user.PhoneNumber = info.Phone;
user.Email = info.Email;
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
return true;
}
}

View File

@ -1,25 +1,191 @@
using System.Reactive.Linq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using DynamicData;
using DynamicData.Binding;
using Flawless.Client.Models;
using Flawless.Client.Service;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels;
public class LocalChangesNode
{
public required string FullPath { get; set; }
public required string Type { get; set; }
public DateTime? ModifiedTime { get; set; }
public bool Included
{
get
{
if (Contents != null) return Contents.All(c => c.Included);
return _actualIncluded;
}
set
{
if (Contents != null) foreach (var n in Contents) n.Included = value;
_actualIncluded = value;
}
}
private bool _actualIncluded;
public ObservableCollection<LocalChangesNode>? Contents { get; set; }
public static LocalChangesNode FromFolder(string folderPath)
{
return new LocalChangesNode
{
Type = "Folder",
FullPath = folderPath,
Contents = new()
};
}
public static LocalChangesNode FromWorkspaceFile(LocalFileTreeAccessor.ChangeRecord file)
{
return new LocalChangesNode
{
Type = file.Type.ToString(),
FullPath = file.File.WorkPath,
ModifiedTime = file.File.ModifyTime
};
}
}
public partial class RepositoryViewModel : RoutableViewModelBase
{
public RepositoryModel Repository { get; }
public RepositoryLocalDatabaseModel LocalDatabase { get; }
public HierarchicalTreeDataGridSource<LocalChangesNode> LocalChange { get; }
public ObservableCollection<LocalChangesNode> LocalChangeSetRaw { get; } = new();
public UserModel User { get; }
[Reactive] private bool _autoDetectChanges = true;
[ReactiveCommand]
private async Task GoBackAsync()
{
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole;
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
{
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
LocalChangeSetRaw.Add(new LocalChangesNode
{
Type = "Add",
FullPath = "test.md",
ModifiedTime = DateTime.Now,
});
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
{
Columns =
{
new CheckBoxColumn<LocalChangesNode>(
string.Empty, n => n.Included, (n, v) => n.Included = v),
new TextColumn<LocalChangesNode, string>(
"Change",
n => n.Contents != null ? String.Empty : n.Type),
new HierarchicalExpanderColumn<LocalChangesNode>(
new TextColumn<LocalChangesNode, string>(
"Name",
n => Path.GetFileName(n.FullPath)),
n => n.Contents),
new TextColumn<LocalChangesNode, string>(
"File Type",
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
new TextColumn<LocalChangesNode, ulong>(
"Size",
n => 0),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
// Do refresh when entered
// DetectLocalChangesAsyncCommand.Execute();
}
[ReactiveCommand]
private async Task CloseRepositoryAsync()
{
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[ReactiveCommand]
private void RefreshRepositoryRoleInfo()
{
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 ValueTask DetectLocalChangesAsync()
{
var ns = await Task.Run(() =>
{
LocalDatabase.LocalAccessor.Refresh();
// Generate a map of all folders
var folderMap = new Dictionary<string, LocalChangesNode>();
foreach (var k in LocalDatabase.LocalAccessor.Changes.Keys)
AddParentToMap(k);
var nodes = new List<LocalChangesNode>();
foreach (var file in LocalDatabase.LocalAccessor.Changes.Values)
{
var directory = Path.GetDirectoryName(file.File.WorkPath);
var n = LocalChangesNode.FromWorkspaceFile(file);
if (string.IsNullOrEmpty(directory)) nodes.Add(n);
else folderMap[directory].Contents!.Add(n);
}
nodes.AddRange(folderMap.Values);
return nodes;
void AddParentToMap(string path)
{
var parent = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(parent) || folderMap.ContainsKey(parent)) return;
folderMap.Add(parent, LocalChangesNode.FromFolder(parent));
}
});
LocalChangeSetRaw.Clear();
LocalChangeSetRaw.AddRange(ns);
}
}

View File

@ -19,7 +19,7 @@
<TextBox Text="{Binding RepositoryName}"/>
</u:FormItem>
<u:FormItem Grid.Row="1" Label="Description">
<TextBox MinHeight="40" MaxHeight="80" Text="{Binding Description}"/>
<TextBox MinHeight="40" MaxHeight="80" Text="{Binding Description}"/>
</u:FormItem>
</u:Form>
</UserControl>

View File

@ -10,20 +10,22 @@
<Grid ColumnDefinitions="2*, *">
<Border Grid.Column="0" Classes="Shadow" Theme="{StaticResource CardBorder}">
<StackPanel>
<Label Content="No Readme File Existed"/>
<!-- <md:MarkdownScrollViewer Markdown="## Hello World!!"/> -->
</StackPanel>
<Grid RowDefinitions="Auto, *">
<StackPanel Orientation="Horizontal" Spacing="8"
IsVisible="{Binding IsOwnerRole}">
<u:IconButton Icon="{StaticResource SemiIconEdit}" Content="Edit"/>
</StackPanel>
<ScrollViewer Grid.Row="1">
<SelectableTextBlock Text="{Binding Repository.Description}"/>
</ScrollViewer>
</Grid>
</Border>
<ScrollViewer Grid.Column="1" Margin="36 20">
<StackPanel Orientation="Vertical">
<u:Timeline
HorizontalAlignment="Left"
Mode="Left">
<u:TimelineItem Header="New Commit"/>
<u:TimelineItem/>
<u:TimelineItem/>
<u:TimelineItem/>
</u:Timeline>
</StackPanel>
</ScrollViewer>

View File

@ -2,51 +2,33 @@
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.RepoWorkspacePageView">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Spacing="6" Margin="8">
<StackPanel Orientation="Horizontal">
<Label Content="Commit Message"/>
</StackPanel>
<TextBox Height="80"/>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
</StackPanel>
<Grid ColumnDefinitions="Auto, *, Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Sync"></Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<SplitButton Content="Commit">
<Grid ColumnDefinitions="3*, 10, 2*" RowDefinitions="Auto, *">
<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"/>
<ToggleButton Content="Auto Refresh" IsChecked="{Binding AutoDetectChanges}"/>
</StackPanel>
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">
<Grid RowDefinitions="*, Auto">
<u:Form HorizontalAlignment="Stretch">
<TextBox Text="{Binding LocalDatabase.CommitMessage}"
Classes="TextArea" MaxHeight="300" Watermark="Description of this commit"/>
<SplitButton HorizontalAlignment="Stretch" Content="Commit">
<SplitButton.Flyout>
<MenuFlyout Placement="Top">
<MenuItem Header="Lock"/>
<MenuFlyout Placement="TopEdgeAlignedRight">
<MenuItem Header="Force Commit as Baseline"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</StackPanel>
</Grid>
</StackPanel>
<Border Classes="Shadow" Theme="{StaticResource CardBorder}">
<Grid ColumnDefinitions="*, 8, *">
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="8">
<Grid RowDefinitions="Auto, Auto">
<Label Grid.Row="0" FontSize="12" Content="Changes" HorizontalAlignment="Left"/>
<TreeDataGrid Grid.Row="1">
</TreeDataGrid>
</Grid>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Vertical" Spacing="8">
<Grid RowDefinitions="Auto, Auto">
<Label Grid.Row="0" FontSize="12" Content="Ready" HorizontalAlignment="Right"/>
<TreeDataGrid Grid.Row="1">
</TreeDataGrid>
</Grid>
</StackPanel>
</u:Form>
</Grid>
</Border>
</DockPanel>
</Grid>
</UserControl>

View File

@ -16,7 +16,7 @@
Command="{Binding GoBackCommand}" Content="All Repositories"/>
<Label FontWeight="400" FontSize="28" Content="{Binding Repository.StandaloneName}"/>
</StackPanel>
<TabControl TabStripPlacement="Top" Margin="0 20">
<TabControl TabStripPlacement="Top" Margin="0 20 0 0">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
@ -24,16 +24,16 @@
<Label Content="Dashboard"/>
</StackPanel>
</TabItem.Header>
<page:RepoDashboardPageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoDashboardPageView Margin="18"/>
</TabItem>
<TabItem>
<TabItem IsVisible="{Binding IsDeveloperRole}">
<TabItem.Header>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconSourceControl}"/>
<Label Content="Workspace"/>
</StackPanel>
</TabItem.Header>
<page:RepoWorkspacePageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoWorkspacePageView Margin="18"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -42,34 +42,34 @@
<Label Content="File Tree"/>
</StackPanel>
</TabItem.Header>
<page:RepoFileTreePageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoFileTreePageView Margin="18"/>
</TabItem>
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconBrackets}"/>
<Label Content="Commit"/>
<Label Content="Commit History"/>
</StackPanel>
</TabItem.Header>
<page:RepoCommitPageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoCommitPageView Margin="18"/>
</TabItem>
<TabItem>
<TabItem IsVisible="{Binding IsReporterRole}">
<TabItem.Header>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconConnectionPoint1}"/>
<Label Content="Issue"/>
</StackPanel>
</TabItem.Header>
<page:RepoIssuePageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoIssuePageView Margin="18"/>
</TabItem>
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconSetting}"/>
<Label Content="Setting"/>
</StackPanel>
</TabItem.Header>
<page:RepoSettingPageView Margin="18" DataContext="{Binding $self}"/>
<page:RepoSettingPageView Margin="18"/>
</TabItem>
</TabControl>
</DockPanel>