mirror of
https://github.com/bitwarden/web
synced 2025-12-16 16:23:31 +00:00
manage item collections
This commit is contained in:
2
jslib
2
jslib
Submodule jslib updated: b3f71ed8e4...1021f23277
@@ -36,6 +36,7 @@ import { ToolsComponent } from './tools/tools.component';
|
|||||||
import { AddEditComponent } from './vault/add-edit.component';
|
import { AddEditComponent } from './vault/add-edit.component';
|
||||||
import { AttachmentsComponent } from './vault/attachments.component';
|
import { AttachmentsComponent } from './vault/attachments.component';
|
||||||
import { CiphersComponent } from './vault/ciphers.component';
|
import { CiphersComponent } from './vault/ciphers.component';
|
||||||
|
import { CollectionsComponent } from './vault/collections.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';
|
||||||
@@ -81,6 +82,7 @@ import { Folder } from 'jslib/models/domain';
|
|||||||
BlurClickDirective,
|
BlurClickDirective,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
CiphersComponent,
|
CiphersComponent,
|
||||||
|
CollectionsComponent,
|
||||||
ExportComponent,
|
ExportComponent,
|
||||||
FallbackSrcDirective,
|
FallbackSrcDirective,
|
||||||
FolderAddEditComponent,
|
FolderAddEditComponent,
|
||||||
@@ -113,6 +115,7 @@ import { Folder } from 'jslib/models/domain';
|
|||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
|
CollectionsComponent,
|
||||||
FolderAddEditComponent,
|
FolderAddEditComponent,
|
||||||
ModalComponent,
|
ModalComponent,
|
||||||
ShareComponent,
|
ShareComponent,
|
||||||
|
|||||||
48
src/app/vault/collections.component.html
Normal file
48
src/app/vault/collections.component.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<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">
|
||||||
|
{{'collections' | 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>{{'collectionsDesc' | i18n}}</p>
|
||||||
|
<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>
|
||||||
85
src/app/vault/collections.component.ts
Normal file
85
src/app/vault/collections.component.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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 { CipherView } from 'jslib/models/view/cipherView';
|
||||||
|
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||||
|
|
||||||
|
import { Cipher } from 'jslib/models/domain/cipher';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vault-collections',
|
||||||
|
templateUrl: 'collections.component.html',
|
||||||
|
})
|
||||||
|
export class CollectionsComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Output() onSavedCollections = new EventEmitter();
|
||||||
|
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
cipher: CipherView;
|
||||||
|
collections: CollectionView[] = [];
|
||||||
|
|
||||||
|
private cipherDomain: Cipher;
|
||||||
|
|
||||||
|
constructor(private collectionService: CollectionService, private analytics: Angulartics2,
|
||||||
|
private toasterService: ToasterService, private i18nService: I18nService,
|
||||||
|
private cipherService: CipherService) { }
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.cipherDomain = await this.cipherService.get(this.cipherId);
|
||||||
|
this.cipher = await this.cipherDomain.decrypt();
|
||||||
|
const allCollections = await this.collectionService.getAllDecrypted();
|
||||||
|
this.collections = allCollections.filter((c) =>
|
||||||
|
!c.readOnly && c.organizationId === this.cipher.organizationId);
|
||||||
|
|
||||||
|
this.unselectAll();
|
||||||
|
if (this.cipherDomain.collectionIds != null) {
|
||||||
|
for (const collection of this.collections) {
|
||||||
|
(collection as any).checked = this.cipherDomain.collectionIds.indexOf(collection.id) > -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.unselectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
this.cipherDomain.collectionIds = this.collections
|
||||||
|
.filter((c) => !!(c as any).checked)
|
||||||
|
.map((c) => c.id);
|
||||||
|
this.formPromise = this.cipherService.saveCollectionsWithServer(this.cipherDomain);
|
||||||
|
await this.formPromise;
|
||||||
|
this.onSavedCollections.emit();
|
||||||
|
this.analytics.eventTrack.next({ action: 'Edited Cipher Collections' });
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('editedItem'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.collections) {
|
||||||
|
(c as any).checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</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)">
|
(onShareClicked)="shareCipher($event)" (onCollectionsClicked)="editCipherCollections($event)">
|
||||||
</app-vault-ciphers>
|
</app-vault-ciphers>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ import { ModalComponent } from '../modal.component';
|
|||||||
import { AddEditComponent } from './add-edit.component';
|
import { AddEditComponent } from './add-edit.component';
|
||||||
import { AttachmentsComponent } from './attachments.component';
|
import { AttachmentsComponent } from './attachments.component';
|
||||||
import { CiphersComponent } from './ciphers.component';
|
import { CiphersComponent } from './ciphers.component';
|
||||||
|
import { CollectionsComponent } from './collections.component';
|
||||||
import { FolderAddEditComponent } from './folder-add-edit.component';
|
import { FolderAddEditComponent } from './folder-add-edit.component';
|
||||||
import { GroupingsComponent } from './groupings.component';
|
import { GroupingsComponent } from './groupings.component';
|
||||||
import { OrganizationsComponent } from './organizations.component';
|
import { OrganizationsComponent } from './organizations.component';
|
||||||
|
import { ShareComponent } from './share.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',
|
||||||
@@ -177,6 +178,26 @@ export class VaultComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCipherCollections(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<CollectionsComponent>(CollectionsComponent, this.collectionsModalRef);
|
||||||
|
|
||||||
|
childComponent.cipherId = cipher.id;
|
||||||
|
childComponent.onSavedCollections.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();
|
||||||
|
|||||||
@@ -660,6 +660,9 @@
|
|||||||
"message": "Organizations"
|
"message": "Organizations"
|
||||||
},
|
},
|
||||||
"shareDesc": {
|
"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."
|
"message": "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."
|
||||||
|
},
|
||||||
|
"collectionsDesc": {
|
||||||
|
"message": "Edit the collections that this item is being shared with. Only organization users with access to these collections will be able to see this item."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user