diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index c50ca2337d..007f067644 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -12,6 +12,7 @@ import { ToasterModule } from 'angular2-toaster';
import { AddEditComponent } from './vault/add-edit.component';
import { AppComponent } from './app.component';
+import { AttachmentsComponent } from './vault/attachments.component';
import { BlurClickDirective } from './directives/blur-click.directive';
import { CiphersComponent } from './vault/ciphers.component';
import { FallbackSrcDirective } from './directives/fallback-src.directive';
@@ -43,6 +44,7 @@ import { ViewComponent } from './vault/view.component';
declarations: [
AddEditComponent,
AppComponent,
+ AttachmentsComponent,
BlurClickDirective,
CiphersComponent,
FallbackSrcDirective,
@@ -59,6 +61,7 @@ import { ViewComponent } from './vault/view.component';
ViewComponent,
],
entryComponents: [
+ AttachmentsComponent,
ModalComponent,
PasswordGeneratorComponent,
],
diff --git a/src/app/vault/add-edit.component.html b/src/app/vault/add-edit.component.html
index b79a9966bb..7618fe00df 100644
--- a/src/app/vault/add-edit.component.html
+++ b/src/app/vault/add-edit.component.html
@@ -185,9 +185,10 @@
-
- {{'attachments' | i18n}}
-
+
+ {{'attachments' | i18n}}
+
diff --git a/src/app/vault/attachments.component.html b/src/app/vault/attachments.component.html
new file mode 100644
index 0000000000..b56847b9d4
--- /dev/null
+++ b/src/app/vault/attachments.component.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+ {{a.fileName}}
+
+
{{a.sizeName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/vault/attachments.component.ts b/src/app/vault/attachments.component.ts
new file mode 100644
index 0000000000..574e77f418
--- /dev/null
+++ b/src/app/vault/attachments.component.ts
@@ -0,0 +1,102 @@
+import * as template from './attachments.component.html';
+
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnInit,
+ Output,
+} from '@angular/core';
+
+import { Angulartics2 } from 'angulartics2';
+import { ToasterService } from 'angular2-toaster';
+
+import { CipherService } from 'jslib/abstractions/cipher.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { TokenService } from 'jslib/abstractions/token.service';
+
+import { Cipher } from 'jslib/models/domain/cipher';
+
+import { CipherView } from 'jslib/models/view/cipherView';
+import { AttachmentView } from 'jslib/models/view/attachmentView';
+
+@Component({
+ selector: 'app-vault-attachments',
+ template: template,
+})
+export class AttachmentsComponent implements OnInit {
+ @Input() cipherId: string;
+
+ cipher: CipherView;
+ cipherDomain: Cipher;
+ hasUpdatedKey: boolean;
+ canAccessAttachments: boolean;
+
+ constructor(private cipherService: CipherService, private analytics: Angulartics2,
+ private toasterService: ToasterService, private i18nService: I18nService,
+ private cryptoService: CryptoService, private tokenService: TokenService) { }
+
+ async ngOnInit() {
+ this.cipherDomain = await this.cipherService.get(this.cipherId);
+ this.cipher = await this.cipherDomain.decrypt();
+
+ const key = await this.cryptoService.getEncKey();
+ this.hasUpdatedKey = key != null;
+ const isPremium = this.tokenService.getPremium();
+ this.canAccessAttachments = isPremium || this.cipher.organizationId != null;
+
+ if (!this.canAccessAttachments) {
+ alert(this.i18nService.t('premiumRequiredDesc'));
+ } else if (!this.hasUpdatedKey) {
+ alert(this.i18nService.t('updateKey'));
+ }
+ }
+
+ async save() {
+ if (!this.hasUpdatedKey) {
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('updateKey'));
+ return;
+ }
+
+ const fileEl = document.getElementById('file') as HTMLInputElement;
+ const files = fileEl.files;
+ if (files == null || files.length === 0) {
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('selectFile'));
+ return;
+ }
+
+ if (files[0].size > 104857600) { // 100 MB
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('maxFileSize'));
+ return;
+ }
+
+ this.cipherDomain = await this.cipherService.saveAttachmentWithServer(this.cipherDomain, files[0]);
+ this.cipher = await this.cipherDomain.decrypt();
+ this.analytics.eventTrack.next({ action: 'Added Attachment' });
+ this.toasterService.popAsync('success', null, this.i18nService.t('attachmentSaved'));
+
+ // reset file input
+ // ref: https://stackoverflow.com/a/20552042
+ fileEl.type = '';
+ fileEl.type = 'file';
+ fileEl.value = '';
+ }
+
+ async delete(attachment: AttachmentView) {
+ if (!confirm(this.i18nService.t('deleteAttachmentConfirmation'))) {
+ return;
+ }
+
+ await this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachment.id);
+ this.analytics.eventTrack.next({ action: 'Deleted Attachment' });
+ this.toasterService.popAsync('success', null, this.i18nService.t('deletedAttachment'));
+ const i = this.cipher.attachments.indexOf(attachment);
+ if (i > -1) {
+ this.cipher.attachments.splice(i, 1);
+ }
+ }
+}
diff --git a/src/app/vault/vault.component.html b/src/app/vault/vault.component.html
index d7c365a04d..6d6bd9bd2d 100644
--- a/src/app/vault/vault.component.html
+++ b/src/app/vault/vault.component.html
@@ -27,4 +27,5 @@
(onGeneratePassword)="openPasswordGenerator()">
+
diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts
index f55705dd7a..91ea3e46ff 100644
--- a/src/app/vault/vault.component.ts
+++ b/src/app/vault/vault.component.ts
@@ -15,6 +15,7 @@ import {
import { Location } from '@angular/common';
+import { AttachmentsComponent } from './attachments.component';
import { AddEditComponent } from './add-edit.component';
import { CiphersComponent } from './ciphers.component';
import { GroupingsComponent } from './groupings.component';
@@ -38,6 +39,7 @@ export class VaultComponent implements OnInit {
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild('passwordGenerator', { read: ViewContainerRef }) passwordGeneratorModal: ViewContainerRef;
+ @ViewChild('attachments', { read: ViewContainerRef }) attachmentsModal: ViewContainerRef;
action: string;
cipherId: string = null;
@@ -130,7 +132,11 @@ export class VaultComponent implements OnInit {
}
editCipherAttachments(cipher: CipherView) {
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ const modal = this.attachmentsModal.createComponent(factory).instance;
+ const childComponent = modal.show(AttachmentsComponent, this.attachmentsModal);
+ childComponent.cipherId = cipher.id;
}
cancelledAddEdit(cipher: CipherView) {
@@ -180,10 +186,9 @@ export class VaultComponent implements OnInit {
}
async openPasswordGenerator() {
- let factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
- let componentRef = this.passwordGeneratorModal.createComponent(factory);
- let modal = componentRef.instance as ModalComponent;
- let childComponent = modal.show(PasswordGeneratorComponent,
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ const modal = this.passwordGeneratorModal.createComponent(factory).instance;
+ const childComponent = modal.show(PasswordGeneratorComponent,
this.passwordGeneratorModal);
childComponent.showSelect = true;
diff --git a/src/app/vault/view.component.html b/src/app/vault/view.component.html
index c1365a4f64..30ad8e1c67 100644
--- a/src/app/vault/view.component.html
+++ b/src/app/vault/view.component.html
@@ -219,12 +219,12 @@
{{'attachments' | i18n}}
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 85747d9005..186e315bc7 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -346,5 +346,29 @@
"searchType": {
"message": "Search type",
"description": "Search item type"
+ },
+ "newAttachment": {
+ "message": "Add New Attachment"
+ },
+ "deletedAttachment": {
+ "message": "Deleted attachment"
+ },
+ "deleteAttachmentConfirmation": {
+ "message": "Are you sure you want to delete this attachment?"
+ },
+ "attachmentSaved": {
+ "message": "The attachment has been saved."
+ },
+ "file": {
+ "message": "File"
+ },
+ "selectFile": {
+ "message": "Select a file."
+ },
+ "maxFileSize": {
+ "message": "Maximum file size is 100 MB."
+ },
+ "updateKey": {
+ "message": "You cannot use this feature until you update your encryption key."
}
}
diff --git a/src/scss/box.scss b/src/scss/box.scss
index 11d183fd3e..a250dddbf4 100644
--- a/src/scss/box.scss
+++ b/src/scss/box.scss
@@ -21,6 +21,7 @@
z-index: 1;
color: $text-color;
overflow-wrap: break-word;
+ word-break: break-all;
&:before {
content: "";
@@ -58,12 +59,6 @@
overflow-x: auto;
}
- .no-wrap {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
.row-label, label {
font-size: $font-size-small;
color: $text-muted;
@@ -137,6 +132,10 @@
input {
height: 10px;
}
+
+ label, span {
+ white-space: nowrap;
+ }
}
input:not([type="checkbox"]), textarea {
@@ -160,8 +159,10 @@
}
.action-buttons {
+ display: flex;
+ margin-left: 5px;
+
.row-btn {
- float: left;
cursor: pointer;
padding: 10px 8px;
background: none;
@@ -184,6 +185,11 @@
padding-right: 2px !important;
}
}
+
+ &.no-pad .row-btn {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
}
select.field-type {
@@ -191,21 +197,14 @@
width: calc(100% - 25px);
}
- .right-icon, .fa-chevron-right {
- float: right;
- margin-top: 4px;
+ .row-sub-icon {
color: $list-icon-color;
}
.row-sub-label {
- float: right;
- display: block;
- margin-right: 15px;
+ margin: 0 15px;
color: $gray-light;
- }
-
- small.row-sub-label {
- margin-top: 2px;
+ white-space: nowrap;
}
}