From 0b1c0be0f06ff8f2145a0615ceae70890b974ad9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 6 Dec 2018 14:17:28 -0500 Subject: [PATCH] support for showing groupings on ciphers list page --- src/App/Controls/VaultGroupingViewCell.cs | 2 + src/App/Models/Page/VaultListPageModel.cs | 7 +- src/App/Pages/Vault/VaultListCiphersPage.cs | 218 +++++++++++++------- 3 files changed, 152 insertions(+), 75 deletions(-) diff --git a/src/App/Controls/VaultGroupingViewCell.cs b/src/App/Controls/VaultGroupingViewCell.cs index cc71dbacc..ca0eb2d5a 100644 --- a/src/App/Controls/VaultGroupingViewCell.cs +++ b/src/App/Controls/VaultGroupingViewCell.cs @@ -36,6 +36,8 @@ namespace Bit.App.Controls }; CountLabel.SetBinding(Label.TextProperty, string.Format("{0}.Node.{1}", nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.Count))); + CountLabel.SetBinding(VisualElement.IsVisibleProperty, string.Format("{0}.Node.{1}", + nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.ShowCount))); var stackLayout = new StackLayout { diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index c0edc9335..e5d676f07 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -217,7 +217,7 @@ namespace Bit.App.Models.Page Count = count; } - public Grouping(Folder folder, int count) + public Grouping(Folder folder, int? count) { Id = folder.Id; Name = folder.Name?.Decrypt(); @@ -225,7 +225,7 @@ namespace Bit.App.Models.Page Count = count; } - public Grouping(Collection collection, int count) + public Grouping(Collection collection, int? count) { Id = collection.Id; Name = collection.Name?.Decrypt(collection.OrganizationId); @@ -235,9 +235,10 @@ namespace Bit.App.Models.Page public string Id { get; set; } public string Name { get; set; } = AppResources.FolderNone; - public int Count { get; set; } + public int? Count { get; set; } public bool Folder { get; set; } public bool Collection { get; set; } + public bool ShowCount => Count.HasValue; } } } diff --git a/src/App/Pages/Vault/VaultListCiphersPage.cs b/src/App/Pages/Vault/VaultListCiphersPage.cs index 0a6f968a6..69ce84ee2 100644 --- a/src/App/Pages/Vault/VaultListCiphersPage.cs +++ b/src/App/Pages/Vault/VaultListCiphersPage.cs @@ -25,6 +25,8 @@ namespace Bit.App.Pages private readonly IAppSettingsService _appSettingsService; private readonly IGoogleAnalyticsService _googleAnalyticsService; private readonly IDeviceActionService _deviceActionService; + private readonly IFolderService _folderService; + private readonly ICollectionService _collectionService; private CancellationTokenSource _filterResultsCancellationTokenSource; private readonly bool _favorites = false; private readonly bool _folder = false; @@ -52,13 +54,16 @@ namespace Bit.App.Pages _appSettingsService = Resolver.Resolve(); _googleAnalyticsService = Resolver.Resolve(); _deviceActionService = Resolver.Resolve(); + _folderService = Resolver.Resolve(); + _collectionService = Resolver.Resolve(); Init(); } - public ExtendedObservableCollection> PresentationSections { get; private set; } - = new ExtendedObservableCollection>(); + public ExtendedObservableCollection> PresentationSections { get; private set; } + = new ExtendedObservableCollection>(); public Cipher[] Ciphers { get; set; } = new Cipher[] { }; + public GroupingOrCipher[] Groupings { get; set; } = new GroupingOrCipher[] { }; public ExtendedListView ListView { get; set; } public SearchBar Search { get; set; } public ActivityIndicator LoadingIndicator { get; set; } @@ -75,11 +80,10 @@ namespace Bit.App.Pages IsGroupingEnabled = true, ItemsSource = PresentationSections, HasUnevenRows = true, - GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(nameof(Section.Name), - nameof(Section.Count))), - GroupShortNameBinding = new Binding(nameof(Section.Name)), - ItemTemplate = new DataTemplate(() => new VaultListViewCell( - (Cipher c) => Helpers.CipherMoreClickedAsync(this, c, !string.IsNullOrWhiteSpace(_uri)))) + GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( + nameof(Section.Name), nameof(Section.Count))), + GroupShortNameBinding = new Binding(nameof(Section.Name)), + ItemTemplate = new GroupingOrCipherDataTemplateSelector(this) }; if(Device.RuntimePlatform == Device.iOS) @@ -250,7 +254,7 @@ namespace Bit.App.Pages if(string.IsNullOrWhiteSpace(searchFilter)) { - LoadSections(Ciphers, ct); + LoadSections(Ciphers, Groupings, ct); } else { @@ -263,7 +267,7 @@ namespace Bit.App.Pages .ToArray(); ct.ThrowIfCancellationRequested(); - LoadSections(filteredCiphers, ct); + LoadSections(filteredCiphers, null, ct); } } @@ -291,7 +295,7 @@ namespace Bit.App.Pages }); AddCipherItem?.InitEvents(); - ListView.ItemSelected += CipherSelected; + ListView.ItemSelected += GroupingOrCipherSelected; Search.TextChanged += SearchBar_TextChanged; Search.SearchButtonPressed += SearchBar_SearchButtonPressed; _filterResultsCancellationTokenSource = FetchAndLoadVault(); @@ -303,7 +307,7 @@ namespace Bit.App.Pages MessagingCenter.Unsubscribe(Application.Current, "SyncCompleted"); AddCipherItem?.Dispose(); - ListView.ItemSelected -= CipherSelected; + ListView.ItemSelected -= GroupingOrCipherSelected; Search.TextChanged -= SearchBar_TextChanged; Search.SearchButtonPressed -= SearchBar_SearchButtonPressed; } @@ -324,10 +328,28 @@ namespace Bit.App.Pages if(_folder || !string.IsNullOrWhiteSpace(_folderId)) { ciphers = await _cipherService.GetAllByFolderAsync(_folderId); + + var folders = await _folderService.GetAllAsync(); + var fGroupings = folders.Select(f => new Grouping(f, null)).OrderBy(g => g.Name).ToList(); + var fTreeNodes = Helpers.GetAllNested(fGroupings); + var fTreeNode = Helpers.GetTreeNodeObject(fTreeNodes, _folderId); + if(fTreeNode.Children?.Any() ?? false) + { + Groupings = fTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray(); + } } else if(!string.IsNullOrWhiteSpace(_collectionId)) { ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId); + + var collections = await _collectionService.GetAllAsync(); + var cGroupings = collections.Select(c => new Grouping(c, null)).OrderBy(g => g.Name).ToList(); + var cTreeNodes = Helpers.GetAllNested(cGroupings); + var cTreeNode = Helpers.GetTreeNodeObject(cTreeNodes, _collectionId); + if(cTreeNode.Children?.Any() ?? false) + { + Groupings = cTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray(); + } } else if(_favorites) { @@ -363,11 +385,20 @@ namespace Bit.App.Pages return cts; } - private void LoadSections(Cipher[] ciphers, CancellationToken ct) + private void LoadSections(Cipher[] ciphers, GroupingOrCipher[] groupings, CancellationToken ct) { ct.ThrowIfCancellationRequested(); + var sections = ciphers.GroupBy(c => c.NameGroup.ToUpperInvariant()) - .Select(g => new Section(g.ToList(), g.Key)); + .Select(g => new Section(g.Select(g2 => new GroupingOrCipher(g2)).ToList(), g.Key)) + .ToList(); + + if(groupings?.Any() ?? false) + { + sections.Insert(0, new Section(groupings.ToList(), + _folder ? AppResources.Folders : AppResources.Collections)); + } + ct.ThrowIfCancellationRequested(); Device.BeginInvokeOnMainThread(() => { @@ -393,79 +424,122 @@ namespace Bit.App.Pages }); } - private async void CipherSelected(object sender, SelectedItemChangedEventArgs e) + private async void GroupingOrCipherSelected(object sender, SelectedItemChangedEventArgs e) { - var cipher = e.SelectedItem as Cipher; - if(cipher == null) + var groupingOrCipher = e.SelectedItem as GroupingOrCipher; + if(groupingOrCipher == null) { return; } - string selection = null; - if(!string.IsNullOrWhiteSpace(_uri)) + if(groupingOrCipher.Grouping != null) { - var options = new List { AppResources.Autofill }; - if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected) + Page page; + if(groupingOrCipher.Grouping.Node.Folder) { - options.Add(AppResources.AutofillAndSave); - } - options.Add(AppResources.View); - selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null, - options.ToArray()); - } - - if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri)) - { - var page = new VaultViewCipherPage(cipher.Type, cipher.Id); - await Navigation.PushForDeviceAsync(page); - } - else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) - { - if(selection == AppResources.AutofillAndSave) - { - if(!_connectivity.IsConnected) - { - Helpers.AlertNoConnection(this); - } - else - { - var uris = cipher.CipherModel.Login?.Uris?.ToList(); - if(uris == null) - { - uris = new List(); - } - - uris.Add(new Models.LoginUri - { - Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId), - Match = null - }); - - cipher.CipherModel.Login.Uris = uris; - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); - await _deviceActionService.HideLoadingAsync(); - if(saveTask.Succeeded) - { - _googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); - } - } - } - - if(_deviceInfoService.Version < 21) - { - Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri)); + page = new VaultListCiphersPage(folder: true, + folderId: groupingOrCipher.Grouping.Node.Id, groupingName: groupingOrCipher.Grouping.Node.Name); } else { - _googleAnalyticsService.TrackExtensionEvent("AutoFilled", - _uri.StartsWith("http") ? "Website" : "App"); - _deviceActionService.Autofill(cipher); + page = new VaultListCiphersPage(collectionId: groupingOrCipher.Grouping.Node.Id, + groupingName: groupingOrCipher.Grouping.Node.Name); + } + + await Navigation.PushAsync(page); + } + else if(groupingOrCipher.Cipher != null) + { + var cipher = groupingOrCipher.Cipher; + string selection = null; + if(!string.IsNullOrWhiteSpace(_uri)) + { + var options = new List { AppResources.Autofill }; + if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected) + { + options.Add(AppResources.AutofillAndSave); + } + options.Add(AppResources.View); + selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null, + options.ToArray()); + } + + if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri)) + { + var page = new VaultViewCipherPage(cipher.Type, cipher.Id); + await Navigation.PushForDeviceAsync(page); + } + else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) + { + if(selection == AppResources.AutofillAndSave) + { + if(!_connectivity.IsConnected) + { + Helpers.AlertNoConnection(this); + } + else + { + var uris = cipher.CipherModel.Login?.Uris?.ToList(); + if(uris == null) + { + uris = new List(); + } + + uris.Add(new Models.LoginUri + { + Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId), + Match = null + }); + + cipher.CipherModel.Login.Uris = uris; + + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); + await _deviceActionService.HideLoadingAsync(); + if(saveTask.Succeeded) + { + _googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); + } + } + } + + if(_deviceInfoService.Version < 21) + { + Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri)); + } + else + { + _googleAnalyticsService.TrackExtensionEvent("AutoFilled", + _uri.StartsWith("http") ? "Website" : "App"); + _deviceActionService.Autofill(cipher); + } } } ((ListView)sender).SelectedItem = null; } + + public class GroupingOrCipherDataTemplateSelector : DataTemplateSelector + { + public GroupingOrCipherDataTemplateSelector(VaultListCiphersPage page) + { + GroupingTemplate = new DataTemplate(() => new VaultGroupingViewCell()); + CipherTemplate = new DataTemplate(() => new VaultListViewCell( + (Cipher c) => Helpers.CipherMoreClickedAsync(page, c, !string.IsNullOrWhiteSpace(page._uri)), + true)); + } + + public DataTemplate GroupingTemplate { get; set; } + public DataTemplate CipherTemplate { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + if(item == null) + { + return null; + } + return ((GroupingOrCipher)item).Cipher == null ? GroupingTemplate : CipherTemplate; + } + } } }