diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/Controller/Password/Update.js | 63 | ||||
-rw-r--r-- | src/js/Manager/ControllerManager.js | 7 | ||||
-rw-r--r-- | src/platform/generic/_locales/de/messages.json | 44 | ||||
-rw-r--r-- | src/platform/generic/_locales/en/messages.json | 44 | ||||
-rw-r--r-- | src/vue/App/Popup.vue | 7 | ||||
-rw-r--r-- | src/vue/Components/Form/InputField.vue | 18 | ||||
-rw-r--r-- | src/vue/Components/Form/SliderField.vue | 9 | ||||
-rw-r--r-- | src/vue/Components/Icon.vue | 1 | ||||
-rw-r--r-- | src/vue/Components/List/Item/Menu/PasswordMenu.vue | 3 | ||||
-rw-r--r-- | src/vue/Components/List/Item/Password.vue | 26 | ||||
-rw-r--r-- | src/vue/Components/Password/CustomProperty.vue | 244 | ||||
-rw-r--r-- | src/vue/Components/Password/Property.vue | 224 | ||||
-rw-r--r-- | src/vue/Components/Password/View.vue | 253 |
13 files changed, 932 insertions, 11 deletions
diff --git a/src/js/Controller/Password/Update.js b/src/js/Controller/Password/Update.js new file mode 100644 index 0000000..3d393dc --- /dev/null +++ b/src/js/Controller/Password/Update.js @@ -0,0 +1,63 @@ +import AbstractController from '@js/Controller/AbstractController'; +import SearchIndex from '@js/Search/Index/SearchIndex'; +import ApiRepository from "@js/Repositories/ApiRepository"; +import ToastService from "@js/Services/ToastService"; +import ErrorManager from "@js/Manager/ErrorManager"; +import ServerManager from '@js/Manager/ServerManager'; + +export default class Update extends AbstractController { + + async execute(message, reply) { + let {data} = message.getPayload(); + data.edited = new Date(); + + if(data.id === undefined) { + this._returnError(); + return; + } + + /** @type {EnhancedPassword} **/ + let model = SearchIndex.getItem(data.id); + model.setProperties(data); + + if(model !== null && !model.isTrashed()) { + await this._updatePassword(model, data.customFields === undefined ? false:true); + reply.setPayload({success: true, data: data}); + } else { + reply.setPayload({success: false}); + this._returnError() + } + } + + + /** + * + * @param {EnhancedPassword} password + * @private + */ + async _updatePassword(password, reloadServer) { + let api = /** @type {PasswordsClient} **/ await ApiRepository.findById(password.getServer().getId()); + let repository = /** @type {PasswordRepository} **/ api.getInstance('repository.password'); + await repository.update(password); + + if(reloadServer === true) { + ServerManager.reloadServer(password.getServer()); + } else { + SearchIndex.removeItem(password); + SearchIndex.addItem(password, true); + } + ToastService.success('ToastPasswordUpdated') + .catch(ErrorManager.catch); + } + + /** + * + * @private + */ + _returnError() { + ToastService + .error('ToastPasswordUpdateFailed') + .catch(ErrorManager.catch); + } + +}
\ No newline at end of file diff --git a/src/js/Manager/ControllerManager.js b/src/js/Manager/ControllerManager.js index 51e31fa..c8e0691 100644 --- a/src/js/Manager/ControllerManager.js +++ b/src/js/Manager/ControllerManager.js @@ -68,6 +68,13 @@ class ControllerManager { } ); MessageService.listen( + 'password.update', + async (message, reply) => { + let module = await import(/* webpackChunkName: "PasswordUpdate" */ '@js/Controller/Password/Update'); + await this._executeController(module, message, reply); + } + ); + MessageService.listen( 'folder.list', async (message, reply) => { let module = await import(/* webpackChunkName: "FolderList" */ '@js/Controller/Folder/List'); diff --git a/src/platform/generic/_locales/de/messages.json b/src/platform/generic/_locales/de/messages.json index ff73dd4..ba330d6 100644 --- a/src/platform/generic/_locales/de/messages.json +++ b/src/platform/generic/_locales/de/messages.json @@ -243,6 +243,10 @@ "message" : "In den Papierkorb verschieben", "description": "Title of the menu option to move a password to the trash" }, + "PasswordItemViewEdit" : { + "message" : "Passwort ansehen/bearbeiten", + "description": "Title of the menu option to view and edit a password" + }, "ValidationLabel" : { "message" : "Titel", "description": "Name of the account label field in account validation message. This value is used when the user attempts to save an account with an invalid label" @@ -455,6 +459,22 @@ "message" : "Ordner", "description": "Label for the folder count in the server info in the browse tab" }, + "LabelFolder" : { + "message" : "Ordner", + "description": "Label for the folder of this password." + }, + "LabelCreated" : { + "message" : "Erstellt", + "description": "Label for the creation time of this password." + }, + "LabelEdited" : { + "message" : "Geändert", + "description": "Label for the last edit time of this password." + }, + "LabelNotes" : { + "message" : "Notitzen", + "description": "Label for the notes of this password." + }, "LabelTags" : { "message" : "Tags", "description": "Label for the tag count in the server info in the browse tab" @@ -641,6 +661,14 @@ } } }, + "ToastPasswordUpdated" : { + "message" : "Passwort aktualisiert", + "description": "Text of the toast notification when password was updated" + }, + "ToastPasswordUpdateFailed" : { + "message" : "Passwort aktualisierung fehlgeschlagen", + "description": "Text of the toast notification when password update failed" + }, "PasswordPastedSuccess" : { "message" : "$LABEL$ eingefügt", "description" : "Text of the toast notification when a password entry was pasted successfully into the current tab", @@ -1310,5 +1338,21 @@ "SettingsShowUsernameInList" : { "message" : "Zeige Benutzername neben dem Titel", "description": "Label of the setting in the extension settings to show the username next to the title in password lists." + }, + "PasswordCustomFieldsTypeText" : { + "message" : "Text", + "description": "Label of the password custom setting type 'text'." + }, + "PasswordCustomFieldsTypeSecret" : { + "message" : "Geheimnis", + "description": "Label of the password custom setting type 'secret'." + }, + "PasswordCustomFieldsTypeEmail" : { + "message" : "Email", + "description": "Label of the password custom setting type 'email'." + }, + "PasswordCustomFieldsTypeUrl" : { + "message" : "Link", + "description": "Label of the password custom setting type 'url'." } } diff --git a/src/platform/generic/_locales/en/messages.json b/src/platform/generic/_locales/en/messages.json index c535582..cc7cc38 100644 --- a/src/platform/generic/_locales/en/messages.json +++ b/src/platform/generic/_locales/en/messages.json @@ -257,6 +257,10 @@ "message" : "Move to trash", "description": "Title of the menu option to move a password to the trash" }, + "PasswordItemViewEdit" : { + "message" : "View/edit password", + "description": "Title of the menu option to view and edit a password" + }, "ValidationLabel" : { "message" : "label", "description": "Name of the account label field in account validation message. This value is used when the user attempts to save an account with an invalid label" @@ -469,6 +473,22 @@ "message" : "Folders", "description": "Label for the folder count in the server info in the browse tab" }, + "LabelFolder" : { + "message" : "Folder", + "description": "Label for the folder of this password." + }, + "LabelCreated" : { + "message" : "Created", + "description": "Label for the creation time of this password." + }, + "LabelEdited" : { + "message" : "Modified", + "description": "Label for the last edit time of this password." + }, + "LabelNotes" : { + "message" : "Notes", + "description": "Label for the notes of this password." + }, "LabelTags" : { "message" : "Tags", "description": "Label for the tag count in the server info in the browse tab" @@ -665,6 +685,14 @@ } } }, + "ToastPasswordUpdated" : { + "message" : "Password updated", + "description": "Text of the toast notification when password was updated" + }, + "ToastPasswordUpdateFailed" : { + "message" : "Password update failed", + "description": "Text of the toast notification when password update failed" + }, "PasswordPastedError" : { "message" : "Could not paste $LABEL$", "description" : "Text of the toast notification when a password could not be pasted into the current tab", @@ -1324,5 +1352,21 @@ "SettingsShowUsernameInList" : { "message" : "Show username next to title", "description": "Label of the setting in the extension settings to show the username next to the title in password lists." + }, + "PasswordCustomFieldsTypeText" : { + "message" : "Text", + "description": "Label of the password custom setting type 'text'." + }, + "PasswordCustomFieldsTypeSecret" : { + "message" : "Secret", + "description": "Label of the password custom setting type 'secret'." + }, + "PasswordCustomFieldsTypeEmail" : { + "message" : "Email", + "description": "Label of the password custom setting type 'email'." + }, + "PasswordCustomFieldsTypeUrl" : { + "message" : "Link", + "description": "Label of the password custom setting type 'url'." } } diff --git a/src/vue/App/Popup.vue b/src/vue/App/Popup.vue index 7030c45..14bcee6 100644 --- a/src/vue/App/Popup.vue +++ b/src/vue/App/Popup.vue @@ -112,8 +112,11 @@ .send({type: 'popup.status.set', payload: {tab}}); }, searchEvent($event) { - this.search.query = $event; - this.$refs.tabs.setActive('search'); + var pwdViews = document.getElementsByClassName("password-view") + if(pwdViews.length === 0) { + this.search.query = $event; + this.$refs.tabs.setActive('search'); + } } } }; diff --git a/src/vue/Components/Form/InputField.vue b/src/vue/Components/Form/InputField.vue index 65b3e94..e902f54 100644 --- a/src/vue/Components/Form/InputField.vue +++ b/src/vue/Components/Form/InputField.vue @@ -1,9 +1,21 @@ <template> - <input :type="type" + <textarea v-if="type === 'textarea'" + :value="value" + :placeholder="getPlaceholder" + :title="getTitle" + :readonly="readonly" + filled + auto-grow + v-on="listeners" + @input="handleInput" + @change="handleChange"/> + <input v-else + :type="type" :value="value" :checked="isChecked" :placeholder="getPlaceholder" :title="getTitle" + :readonly="readonly" v-on="listeners" @input="handleInput" @change="handleChange"/> @@ -34,6 +46,10 @@ title : { type : String, default: '' + }, + readonly : { + type : Boolean, + default: false } }, diff --git a/src/vue/Components/Form/SliderField.vue b/src/vue/Components/Form/SliderField.vue index 729c113..e1acbf4 100644 --- a/src/vue/Components/Form/SliderField.vue +++ b/src/vue/Components/Form/SliderField.vue @@ -6,6 +6,7 @@ ref="checkbox" :id="id" :name="name" + :readonly="readonly" v-model="model" v-on="listeners" /> </span> @@ -31,6 +32,10 @@ name : { type : String, default: undefined + }, + readonly: { + type : Boolean, + default: false } }, @@ -66,7 +71,9 @@ methods: { toggleSwitch() { - this.model = !this.model; + if(this.readonly === false) { + this.model = !this.model; + } } }, diff --git a/src/vue/Components/Icon.vue b/src/vue/Components/Icon.vue index cdbc5b4..b8f6a2f 100644 --- a/src/vue/Components/Icon.vue +++ b/src/vue/Components/Icon.vue @@ -50,7 +50,6 @@ let icon = this.hover && this.hoverIcon !== null ? this.hoverIcon:this.icon, font = this.hover && this.hoverFont !== null ? this.hoverFont:this.font, style = font === 'solid' ? 'fas':'far'; - if(this.spin) style += ' fa-spin'; return `icon icon-${icon} font-${font} ${style} fa-${icon}`; diff --git a/src/vue/Components/List/Item/Menu/PasswordMenu.vue b/src/vue/Components/List/Item/Menu/PasswordMenu.vue index 93b0ef8..f343694 100644 --- a/src/vue/Components/List/Item/Menu/PasswordMenu.vue +++ b/src/vue/Components/List/Item/Menu/PasswordMenu.vue @@ -4,6 +4,9 @@ <icon icon="globe-europe" font="solid" slot="before"/> <icon class="option" icon="clipboard" slot="after" @click.stop="$emit('copy', 'url')"/> </translate> + <translate tag="div" class="menu-item" say="PasswordItemViewEdit" @click="$emit('toggleEntry')"> + <icon icon="file-alt" font="solid" slot="before"/> + </translate> <translate tag="div" class="menu-item" say="PasswordItemToTrash" @click="moveToTrash"> <icon icon="trash" font="solid" slot="before"/> </translate> diff --git a/src/vue/Components/List/Item/Password.vue b/src/vue/Components/List/Item/Password.vue index cffad61..c0dd21c 100644 --- a/src/vue/Components/List/Item/Password.vue +++ b/src/vue/Components/List/Item/Password.vue @@ -1,6 +1,6 @@ <template> <li class="item password-item"> - <div class="item-main" :class="{'has-menu':showMenu}"> + <div class="item-main" :class="{'has-menu':(showMenu || showEntry ? true : false)}"> <div class="label" @click="sendPassword()" :title="title"> <favicon :password="password.getId()" :size="22" v-if="favicon"/> {{ label }} @@ -8,11 +8,12 @@ <div class="options"> <icon icon="user" hover-icon="clipboard" @click="copy('username')" draggable="true" @dragstart="drag($event, 'username')"/> <icon icon="key" font="solid" hover-icon="clipboard" hover-font="regular" @click="copy('password')" draggable="true" @dragstart="drag($event, 'password')"/> - <icon icon="ellipsis-h" font="solid" @click="showMenu = !showMenu"/> + <icon icon="ellipsis-h" font="solid" @click="toggleMenu()"/> </div> <icon :class="securityClass" icon="shield-alt" font="solid"/> </div> - <password-menu :show="showMenu" :password="password" v-on:copy="copy($event)" v-on:delete="$emit('delete', password)"/> + <password-menu :show="showMenu" :password="password" v-on:copy="copy($event)" v-on:delete="$emit('delete', password)" v-on:toggleEntry="toggleEntry()"/> + <password-view v-if="showEntry" :password="password" v-on:toggleEntry="toggleEntry()"/> </li> </template> @@ -28,9 +29,10 @@ import PasswordSettingsManager from '@js/Manager/PasswordSettingsManager'; import Translate from "@vue/Components/Translate"; import PasswordMenu from "@vue/Components/List/Item/Menu/PasswordMenu"; + import PasswordView from '@vue/Components/Password/View'; export default { - components: {PasswordMenu, Translate, Favicon, Icon}, + components: {PasswordMenu, Translate, Favicon, Icon, PasswordView}, props : { password: { type: Password @@ -47,8 +49,9 @@ data() { return { - active : true, - showMenu: false + active : true, + showMenu : false, + showEntry: false }; }, @@ -127,6 +130,17 @@ drag(event, property) { let data = this.password.getProperty(property); event.dataTransfer.setData('text/plain', data); + }, + toggleMenu() { + if(this.showEntry === true) { + this.showEntry = false; + } else { + this.showMenu = !this.showMenu; + } + }, + toggleEntry() { + this.showMenu = false; + this.showEntry = !this.showEntry; } } }; diff --git a/src/vue/Components/Password/CustomProperty.vue b/src/vue/Components/Password/CustomProperty.vue new file mode 100644 index 0000000..1b3cd0d --- /dev/null +++ b/src/vue/Components/Password/CustomProperty.vue @@ -0,0 +1,244 @@ +<template> + <div :class="classList" v-if="showField"> + <div :class="'property-label' + activeClassName"> + <label v-if="!editable">{{label}}</label> + <input-field v-else :class="labelClassName" v-model="label"/> + <select-field :class="activeClassName" v-model="type" :options="customTypeOptions" :disabled="!editable"/> + </div> + <div class="property-value"> + <a v-if="field.type === 'url' && !editable" :href="value">{{text}}</a> + <input-field v-else :class="activeClassName" @click="copy(field.name)" :type="getInputType" v-model="value" :readonly="!editable"/> + <div class="password-eye"> + <icon v-if="field.type === 'secret'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/> + </div> + </div> + </div> +</template> + +<script> + import InputField from '@vue/Components/Form/InputField'; + import ToastService from '@js/Services/ToastService'; + import MessageService from '@js/Services/MessageService'; + import ErrorManager from '@js/Manager/ErrorManager'; + import SelectField from '@vue/Components/Form/SelectField'; + import Icon from "@vue/Components/Icon"; + + export default { + components: { Icon, InputField, SelectField }, + props : { + field : { + type : Object + }, + editable : { + type : Boolean + } + }, + + data() { + return { + value : this.field.value, + label : this.field.label, + type : this.field.type, + plainText: false + }; + }, + + computed: { + customTypeOptions() { + return [ + { + id : 'text', + label: 'PasswordCustomFieldsTypeText' + }, + { + id : 'secret', + label: 'PasswordCustomFieldsTypeSecret' + }, + { + id : 'email', + label: 'PasswordCustomFieldsTypeEmail' + }, + { + id : 'url', + label: 'PasswordCustomFieldsTypeUrl', + } + ]; + }, + labelClassName() { + return "label" + (this.editable === true ? " active": ""); + }, + activeClassName() { + return this.editable === true ? " active": ""; + }, + showField() { + if(this.field.type === "file") return false; + if(this.field.type === "data") return false; + if(this.editable || this.value !== '') { + return true; + } + return false; + }, + label() { + return this.field.label; + }, + classList() { + var result = "password-view-customproperty"; + if(!this.editable) { + result += " readonly"; + } + if(this.field.type !== "url" && this.editable === false) { + result += " allow-copy"; + } + return result; + }, + passwordIcon() { + if(this.plainText) { + return "eye-slash"; + } + return "eye"; + }, + getInputType() { + if(this.field.type === "secret" && this.plainText !== true) { + return "password"; + } + return "text"; + } + }, + + methods: { + copy() { + if(this.editable) return; + var type = (this.field.type === "secret" ? "password":"text"); + let data = this.field.value; + MessageService.send({type: 'clipboard.write', payload: {type: type, value: data}}).catch(ErrorManager.catch); + + ToastService.success(['PasswordPropertyCopied', this.field.label]) + .catch(ErrorManager.catch); + }, + }, + watch : { + value(value) { + if(value === undefined || null) return; + this.field.value = value; + this.$emit('updateField'); + }, + label(label) { + if(label === undefined || null) return; + this.field.label = label; + this.$emit('updateField'); + }, + type(type) { + if(type === undefined || null) return; + this.field.type = type; + this.$emit('updateField'); + }, + editable(value) { + if(value === false) { + this.plainText = false; + } + } + } + + }; +</script> + +<style lang="scss"> +.password-view-customproperty { + padding : .5rem; + cursor : initial; + + .property-label { + display : flex; + flex-direction : row; + justify-content : space-between; + line-height : 1rem; + + &.active { + line-height : 2rem; + margin-bottom : .25rem; + } + + .input-select { + padding : 0; + position : relative; + top : -.25rem; + + select { + padding : 0 1.5rem 0 0; + } + + &.active{ + padding : .25rem; + top : 0; + + select { + padding : .25rem 1.75rem .25rem .25rem; + } + } + } + + label { + font-weight : 550; + } + + } + + + .property-value { + display : flex; + flex-direction : row; + position : relative; + } + + .password-eye { + width : 0; + cursor : pointer; + + .icon { + position : absolute; + right : 1rem; + top : .7rem; + } + } + + .input-select { + margin-left : .25rem; + } + + input { + width : 100%; + padding : .25rem; + box-sizing : border-box; + border-radius : 3px; + border : none; + line-height : 2rem; + background-color : var(--element-hover-bg-color); + color : var(--element-fg-color); + } + + input.active, .label.active, .input-select.active { + box-shadow : 0 0 0 1px var(--element-active-fg-color); + + } + + a { + width : 100%; + padding : .25rem; + line-height : 2rem; + background-color : var(--element-hover-bg-color); + color : var(--element-active-fg-color); + } + + .readonly { + box-shadow : none; + border : none; + } + + &.allow-copy { + input:hover, input:active { + cursor : pointer; + border : none; + } + } +} +</style>
\ No newline at end of file diff --git a/src/vue/Components/Password/Property.vue b/src/vue/Components/Password/Property.vue new file mode 100644 index 0000000..5e81a82 --- /dev/null +++ b/src/vue/Components/Password/Property.vue @@ -0,0 +1,224 @@ +<template> + <div :class="classList" v-if="canEdit || value !== ''"> + <div v-if="field.type === 'checkbox'" class="password-checkbox"> + <label class="property-label">{{ label }}</label> + <slider-field v-model="value" :readonly="!canEdit" :class="activeClassName"/> + </div> + <div v-else> + <label class="property-label">{{ label }}</label> + <div class="property-value"> + <a v-if="field.type === 'url' && !canEdit" :href="value">{{text}}</a> + <input-field v-else-if="field.type === 'datetime' || field.type === 'folder'" v-model="text" :readonly="true" class="readonly"/> + <input-field v-else v-model="value" :type="getInputType" @click="copyProperty(field.name)" @dblclick="copyNotes(field.name)" :readonly="!canEdit" :class="activeClassName"/> + <div class="password-eye"> + <icon v-if="field.type === 'password'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/> + </div> + </div> + </div> + </div> +</template> + +<script> + import Password from 'passwords-client/src/Model/Password/Password'; + import InputField from '@vue/Components/Form/InputField'; + import SliderField from '@vue/Components/Form/SliderField'; + import ToastService from '@js/Services/ToastService'; + import MessageService from '@js/Services/MessageService'; + import ErrorManager from '@js/Manager/ErrorManager'; + import LocalisationService from '@js/Services/LocalisationService'; + import Icon from "@vue/Components/Icon"; + + export default { + components: { Icon, InputField, SliderField }, + props : { + password : { + type : Password + }, + field : { + type : JSON + }, + editable : { + type : Boolean + } + }, + + data() { + return { + value : this.password.getProperty(this.field.name), + plainText: false, + folder: undefined + }; + }, + + async mounted() { + var response = await MessageService.send({type: 'folder.show', payload: this.value}); + var payload = response.getPayload(); + if(payload !== undefined && payload !== null ) { + this.folder = payload; + } + }, + + computed: { + canEdit() { + if(this.editable && this.field.editable) { + return true; + } + return false; + }, + label() { + return LocalisationService.translate(`Label${this.field.name.capitalize()}`); + }, + text() { + var type = this.field.type; + if(type === 'datetime') { + return new Date(this.value * 1000).toLocaleString(); + } + if(type === 'folder') { + if(this.folder === undefined) { + return this.value; + } + + return this.folder.getLabel(); + } + return this.value; + }, + classList() { + var result = "password-view-property"; + if(!this.canEdit) { + result += " readonly"; + } + if(this.field.allowCopy && this.editable === false) { + result += " allow-copy"; + } + return result; + }, + getInputType() { + if(this.field.type === "textarea") { + return "textarea"; + } + if(this.field.type === "password" && this.plainText !== true) { + return "password"; + } + return "text"; + }, + activeClassName() { + return this.editable === true ? " active": ""; + }, + passwordIcon() { + if(this.plainText) { + return "eye-slash"; + } + return "eye"; + } + }, + + methods: { + copy(property) { + let data = this.password.getProperty(property); + MessageService.send({type: 'clipboard.write', payload: {type: this.field.type, value: data}}).catch(ErrorManager.catch); + + let label = LocalisationService.translate(`Label${property.capitalize()}`); + ToastService.success(['PasswordPropertyCopied', label]) + .catch(ErrorManager.catch); + }, + copyProperty(property) { + if(this.field.allowCopy == false || this.editable === true || property === 'notes') return; + this.copy(property); + }, + copyNotes(property) { + if(this.editable === true || property !== 'notes') return; + this.copy(property); + } + }, + + watch : { + value(value) { + if(value === undefined || value === null) return; + this.$emit('updateField', this.field.name, value); + }, + editable(value) { + if(value === false) { + this.plainText = false; + } + } + } + + }; +</script> + +<style lang="scss"> +.password-view-property { + padding : .5rem; + cursor : initial; + + padding : .5rem; + cursor : initial; + + .property-label { + display : block; + font-weight : 550; + line-height : 1rem; + } + + .property-value { + display : flex; + flex-direction : row; + position : relative; + } + + .password-eye { + width : 0; + cursor : pointer; + + .icon { + position : absolute; + right : 1rem; + top : .7rem; + } + } + + input, textarea { + width : 100%; + padding : .25rem; + box-sizing : border-box; + border-radius : 3px; + border : none; + line-height : 2rem; + background-color : var(--element-hover-bg-color); + color : var(--element-fg-color); + scrollbar-width : thin; + } + + input.active, textarea.active, .label.active { + box-shadow : 0 0 0 1px var(--element-active-fg-color); + + } + + a { + width : 100%; + padding : .25rem; + line-height : 2rem; + background-color : var(--element-hover-bg-color); + color : var(--element-active-fg-color); + } + + .readonly { + box-shadow : none; + border : none; + } + + &.allow-copy { + input:hover, input:active { + cursor : pointer; + border : none; + } + } + + .password-checkbox { + display : flex; + flex-direction : row; + justify-content : space-between; + padding-right : 1rem; + } +} +</style>
\ No newline at end of file diff --git a/src/vue/Components/Password/View.vue b/src/vue/Components/Password/View.vue new file mode 100644 index 0000000..57aafdf --- /dev/null +++ b/src/vue/Components/Password/View.vue @@ -0,0 +1,253 @@ +<template> + <div class="item-menu password-view"> + <div class="password-header"> + <div class="left-space"/> + <div class="badge-container"> + <icon :class="securityClass" icon="shield-alt" font="solid"/> + <icon class="favorite" @click="updateFavorite()" icon="star" :font="favoriteIconSolid"/> + <icon :icon="sharedIcon" font="solid"/> + </div> + <div class="action-container"> + <icon icon="save" @click="save()" v-if="editable"/> + <icon icon="edit" @click="editable = !editable" v-if="!editable"/> + </div> + </div> + <div class="password-view-item"> + <property :password="password" :editable="editable" :field="field" v-for="field in defaultFields" :key="field" v-on:updateField="updateField"/> + <custom-property :field="field" :editable="editable" v-for="field in customFields" :key="field" v-on:updateField="updateCustomField"/> + </div> + </div> +</template> + +<script> + import Icon from "@vue/Components/Icon"; + import Password from "passwords-client/src/Model/Password/Password"; + import Property from '@vue/Components/Password/Property'; + import CustomProperty from '@vue/Components/Password/CustomProperty'; + import MessageService from "@js/Services/MessageService"; + + export default { + components: {Icon, Property, CustomProperty}, + props : { + password: { + type: Password + } + }, + + data() { + return { + editable : false, + defaultFields : this.getDefaultFields(), + customFields : this.getCustomFields(), + updatedFields : {} + } + }, + + computed: { + securityClass() { + let types = ['secure', 'warn', 'bad']; + + return `security ${types[this.password.getStatus()]}`; + }, + favoriteIconSolid() { + if(this.password.getProperty('favorite') === true) { + return "solid"; + } + return "reqular"; + }, + sharedIcon() { + if(this.password.getProperty('shared') === true) { + return "users"; + } + return "user-shield"; + }, + showNewCustomField() { + if(this.allowNewCustomField()) { + return true; + } + return false; + } + }, + + methods : { + getDefaultFields() { + var fields = []; + for (var property in this.password.getProperties()) { + if(property === "password") { + fields.push(this.getFieldObject(property, "password", true, true)); + } + if(property === "edited" || property === "created") { + fields.push(this.getFieldObject(property, "datetime", false, false)); + } + if(property === "label") { + fields.push(this.getFieldObject(property, "text", true, false)); + } + if(property === "folder") { + fields.push(this.getFieldObject(property, "folder", false, false)); + } + if(property === "notes") { + fields.push(this.getFieldObject(property, "textarea", true, true)); + } + if(property === "url") { + fields.push(this.getFieldObject(property, "url", true, true)); + } + if(property === "username") { + fields.push(this.getFieldObject(property, "text", true, true)); + } + if(property === "hidden") { + fields.push(this.getFieldObject(property, "checkbox", true, false)); + } + } + return fields; + }, + getFieldObject(property, type, editable, allowCopy) { + return { + name: property, + type: type, + editable: editable, + allowCopy: allowCopy, + } + }, + getCustomFields() { + var result = this.password.getProperty('customFields'); + result.push(this.getNewCustomField()); + return result; + }, + getNewCustomField() { + return ( + { + label : "", + value : "", + type : "text" + } + ) + }, + allowNewCustomField() { + if(this.customFields === undefined + || this.customFields.length >= 20) return false; + if(this.updatedFields !== undefined + && this.updatedFields.customFields !== undefined + && this.updatedFields.customFields.length >=20) return false; + return true; + }, + async updateFavorite() { + var data = { + id: this.password.getId(), + favorite: !this.password.getFavorite() + } + var result = await MessageService.send({type: 'password.update', payload: {data: data}}); + if(result.getPayload().success === true) { + this.password.setProperties(result.getPayload().data); + } + }, + save() { + if(Object.keys(this.updatedFields).length !== 0) { + this.removeEmptyCustomFields(); + this.updatedFields.id = this.password.getId(); + MessageService.send({type: 'password.update', payload: {data: this.updatedFields}}); + this.updatedFields = {}; + } + this.editable = false; + }, + updateField(field, value) { + this.updatedFields[field] = value; + }, + updateCustomField() { + this.updatedFields.customFields = this.customFields; + if(!this.allowNewCustomField()) return; + var emptyFieldAvailable = false; + this.updatedFields.customFields.forEach((e) => { + if(e.label === "" && e.value === "" && e.type !== "data" && e.type !== "file") { + emptyFieldAvailable = true; + } + }) + if(!emptyFieldAvailable) { + this.customFields.push(this.getNewCustomField()); + } + }, + removeEmptyCustomFields(){ + if(this.updatedFields.customFields === undefined) return; + this.updatedFields.customFields.forEach((e) => { + if(e.label === "" && e.value === "" && e.type !== "data" && e.type !== "file") { + var i = this.updatedFields.customFields.indexOf(e) + this.updatedFields.customFields.splice(i, 1); + } + }) + } + } + }; +</script> + +<style lang="scss"> +.item-menu.password-view { + background-color : var(--element-hover-bg-color); + color : var(--element-hover-fg-color); + + .password-header { + line-height : 3rem; + display : flex; + justify-content: space-between; + margin-bottom : -1rem; + + .left-space{ + width : 3rem + } + .icon { + width : 3rem; + display : inline-block; + text-align : center; + } + + .badge-container { + .security { + &.secure { + color : var(--success-bg-color) + } + + &.warn { + color : var(--warning-bg-color) + } + + &.bad { + color : var(--error-bg-color) + } + } + + .favorite { + cursor : pointer; + color : var(--warning-bg-color); + font-size :calc(var(--font-size) + 1rem); + position : relative; + bottom : -.25rem; + } + } + + .action-container { + cursor : pointer; + + .icon:hover { + background-color : var(--button-hover-bg-color); + color : var(--button-hover-fg-color); + } + } + } + + + .view-item { + line-height : 3rem; + cursor : pointer; + display : flex; + + .icon { + text-align : center; + width : 3rem; + display : inline-block; + } + + &:hover { + background-color : var(--element-active-hover-bg-color); + color : var(--element-active-hover-fg-color); + } + } +} +</style>
\ No newline at end of file |