mirror of
https://github.com/bitwarden/web
synced 2025-12-13 06:43:31 +00:00
sharing
This commit is contained in:
2
jslib
2
jslib
Submodule jslib updated: 4bd9a9fc11...b3f71ed8e4
@@ -3,7 +3,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">{{'twoStepOptions' | i18n}}</h2>
|
<h2 class="modal-title">{{'twoStepOptions' | i18n}}</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { CiphersComponent } from './vault/ciphers.component';
|
|||||||
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
|
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
|
||||||
import { GroupingsComponent } from './vault/groupings.component';
|
import { GroupingsComponent } from './vault/groupings.component';
|
||||||
import { OrganizationsComponent } from './vault/organizations.component';
|
import { OrganizationsComponent } from './vault/organizations.component';
|
||||||
|
import { ShareComponent } from './vault/share.component';
|
||||||
import { VaultComponent } from './vault/vault.component';
|
import { VaultComponent } from './vault/vault.component';
|
||||||
|
|
||||||
import { IconComponent } from 'jslib/angular/components/icon.component';
|
import { IconComponent } from 'jslib/angular/components/icon.component';
|
||||||
@@ -99,6 +100,7 @@ import { Folder } from 'jslib/models/domain';
|
|||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
RegisterComponent,
|
RegisterComponent,
|
||||||
SearchCiphersPipe,
|
SearchCiphersPipe,
|
||||||
|
ShareComponent,
|
||||||
StopClickDirective,
|
StopClickDirective,
|
||||||
StopPropDirective,
|
StopPropDirective,
|
||||||
ToolsComponent,
|
ToolsComponent,
|
||||||
@@ -113,6 +115,7 @@ import { Folder } from 'jslib/models/domain';
|
|||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
FolderAddEditComponent,
|
FolderAddEditComponent,
|
||||||
ModalComponent,
|
ModalComponent,
|
||||||
|
ShareComponent,
|
||||||
TwoFactorOptionsComponent,
|
TwoFactorOptionsComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">{{title}}</h2>
|
<h2 class="modal-title">{{title}}</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{{'attachments' | i18n}}
|
{{'attachments' | i18n}}
|
||||||
<small *ngIf="cipher">{{cipher.name}}</small>
|
<small *ngIf="cipher">{{cipher.name}}</small>
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { CipherView } from 'jslib/models/view/cipherView';
|
|||||||
})
|
})
|
||||||
export class CiphersComponent extends BaseCiphersComponent {
|
export class CiphersComponent extends BaseCiphersComponent {
|
||||||
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
|
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
|
||||||
|
@Output() onShareClicked = new EventEmitter<CipherView>();
|
||||||
|
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
|
||||||
cipherType = CipherType;
|
cipherType = CipherType;
|
||||||
|
|
||||||
constructor(cipherService: CipherService, private analytics: Angulartics2,
|
constructor(cipherService: CipherService, private analytics: Angulartics2,
|
||||||
@@ -40,11 +42,11 @@ export class CiphersComponent extends BaseCiphersComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
share(c: CipherView) {
|
share(c: CipherView) {
|
||||||
//
|
this.onShareClicked.emit(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
collections(c: CipherView) {
|
collections(c: CipherView) {
|
||||||
//
|
this.onCollectionsClicked.emit(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(c: CipherView) {
|
delete(c: CipherView) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">{{title}}</h2>
|
<h2 class="modal-title">{{title}}</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
54
src/app/vault/share.component.html
Normal file
54
src/app/vault/share.component.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<div class="modal fade">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">
|
||||||
|
{{'share' | i18n}}
|
||||||
|
<small *ngIf="cipher">{{cipher.name}}</small>
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{'shareDesc' | i18n}}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="organization">{{'organization' | i18n}}</label>
|
||||||
|
<select id="organization" name="OrganizationId" [(ngModel)]="organizationId" class="form-control" (change)="filterCollections()">
|
||||||
|
<option *ngFor="let o of organizations" [ngValue]="o.id">{{o.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<h3>{{'collections' | i18n}}</h3>
|
||||||
|
<small class="ml-auto d-flex">
|
||||||
|
<button type="button" appBlurClick (click)="selectAll()" class="btn btn-link btn-sm py-0">
|
||||||
|
{{'selectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button type="button" appBlurClick (click)="unselectAll()" class="btn btn-link btn-sm py-0">
|
||||||
|
{{'unselectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover table-list mb-0" *ngIf="collections">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||||
|
<td class="table-list-checkbox">
|
||||||
|
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span appStopProp>{{c.name}}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button appBlurClick type="submit" class="btn btn-primary" title="{{'save' | i18n}}" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
|
||||||
|
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal" title="{{'cancel' | i18n}}">{{'cancel' | i18n}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
113
src/app/vault/share.component.ts
Normal file
113
src/app/vault/share.component.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
import { Angulartics2 } from 'angulartics2';
|
||||||
|
|
||||||
|
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||||
|
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||||
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
import { UserService } from 'jslib/abstractions/user.service';
|
||||||
|
|
||||||
|
import { Organization } from 'jslib/models/domain/organization';
|
||||||
|
import { CipherView } from 'jslib/models/view/cipherView';
|
||||||
|
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vault-share',
|
||||||
|
templateUrl: 'share.component.html',
|
||||||
|
})
|
||||||
|
export class ShareComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Input() organizationId: string;
|
||||||
|
@Output() onSharedCipher = new EventEmitter();
|
||||||
|
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
cipher: CipherView;
|
||||||
|
collections: CollectionView[] = [];
|
||||||
|
organizations: Organization[] = [];
|
||||||
|
|
||||||
|
private writeableCollections: CollectionView[] = [];
|
||||||
|
|
||||||
|
constructor(private collectionService: CollectionService, private analytics: Angulartics2,
|
||||||
|
private toasterService: ToasterService, private i18nService: I18nService,
|
||||||
|
private userService: UserService, private cipherService: CipherService) { }
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||||
|
this.cipher = await cipherDomain.decrypt();
|
||||||
|
const allCollections = await this.collectionService.getAllDecrypted();
|
||||||
|
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
|
||||||
|
this.organizations = await this.userService.getAllOrganizations();
|
||||||
|
if (this.organizationId == null && this.organizations.length > 0) {
|
||||||
|
this.organizationId = this.organizations[0].id;
|
||||||
|
}
|
||||||
|
this.filterCollections();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.unselectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterCollections() {
|
||||||
|
this.unselectAll();
|
||||||
|
if (this.organizationId == null || this.writeableCollections.length === 0) {
|
||||||
|
this.collections = [];
|
||||||
|
} else {
|
||||||
|
this.collections = this.writeableCollections.filter((c) => c.organizationId === this.organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||||
|
const cipherView = await cipherDomain.decrypt();
|
||||||
|
|
||||||
|
const attachmentPromises: Array<Promise<any>> = [];
|
||||||
|
if (cipherView.attachments != null) {
|
||||||
|
for (const attachment of cipherView.attachments) {
|
||||||
|
const promise = this.cipherService.shareAttachmentWithServer(attachment,
|
||||||
|
cipherView.id, this.organizationId);
|
||||||
|
attachmentPromises.push(promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherView.organizationId = this.organizationId;
|
||||||
|
cipherView.collectionIds = [];
|
||||||
|
for (const collection of this.collections) {
|
||||||
|
if ((collection as any).checked) {
|
||||||
|
cipherView.collectionIds.push(collection.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formPromise = Promise.all(attachmentPromises).then(async () => {
|
||||||
|
const encCipher = await this.cipherService.encrypt(cipherView);
|
||||||
|
await this.cipherService.shareWithServer(encCipher);
|
||||||
|
this.onSharedCipher.emit();
|
||||||
|
this.analytics.eventTrack.next({ action: 'Shared Cipher' });
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('sharedItem'));
|
||||||
|
});
|
||||||
|
await this.formPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(c: CollectionView) {
|
||||||
|
(c as any).checked = !(c as any).checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
for (const c of this.collections) {
|
||||||
|
(c as any).checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unselectAll() {
|
||||||
|
for (const c of this.writeableCollections) {
|
||||||
|
(c as any).checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-vault-ciphers (onCipherClicked)="editCipher($event)" (onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()">
|
<app-vault-ciphers (onCipherClicked)="editCipher($event)" (onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||||
|
(onShareClicked)="shareCipher($event)">
|
||||||
</app-vault-ciphers>
|
</app-vault-ciphers>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
@@ -68,3 +69,5 @@
|
|||||||
<ng-template #attachments></ng-template>
|
<ng-template #attachments></ng-template>
|
||||||
<ng-template #folderAddEdit></ng-template>
|
<ng-template #folderAddEdit></ng-template>
|
||||||
<ng-template #cipherAddEdit></ng-template>
|
<ng-template #cipherAddEdit></ng-template>
|
||||||
|
<ng-template #share></ng-template>
|
||||||
|
<ng-template #collections></ng-template>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { OrganizationsComponent } from './organizations.component';
|
|||||||
|
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||||
|
import { ShareComponent } from './share.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-vault',
|
selector: 'app-vault',
|
||||||
@@ -38,7 +39,9 @@ export class VaultComponent implements OnInit {
|
|||||||
@ViewChild(OrganizationsComponent) organizationsComponent: OrganizationsComponent;
|
@ViewChild(OrganizationsComponent) organizationsComponent: OrganizationsComponent;
|
||||||
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
||||||
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
|
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
|
||||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditRef: ViewContainerRef;
|
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
|
||||||
|
|
||||||
cipherId: string = null;
|
cipherId: string = null;
|
||||||
favorites: boolean = false;
|
favorites: boolean = false;
|
||||||
@@ -154,6 +157,26 @@ export class VaultComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shareCipher(cipher: CipherView) {
|
||||||
|
if (this.modal != null) {
|
||||||
|
this.modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||||
|
this.modal = this.shareModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show<ShareComponent>(ShareComponent, this.shareModalRef);
|
||||||
|
|
||||||
|
childComponent.cipherId = cipher.id;
|
||||||
|
childComponent.onSharedCipher.subscribe(async () => {
|
||||||
|
this.modal.close();
|
||||||
|
await this.ciphersComponent.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.onClosed.subscribe(async () => {
|
||||||
|
this.modal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async addFolder() {
|
async addFolder() {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
@@ -212,9 +235,9 @@ export class VaultComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||||
this.modal = this.cipherAddEditRef.createComponent(factory).instance;
|
this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
|
||||||
const childComponent = this.modal.show<AddEditComponent>(
|
const childComponent = this.modal.show<AddEditComponent>(
|
||||||
AddEditComponent, this.cipherAddEditRef);
|
AddEditComponent, this.cipherAddEditModalRef);
|
||||||
|
|
||||||
childComponent.cipherId = cipher == null ? null : cipher.id;
|
childComponent.cipherId = cipher == null ? null : cipher.id;
|
||||||
childComponent.onSavedCipher.subscribe(async (c: CipherView) => {
|
childComponent.onSavedCipher.subscribe(async (c: CipherView) => {
|
||||||
|
|||||||
@@ -416,6 +416,9 @@
|
|||||||
"editedItem": {
|
"editedItem": {
|
||||||
"message": "Edited item"
|
"message": "Edited item"
|
||||||
},
|
},
|
||||||
|
"sharedItem": {
|
||||||
|
"message": "Shared item"
|
||||||
|
},
|
||||||
"deleteItem": {
|
"deleteItem": {
|
||||||
"message": "Delete Item"
|
"message": "Delete Item"
|
||||||
},
|
},
|
||||||
@@ -649,5 +652,14 @@
|
|||||||
},
|
},
|
||||||
"continue": {
|
"continue": {
|
||||||
"message": "Continue"
|
"message": "Continue"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"message": "Organization"
|
||||||
|
},
|
||||||
|
"organizations": {
|
||||||
|
"message": "Organizations"
|
||||||
|
},
|
||||||
|
"shareDesc": {
|
||||||
|
"message": "Choose an organization that you wish to share this item with. Sharing an item transfers ownership of that item to the organization. You will no longer be the direct owner of this item once it has been shared."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,21 +132,17 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
|
|
||||||
.swal-button {
|
button.swal-button {
|
||||||
@extend .btn;
|
@extend .btn;
|
||||||
|
|
||||||
&:focus {
|
&.swal-button--confirm {
|
||||||
box-shadow: none;
|
@extend .btn-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.swal-button--cancel {
|
||||||
|
@extend .btn-outline-secondary;
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.swal-button--confirm {
|
|
||||||
@extend .btn-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swal-button--cancel {
|
|
||||||
@extend .btn-outline-secondary;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user