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 @@ + 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; } }