share page

This commit is contained in:
Kyle Spearrin 2019-05-10 13:22:35 -04:00
parent 9aef584494
commit 9668bd85c1
10 changed files with 384 additions and 17 deletions

View file

@ -38,6 +38,9 @@
<Compile Update="Pages\Generator\GeneratorPage.xaml.cs">
<DependentUpon>GeneratorPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\SharePage.xaml.cs">
<DependentUpon>SharePage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\CiphersPage.xaml.cs">
<DependentUpon>CiphersPage.xaml</DependentUpon>
</Compile>

View file

@ -0,0 +1,16 @@
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public class CollectionViewModel : ExtendedViewModel
{
private bool _checked;
public Core.Models.View.CollectionView Collection { get; set; }
public bool Checked
{
get => _checked;
set => SetProperty(ref _checked, value);
}
}
}

View file

@ -16,6 +16,9 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
<ToolbarItem Text="{u:I18n Attachments}" Clicked="Attachments_Clicked" Order="Secondary" />
<ToolbarItem Text="{u:I18n Share}" Clicked="Share_Clicked" Order="Secondary" />
<ToolbarItem Text="{u:I18n Delete}" Clicked="Delete_Clicked" Order="Secondary" IsDestructive="True" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
@ -534,7 +537,7 @@
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:AddEditPageCollectionViewModel">
<DataTemplate x:DataType="pages:CollectionViewModel">
<StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-switch">
<Label

View file

@ -76,5 +76,22 @@ namespace Bit.App.Pages
// await Navigation.PushModalAsync();
}
}
private async void Share_Clicked(object sender, System.EventArgs e)
{
if(DoOnce())
{
var page = new SharePage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
private async void Delete_Clicked(object sender, System.EventArgs e)
{
if(DoOnce())
{
await _vm.DeleteAsync();
}
}
}
}

View file

@ -80,7 +80,7 @@ namespace Bit.App.Pages
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<AddEditPageCollectionViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
TypeOptions = new List<KeyValuePair<string, CipherType>>
{
@ -149,7 +149,7 @@ namespace Bit.App.Pages
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<AddEditPageCollectionViewModel> Collections { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int TypeSelectedIndex
{
get => _typeSelectedIndex;
@ -360,6 +360,13 @@ namespace Bit.App.Pages
if(!EditMode && Cipher.OrganizationId != null)
{
if(!Collections?.Any(c => c.Checked) ?? true)
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
AppResources.Ok);
return false;
}
Cipher.CollectionIds = Collections.Any() ?
new HashSet<string>(Collections.Where(c => c.Checked).Select(c => c.Collection.Id)) : null;
}
@ -575,12 +582,12 @@ namespace Bit.App.Pages
if(Cipher.OrganizationId != null)
{
var cols = _writeableCollections.Where(c => c.OrganizationId == Cipher.OrganizationId)
.Select(c => new AddEditPageCollectionViewModel { Collection = c }).ToList();
.Select(c => new CollectionViewModel { Collection = c }).ToList();
Collections.ResetWithRange(cols);
}
else
{
Collections.ResetWithRange(new List<AddEditPageCollectionViewModel>());
Collections.ResetWithRange(new List<CollectionViewModel>());
}
HasCollections = Collections.Any();
}
@ -615,18 +622,6 @@ namespace Bit.App.Pages
}
}
public class AddEditPageCollectionViewModel : ExtendedViewModel
{
private bool _checked;
public Core.Models.View.CollectionView Collection { get; set; }
public bool Checked
{
get => _checked;
set => SetProperty(ref _checked, value);
}
}
public class AddEditPageFieldViewModel : ExtendedViewModel
{
private FieldView _field;

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SharePage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:DataType="pages:SharePageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SharePageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IsNotNullConverter x:Key="notNull" />
</ResourceDictionary>
</ContentPage.Resources>
<ScrollView x:Name="_scrollView">
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row"
IsVisible="{Binding HasOrganizations, Converter={StaticResource inverseBool}}">
<Label Text="{u:I18n NoOrganizationsToList}" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n Organization}"
StyleClass="box-label" />
<Picker
x:Name="_organizationPicker"
ItemsSource="{Binding OrganizationOptions, Mode=OneTime}"
SelectedIndex="{Binding OrganizationSelectedIndex}"
StyleClass="box-value" />
</StackLayout>
<Label
Text="{u:I18n ShareDesc}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box"
IsVisible="{Binding OrganizationId, Converter={StaticResource notNull}}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Collections}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row"
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}">
<Label Text="{u:I18n NoCollectionsToList}" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CollectionViewModel">
<StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{Binding Collection.Name}"
StyleClass="box-label, box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Checked}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View file

@ -0,0 +1,38 @@
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SharePage : BaseContentPage
{
private SharePageViewModel _vm;
public SharePage(string cipherId = null)
{
InitializeComponent();
_vm = BindingContext as SharePageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
SetActivityIndicator();
_organizationPicker.ItemDisplayBinding = new Binding("Key");
}
protected override async void OnAppearing()
{
base.OnAppearing();
await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync());
}
protected override void OnDisappearing()
{
base.OnDisappearing();
}
private async void Save_Clicked(object sender, System.EventArgs e)
{
if(DoOnce())
{
await _vm.SubmitAsync();
}
}
}
}

View file

@ -0,0 +1,142 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Bit.App.Pages
{
public class SharePageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly ICollectionService _collectionService;
private readonly IUserService _userService;
private readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher;
private int _organizationSelectedIndex;
private bool _hasCollections;
private bool _hasOrganizations;
private List<Core.Models.View.CollectionView> _writeableCollections;
public SharePageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
Collections = new ExtendedObservableCollection<CollectionViewModel>();
OrganizationOptions = new List<KeyValuePair<string, string>>();
PageTitle = AppResources.Share;
}
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public List<KeyValuePair<string, string>> OrganizationOptions { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int OrganizationSelectedIndex
{
get => _organizationSelectedIndex;
set
{
if(SetProperty(ref _organizationSelectedIndex, value))
{
OrganizationChanged();
}
}
}
public bool HasCollections
{
get => _hasCollections;
set => SetProperty(ref _hasCollections, value);
}
public bool HasOrganizations
{
get => _hasOrganizations;
set => SetProperty(ref _hasOrganizations, value);
}
public async Task LoadAsync()
{
var allCollections = await _collectionService.GetAllDecryptedAsync();
_writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList();
var orgs = await _userService.GetAllOrganizationAsync();
OrganizationOptions = orgs.OrderBy(o => o.Name)
.Where(o => o.Enabled && o.Status == OrganizationUserStatusType.Confirmed)
.Select(o => new KeyValuePair<string, string>(o.Name, o.Id)).ToList();
HasOrganizations = OrganizationOptions.Any();
var cipherDomain = await _cipherService.GetAsync(CipherId);
_cipher = await cipherDomain.DecryptAsync();
if(OrganizationId == null && OrganizationOptions.Any())
{
OrganizationId = OrganizationOptions.First().Value;
}
OrganizationSelectedIndex = string.IsNullOrWhiteSpace(OrganizationId) ? 0 :
OrganizationOptions.FindIndex(k => k.Value == OrganizationId);
FilterCollections();
}
public async Task<bool> SubmitAsync()
{
if(!Collections?.Any(c => c.Checked) ?? true)
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
AppResources.Ok);
return false;
}
var cipherDomain = await _cipherService.GetAsync(CipherId);
var cipherView = await cipherDomain.DecryptAsync();
var checkedCollectionIds = new HashSet<string>(
Collections.Where(c => c.Checked).Select(c => c.Collection.Id));
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.ItemShared);
await Page.Navigation.PopModalAsync();
return true;
}
catch(ApiException e)
{
await _deviceActionService.HideLoadingAsync();
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
}
return false;
}
private void OrganizationChanged()
{
if(OrganizationSelectedIndex > -1)
{
OrganizationId = OrganizationOptions[OrganizationSelectedIndex].Value;
FilterCollections();
}
}
private void FilterCollections()
{
if(OrganizationId == null || !_writeableCollections.Any())
{
Collections.ResetWithRange(new List<CollectionViewModel>());
}
else
{
var cols = _writeableCollections.Where(c => c.OrganizationId == OrganizationId)
.Select(c => new CollectionViewModel { Collection = c }).ToList();
Collections.ResetWithRange(cols);
}
HasCollections = Collections.Any();
}
}
}

View file

@ -1905,6 +1905,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Item has been shared..
/// </summary>
public static string ItemShared {
get {
return ResourceManager.GetString("ItemShared", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Item updated..
/// </summary>
@ -2517,6 +2526,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to No organizations to list..
/// </summary>
public static string NoOrgsToList {
get {
return ResourceManager.GetString("NoOrgsToList", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No passwords to list..
/// </summary>
@ -2985,6 +3003,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to You must select at least one collection..
/// </summary>
public static string SelectOneCollection {
get {
return ResourceManager.GetString("SelectOneCollection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to What type of item do you want to add?.
/// </summary>
@ -3075,6 +3102,33 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Share.
/// </summary>
public static string Share {
get {
return ResourceManager.GetString("Share", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared..
/// </summary>
public static string ShareDesc {
get {
return ResourceManager.GetString("ShareDesc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Share Item.
/// </summary>
public static string ShareItem {
get {
return ResourceManager.GetString("ShareItem", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Share Your Vault.
/// </summary>

View file

@ -1429,4 +1429,22 @@
<data name="NoCollectionsToList" xml:space="preserve">
<value>There are no collections to list.</value>
</data>
<data name="ItemShared" xml:space="preserve">
<value>Item has been shared.</value>
</data>
<data name="SelectOneCollection" xml:space="preserve">
<value>You must select at least one collection.</value>
</data>
<data name="Share" xml:space="preserve">
<value>Share</value>
</data>
<data name="ShareItem" xml:space="preserve">
<value>Share Item</value>
</data>
<data name="NoOrgsToList" xml:space="preserve">
<value>No organizations to list.</value>
</data>
<data name="ShareDesc" xml:space="preserve">
<value>Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared.</value>
</data>
</root>