diff options
-rw-r--r-- | css/ContactDetails.scss | 13 | ||||
-rw-r--r-- | css/ContactDetailsAvatar.scss | 11 | ||||
-rw-r--r-- | css/Properties/Properties.scss | 16 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 72 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAvatar.vue | 73 | ||||
-rw-r--r-- | src/components/Properties/PropertyActions.vue | 58 | ||||
-rw-r--r-- | src/components/Properties/PropertyDateTime.vue | 6 | ||||
-rw-r--r-- | src/components/Properties/PropertyMultipleText.vue | 6 | ||||
-rw-r--r-- | src/components/Properties/PropertySelect.vue | 6 | ||||
-rw-r--r-- | src/components/Properties/PropertyText.vue | 6 | ||||
-rw-r--r-- | src/main.js | 4 | ||||
-rw-r--r-- | src/mixins/PropertyMixin.js | 7 |
12 files changed, 158 insertions, 120 deletions
diff --git a/css/ContactDetails.scss b/css/ContactDetails.scss index 2be765e8..6c4e53f7 100644 --- a/css/ContactDetails.scss +++ b/css/ContactDetails.scss @@ -68,14 +68,11 @@ #contact-header-actions { position: relative; display: flex; - .menu-icon { - position: relative; - height: 44px; - width: 44px; - // ! override default server class - > .icon-more-white { - // using #fffffe to trick the accessibility dark theme icon invert - @include icon-color('more', 'actions', '#fffffe', 1, true); + .header-menu { + margin-right: 10px; + .action-item__menutoggle { + // force white over header colour + color: white; } } .header-icon { diff --git a/css/ContactDetailsAvatar.scss b/css/ContactDetailsAvatar.scss index 3f1dd3bf..f86c1709 100644 --- a/css/ContactDetailsAvatar.scss +++ b/css/ContactDetailsAvatar.scss @@ -74,8 +74,15 @@ opacity: .7; } &__popovermenu { - top: calc(50% + 24px); - right: calc(50% - 22px); + // center + margin: calc((100% - 44px) / 2); + & /deep/ .action-item__menutoggle { + // hide three dot menu, in favour of icon-picture-force-white + z-index: -1; + &::before { + opacity: 0; + } + } } } // if picture is set, hide the menu diff --git a/css/Properties/Properties.scss b/css/Properties/Properties.scss index 8aa23f78..9ba2f0d3 100644 --- a/css/Properties/Properties.scss +++ b/css/Properties/Properties.scss @@ -179,24 +179,10 @@ $property-value-max-width: 250px; position: absolute !important; top: 0; left: 100%; - margin: 0; - margin-top: -3px; // align with line because of the 44x44px size + margin: -2px 2px; // align with line because of the 44x44px size border: 0; background-color: transparent; z-index: 10; - // opacity applies on the single action OR - &:not(.action-item--multiple), - &.action-item--multiple .icon-more { - opacity: 0.5; - } - &:hover, - &:active, - &:focus { - &:not(.action-item--multiple), - &.action-item--multiple .icon-more { - opacity: 0.7; - } - } } .property__value { margin-right: 0; diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index da538483..5ec86643 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -66,6 +66,7 @@ <!-- actions --> <div id="contact-header-actions"> + <!-- warning message --> <a v-if="loadingUpdate || warning" v-tooltip.bottom="{ content: warning ? warning.msg : '', @@ -73,25 +74,35 @@ }" :class="{'icon-loading-small': loadingUpdate, [`${warning.icon}`]: warning}" class="header-icon" href="#" /> + + <!-- conflict message --> <div v-if="conflict" v-tooltip="{ content: conflict, show: true, trigger: 'manual', }" class="header-icon header-icon--pulse icon-history-force-white" @click="refreshContact" /> - <div class="menu-icon"> - <div v-click-outside="closeMenu" class="header-icon icon-more-white" @click="toggleMenu" /> - <div :class="{ 'open': openedMenu }" class="popovermenu"> - <popover-menu :menu="contactActions" /> - </div> - </div> + + <!-- menu actions --> + <Actions class="header-menu" menu-align="right"> + <ActionLink :href="contact.url" :download="`${contact.displayName}.vcf`" + icon="icon-download"> + {{ t('contacts', 'Download') }} + </ActionLink> + <ActionButton icon="icon-qrcode" @click="showQRcode"> + {{ t('contacts', 'Generate QR Code') }} + </ActionButton> + <ActionButton v-if="!isReadOnly" icon="icon-delete" @click="deleteContact"> + {{ t('contacts', 'Delete') }} + </ActionButton> + </Actions> </div> <!-- qrcode --> - <modal v-if="qrcode" id="qrcode-modal" :title="contact.displayName" + <Modal v-if="qrcode" id="qrcode-modal" :title="contact.displayName" @close="closeQrModal"> <img :src="`data:image/svg+xml;base64,${qrcode}`" class="qrcode" width="400"> - </modal> + </Modal> </header> <!-- contact details loading --> @@ -138,16 +149,17 @@ import debounce from 'debounce' import PQueue from 'p-queue' import qr from 'qr-image' import { stringify } from 'ical.js' +import { ActionLink, ActionButton } from 'nextcloud-vue' import rfcProps from 'Models/rfcProps' import validate from 'Services/validate' -import ContactProperty from './ContactDetails/ContactDetailsProperty' import AddNewProp from './ContactDetails/ContactDetailsAddNewProp' -import PropertySelect from './Properties/PropertySelect' +import ContactAvatar from './ContactDetails/ContactDetailsAvatar' +import ContactProperty from './ContactDetails/ContactDetailsProperty' import PropertyGroups from './Properties/PropertyGroups' import PropertyRev from './Properties/PropertyRev' -import ContactAvatar from './ContactDetails/ContactDetailsAvatar' +import PropertySelect from './Properties/PropertySelect' const updateQueue = new PQueue({ concurrency: 1 }) @@ -155,12 +167,14 @@ export default { name: 'ContactDetails', components: { + ActionButton, + ActionLink, + AddNewProp, + ContactAvatar, ContactProperty, - PropertySelect, PropertyGroups, PropertyRev, - AddNewProp, - ContactAvatar + PropertySelect }, props: { @@ -247,36 +261,6 @@ export default { }, /** - * Header actions for the contact - * - * @returns {Array} - */ - contactActions() { - let actions = [ - { - icon: 'icon-download', - text: t('contacts', 'Download'), - href: this.contact.url, - download: `${this.contact.displayName}.vcf` - }, - { - icon: 'icon-qrcode', - text: t('contacts', 'Generate QR Code'), - action: this.showQRcode - } - ] - if (!this.contact.addressbook.readOnly) { - actions.push({ - icon: 'icon-delete', - text: t('contacts', 'Delete'), - action: this.deleteContact - }) - } - - return actions - }, - - /** * Contact properties copied and sorted by rfcProps.fieldOrder * * @returns {Array} diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index 4ee26ac0..fb54cd89 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -32,28 +32,49 @@ <div v-click-outside="closeMenu" class="contact-header-avatar__options"> <a v-tooltip.bottom="t('contacts', 'Add a new picture')" href="#" class="contact-avatar-options" :class="loading ? 'icon-loading-small' : 'icon-picture-force-white'" - @click.prevent="toggleMenu" /> + @click.stop.prevent="toggleMenu" /> <input id="contact-avatar-upload" ref="uploadInput" type="file" class="hidden" accept="image/*" @change="processFile"> </div> - <modal v-if="maximizeAvatar" ref="modal" class="contact-header-modal" - :actions="modalActions" size="large" :title="contact.displayName" + <Modal v-if="maximizeAvatar" + ref="modal" class="contact-header-modal" + size="large" :title="contact.displayName" @close="toggleModal"> + <template #actions> + <ActionButton v-if="!isReadOnly" icon="icon-upload" @click="selectFileInput"> + {{ t('contacts', 'Upload a new picture') }} + </ActionButton> + <ActionButton v-if="!isReadOnly" icon="icon-picture" @click="selectFilePicker"> + {{ t('contacts', 'Choose from files') }} + </ActionButton> + <ActionButton v-if="!isReadOnly" icon="icon-delete" @click="removePhoto"> + {{ t('contacts', 'Delete picture') }} + </ActionButton> + <ActionLink :href="`${contact.url}?photo`" icon="icon-download" target="_blank"> + {{ t('contacts', 'Download picture') }} + </ActionLink> + </template> <img ref="img" :src="photo" class="contact-header-modal__photo" :style="{ width, height }" @load="updateImgSize"> - </modal> + </Modal> <!-- out of the avatar__options because of the overflow hidden --> - <div :class="{ 'open': opened }" class="contact-avatar-options__popovermenu popovermenu"> - <popover-menu :menu="actions" /> - </div> + <Actions :open="opened" class="contact-avatar-options__popovermenu"> + <ActionButton v-if="!isReadOnly" icon="icon-upload" @click="selectFileInput"> + {{ t('contacts', 'Upload a new picture') }} + </ActionButton> + <ActionButton v-if="!isReadOnly" icon="icon-picture" @click="selectFilePicker"> + {{ t('contacts', 'Choose from files') }} + </ActionButton> + </Actions> </div> </div> </template> <script> import debounce from 'debounce' +import { ActionLink, ActionButton } from 'nextcloud-vue' import { pickFileOrDirectory } from 'nextcloud-server/dist/files' import { generateRemoteUrl } from 'nextcloud-server/dist/router' @@ -63,6 +84,11 @@ const axios = () => import('axios') export default { name: 'ContactAvatar', + components: { + ActionLink, + ActionButton + }, + props: { contact: { type: Object, @@ -90,34 +116,11 @@ export default { } return this.contact.photo }, - actions() { - return [ - { - icon: 'icon-upload', - text: t('contacts', 'Upload a new picture'), - action: this.selectFileInput - }, - { - icon: 'icon-picture', - text: t('contacts', 'Choose from files'), - action: this.selectFilePicker - } - ] - }, - modalActions() { - return [...this.actions, ...[ - { - icon: 'icon-delete', - text: t('contacts', 'Delete picture'), - action: this.removePhoto - }, - { - icon: 'icon-download', - text: t('contacts', 'Download picture'), - href: this.contact.url + '?photo', - target: '_blank' - } - ]] + isReadOnly() { + if (this.contact.addressbook) { + return this.contact.addressbook.readOnly + } + return false } }, mounted() { diff --git a/src/components/Properties/PropertyActions.vue b/src/components/Properties/PropertyActions.vue new file mode 100644 index 00000000..8a611d37 --- /dev/null +++ b/src/components/Properties/PropertyActions.vue @@ -0,0 +1,58 @@ +<!-- + - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <Actions class="property__actions"> + <ActionButton icon="icon-delete" @click="deleteProperty"> + {{ t('contacts', 'Delete') }} + </ActionButton> + <ActionButton v-for="(action, index) in actions" :key="index" + :icon="action.icon" @click="action.action"> + {{ action.text }} + </ActionButton> + </Actions> +</template> + +<script> +import { ActionButton } from 'nextcloud-vue' + +export default { + name: 'PropertyActions', + + components: { + ActionButton + }, + + props: { + actions: { + type: Array, + default: () => [] + } + }, + + methods: { + deleteProperty() { + this.$emit('delete') + } + } +} +</script> diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue index 7f003aa9..4700615f 100644 --- a/src/components/Properties/PropertyDateTime.vue +++ b/src/components/Properties/PropertyDateTime.vue @@ -44,7 +44,7 @@ </div> <!-- props actions --> - <action :actions="actions" class="property__actions" /> + <PropertyActions :actions="actions" @delete="deleteProperty" /> <!-- Real input where the picker shows --> <DatetimePicker :value="vcardTimeLocalValue.toJSDate()" :minute-step="10" :lang="lang" @@ -63,13 +63,15 @@ import { VCardTime } from 'ical.js' import PropertyMixin from 'Mixins/PropertyMixin' import PropertyTitle from './PropertyTitle' +import PropertyActions from './PropertyActions' export default { name: 'PropertyDateTime', components: { DatetimePicker, - PropertyTitle + PropertyTitle, + PropertyActions }, mixins: [PropertyMixin], diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index 48d65bf8..d14e83e4 100644 --- a/src/components/Properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -50,7 +50,7 @@ class="property__value" type="text" @input="updateValue"> <!-- props actions --> - <action :actions="actions" class="property__actions" /> + <PropertyActions :actions="actions" @delete="deleteProperty" /> </div> <!-- force order based on model --> @@ -79,12 +79,14 @@ <script> import PropertyMixin from 'Mixins/PropertyMixin' import PropertyTitle from './PropertyTitle' +import PropertyActions from './PropertyActions' export default { name: 'PropertyText', components: { - PropertyTitle + PropertyTitle, + PropertyActions }, mixins: [PropertyMixin], diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue index 47be44ec..559aebc8 100644 --- a/src/components/Properties/PropertySelect.vue +++ b/src/components/Properties/PropertySelect.vue @@ -38,7 +38,7 @@ </div> <!-- props actions --> - <action :actions="actions" class="property__actions" /> + <PropertyActions :actions="actions" @delete="deleteProperty" /> <multiselect v-model="matchedOptions" :options="propModel.options" :placeholder="t('contacts', 'Select option')" :disabled="isSingleOption || isReadOnly" class="property__value" track-by="id" @@ -50,12 +50,14 @@ <script> import PropertyMixin from 'Mixins/PropertyMixin' import PropertyTitle from './PropertyTitle' +import PropertyActions from './PropertyActions' export default { name: 'PropertySelect', components: { - PropertyTitle + PropertyTitle, + PropertyActions }, mixins: [PropertyMixin], diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue index 48745571..8ccaf969 100644 --- a/src/components/Properties/PropertyText.vue +++ b/src/components/Properties/PropertyText.vue @@ -60,7 +60,7 @@ target="_blank" /> <!-- props actions --> - <action :actions="actions" class="property__actions" /> + <PropertyActions :actions="actions" @delete="deleteProperty" /> </div> </div> </template> @@ -69,12 +69,14 @@ import debounce from 'debounce' import PropertyMixin from 'Mixins/PropertyMixin' import PropertyTitle from './PropertyTitle' +import PropertyActions from './PropertyActions' export default { name: 'PropertyText', components: { - PropertyTitle + PropertyTitle, + PropertyActions }, mixins: [PropertyMixin], diff --git a/src/main.js b/src/main.js index d43ff739..c6d0f8b7 100644 --- a/src/main.js +++ b/src/main.js @@ -27,7 +27,7 @@ import { sync } from 'vuex-router-sync' import { generateFilePath } from 'nextcloud-server/dist/router' /** GLOBAL COMPONENTS AND DIRECTIVE */ -import { Action, DatetimePicker, Multiselect, PopoverMenu, Modal } from 'nextcloud-vue' +import { Actions, DatetimePicker, Multiselect, PopoverMenu, Modal } from 'nextcloud-vue' import ClickOutside from 'vue-click-outside' import VTooltip from 'nextcloud-vue/dist/Directives/Tooltip' import VueClipboard from 'vue-clipboard2' @@ -44,7 +44,7 @@ __webpack_nonce__ = btoa(OC.requestToken) __webpack_public_path__ = generateFilePath('contacts', '', 'js/') // Register global components -Vue.component('Action', Action) +Vue.component('Actions', Actions) Vue.component('DatetimePicker', DatetimePicker) Vue.component('Modal', Modal) Vue.component('Multiselect', Multiselect) diff --git a/src/mixins/PropertyMixin.js b/src/mixins/PropertyMixin.js index 170d461a..9fa3ed0a 100644 --- a/src/mixins/PropertyMixin.js +++ b/src/mixins/PropertyMixin.js @@ -88,12 +88,7 @@ export default { computed: { actions() { - const del = { - text: t('contacts', 'Delete'), - icon: 'icon-delete', - action: this.deleteProperty - } - return [...this.propModel.actions ? this.propModel.actions : [], del] + return this.propModel.actions ? this.propModel.actions : [] } }, |