diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-04-20 17:04:35 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-05-30 10:28:56 +0200 |
commit | bb5f38e9231b659f348fbd83422af0d65194037b (patch) | |
tree | 54406f474575bf3aea9d7c95f2767941ab399450 /src | |
parent | c948ec1aa78690e9a4902d66fc1365d469cc4565 (diff) |
Circle details
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/AppNavigation/CircleNavigationItem.vue | 3 | ||||
-rw-r--r-- | src/components/CircleDetails.vue | 141 | ||||
-rw-r--r-- | src/components/CircleDetails/CircleConfigs.vue | 110 | ||||
-rw-r--r-- | src/components/CircleDetails/ContentHeading.vue | 39 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 359 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAvatar.vue | 138 | ||||
-rw-r--r-- | src/components/DetailsHeader.vue | 165 | ||||
-rw-r--r-- | src/components/MemberList.vue | 47 | ||||
-rw-r--r-- | src/components/MembersList/MembersListItem.vue | 32 | ||||
-rw-r--r-- | src/models/circle.d.ts | 8 | ||||
-rw-r--r-- | src/models/circle.ts | 13 | ||||
-rw-r--r-- | src/models/constants.d.ts | 10 | ||||
-rw-r--r-- | src/models/constants.ts | 42 | ||||
-rw-r--r-- | src/models/member.d.ts | 10 | ||||
-rw-r--r-- | src/models/member.ts | 16 | ||||
-rw-r--r-- | src/services/circles.d.ts | 16 | ||||
-rw-r--r-- | src/services/circles.ts | 22 | ||||
-rw-r--r-- | src/store/circles.js | 10 |
18 files changed, 823 insertions, 358 deletions
diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue index 26838678..eadea9a7 100644 --- a/src/components/AppNavigation/CircleNavigationItem.vue +++ b/src/components/AppNavigation/CircleNavigationItem.vue @@ -93,7 +93,7 @@ import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem' import ExitToApp from 'vue-material-design-icons/ExitToApp' import LocationEnter from 'vue-material-design-icons/LocationEnter' -import { deleteCircle, joinCircle } from '../../services/circles.ts' +import { joinCircle } from '../../services/circles.ts' import { showError } from '@nextcloud/dialogs' import Circle from '../../models/circle.ts' import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin' @@ -194,7 +194,6 @@ export default { this.loading = true try { - await deleteCircle(this.circle.id) this.$store.dispatch('deleteCircle', this.circle.id) } catch (error) { showError(t('contacts', 'Unable to delete the circle')) diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue index ebe173f7..98e4870c 100644 --- a/src/components/CircleDetails.vue +++ b/src/components/CircleDetails.vue @@ -1,39 +1,101 @@ <!-- - - @copyright Copyright (c) 2021 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/>. - - - --> + - @copyright Copyright (c) 2021 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> <AppContentDetails> - {{ circleId }} + <!-- contact header --> + <DetailsHeader> + <!-- avatar and upload photo --> + <template #avatar="{avatarSize}"> + <Avatar + :disable-tooltip="true" + :display-name="circle.displayName" + :is-no-user="true" + :size="avatarSize" /> + </template> + + <!-- display name --> + <input + slot="title" + v-model="circle.displayName" + :readonly="!circle.isOwner" + :placeholder="t('contacts', 'Circle name')" + type="text" + autocomplete="off" + autocorrect="off" + spellcheck="false" + name="displayname" + @input="debounceUpdateCircle"> + + <!-- org, title --> + <template #subtitle> + </template> + + <!-- actions --> + <template #actions> + </template> + + <!-- menu actions --> + <template #actions-menu> + </template> + </DetailsHeader> + + <section class="circle-details-section"> + <ContentHeading>{{ t('contacts', 'Description') }}</ContentHeading> + + <RichContenteditable class="circle-details-section__description" + :value="circle.description" + :auto-complete="onAutocomplete" + :maxlength="1024" + :multiline="true" + :disabled="loading" + :placeholder="t('contacts', 'Enter a description for the circle')" + @submit="onDescriptionSubmit" /> + </section> + + <section class="circle-details-section"> + <CircleConfigs class="circle-details-section__configs" :circle="circle" /> + </section> </AppContentDetails> </template> <script> import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails' +import Avatar from '@nextcloud/vue/dist/Components/Avatar' +import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable' +import DetailsHeader from './DetailsHeader' +import CircleConfigs from './CircleDetails/CircleConfigs' +import ContentHeading from './CircleDetails/ContentHeading' export default { name: 'CircleDetails', components: { AppContentDetails, + Avatar, + CircleConfigs, + ContentHeading, + DetailsHeader, + RichContenteditable, }, props: { @@ -42,9 +104,46 @@ export default { required: true, }, }, + + computed: { + circle() { + return this.$store.getters.getCircle(this.circleId) + }, + }, + methods: { + /** + * Autocomplete @mentions on the description + * @param {string} search the search term + * @param {Function} callback callback to be called with results array + */ + onAutocomplete(search, callback) { + // TODO: implement autocompletion. Disabled for now + // eslint-disable-next-line node/no-callback-literal + callback([]) + }, + + onDescriptionSubmit() { + console.info(...arguments) + }, + }, } </script> <style lang="scss" scoped> +.app-content-details { + flex: 1 1 100%; + min-width: 0; +} + +.circle-details-section { + padding: 0 80px; + &:not(:first-of-type) { + margin-top: 24px; + } + + &__description { + max-width: 400px; + } +} </style> diff --git a/src/components/CircleDetails/CircleConfigs.vue b/src/components/CircleDetails/CircleConfigs.vue new file mode 100644 index 00000000..bd7b27a8 --- /dev/null +++ b/src/components/CircleDetails/CircleConfigs.vue @@ -0,0 +1,110 @@ +<!-- + - @copyright Copyright (c) 2018 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> + <ul> + <li v-for="(configs, title) in PUBLIC_CIRCLE_CONFIG" :key="title" class="circle-config"> + <ContentHeading class="circle-config__title"> + {{ title }} + </ContentHeading> + + <ul class="circle-config__list"> + <CheckboxRadio v-for="(label, config) in configs" + :key="'circle-config' + config" + :checked="isChecked(config)" + wrapper-element="li" + @update:checked="onChange(config, $event)"> + {{ label }} + </CheckboxRadio> + </ul> + </li> + </ul> +</template> + +<script> +import CheckboxRadio from '@nextcloud/vue/dist/Components/CheckboxRadio' +import ContentHeading from './ContentHeading' + +import { PUBLIC_CIRCLE_CONFIG } from '../../models/constants.ts' +import Circle from '../../models/circle.ts' +import { CircleEdit, editCircle } from '../../services/circles' + +export default { + name: 'CircleConfigs', + + components: { + CheckboxRadio, + ContentHeading, + }, + + props: { + circle: { + type: Circle, + required: true, + }, + }, + + data() { + return { + PUBLIC_CIRCLE_CONFIG, + } + }, + + methods: { + isChecked(config) { + return (this.circle.config & config) !== 0 + }, + + /** + * On toggle, add or remove the config bitwise + * @param {CircleConfig} config the circle config to manage + * @param {boolean} checked checked or not + */ + async onChange(config, checked) { + console.debug('Circle config', `'${PUBLIC_CIRCLE_CONFIG[config]}'`, 'is set to', checked) + + const prevConfig = this.circle.config + + if (checked) { + // eslint-disable-next-line vue/no-mutating-props + this.circle.config = prevConfig | config + } else { + // eslint-disable-next-line vue/no-mutating-props + this.circle.config = prevConfig & ~config + } + + const data = await editCircle(this.circle.id, CircleEdit.Config, this.circle.config) + console.info(data) + + }, + }, +} +</script> + +<style lang="scss" scoped> +.circle-config { + &__title { + user-select: none; + margin-top: 22px; + } +} +</style> diff --git a/src/components/CircleDetails/ContentHeading.vue b/src/components/CircleDetails/ContentHeading.vue new file mode 100644 index 00000000..2f435a0f --- /dev/null +++ b/src/components/CircleDetails/ContentHeading.vue @@ -0,0 +1,39 @@ +<!-- + - @copyright Copyright (c) 2021 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> + <h3 class="app-content-heading"> + <slot /> + </h3> +</template> + +<script> +export default { + name: 'ContentHeading', +} +</script> + +<style lang="scss" scoped> +.app-content-heading { + font-weight: bold; +} +</style> diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 0dbdfd20..f4e53ab4 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -32,56 +32,54 @@ <template v-else> <!-- contact header --> - <header class="contact-header"> + <DetailsHeader> <!-- avatar and upload photo --> <ContactAvatar + slot="avatar" :contact="contact" @update-local-contact="updateLocalContact" /> - <!-- QUESTION: is it better to pass contact as a prop or get it from the store inside - contact-avatar ? :avatar="contact.photo"--> - - <!-- fullname, org, title --> - <div class="contact-header__infos"> - <h2> - <input id="contact-fullname" - ref="fullname" - v-model="contact.fullName" - :readonly="contact.addressbook.readOnly" - :placeholder="t('contacts', 'Name')" - type="text" - autocomplete="off" - autocorrect="off" - spellcheck="false" - name="fullname" - @input="debounceUpdateContact" - @click="selectInput"> - </h2> - <div id="details-org-container"> - <input id="contact-org" - v-model="contact.org" - :readonly="contact.addressbook.readOnly" - :placeholder="t('contacts', 'Company')" - type="text" - autocomplete="off" - autocorrect="off" - spellcheck="false" - name="org" - @input="debounceUpdateContact"> - <input id="contact-title" - v-model="contact.title" - :readonly="contact.addressbook.readOnly" - :placeholder="t('contacts', 'Title')" - type="text" - autocomplete="off" - autocorrect="off" - spellcheck="false" - name="title" - @input="debounceUpdateContact"> - </div> - </div> + + <!-- fullname --> + <input id="contact-fullname" + slot="title" + ref="fullname" + v-model="contact.fullName" + :readonly="contact.addressbook.readOnly" + :placeholder="t('contacts', 'Name')" + type="text" + autocomplete="off" + autocorrect="off" + spellcheck="false" + name="fullname" + @input="debounceUpdateContact" + @click="selectInput"> + + <!-- org, title --> + <template #subtitle> + <input id="contact-org" + v-model="contact.org" + :readonly="contact.addressbook.readOnly" + :placeholder="t('contacts', 'Company')" + type="text" + autocomplete="off" + autocorrect="off" + spellcheck="false" + name="org" + @input="debounceUpdateContact"> + <input id="contact-title" + v-model="contact.title" + :readonly="contact.addressbook.readOnly" + :placeholder="t('contacts', 'Title')" + type="text" + autocomplete="off" + autocorrect="off" + spellcheck="false" + name="title" + @input="debounceUpdateContact"> + </template> <!-- actions --> - <div class="contact-header__actions"> + <template #actions> <!-- warning message --> <a v-if="loadingUpdate || warning" v-tooltip.bottom="{ @@ -112,67 +110,64 @@ }" class="header-icon header-icon--pulse icon-up" @click="updateContact" /> - - <!-- menu actions --> - <Actions ref="actions" - class="header-menu" - menu-align="right" - :open.sync="openedMenu"> - <ActionLink :href="contact.url" - :download="`${contact.displayName}.vcf`" - icon="icon-download"> - {{ t('contacts', 'Download') }} - </ActionLink> - <!-- user can clone if there is at least one option available --> - <ActionButton v-if="isReadOnly && addressbooksOptions.length > 0" - ref="cloneAction" - :close-after-click="true" - icon="icon-clone" - @click="cloneContact"> - {{ t('contacts', 'Clone contact') }} - </ActionButton> - <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" - :clear-view-delay="-1" - :title="contact.displayName" - @close="closeQrModal"> - <img :src="`data:image/svg+xml;base64,${qrcode}`" - :alt="t('contacts', 'Contact vCard as QR code')" - class="qrcode" - width="400"> - </Modal> - - <!-- pick addressbook when cloning contact --> - <Modal v-if="showPickAddressbookModal" - id="pick-addressbook-modal" - :clear-view-delay="-1" - :title="t('contacts', 'Pick an address book')" - @close="closePickAddressbookModal"> - <Multiselect ref="pickAddressbook" - v-model="pickedAddressbook" - :allow-empty="false" - :options="addressbooksOptions" - :placeholder="t('contacts', 'Select address book')" - track-by="id" - label="name" /> - <button @click="closePickAddressbookModal"> - {{ t('contacts', 'Cancel') }} - </button> - <button class="primary" @click="cloneContact"> + </template> + + <!-- menu actions --> + <template #actions-menu> + <ActionLink :href="contact.url" + :download="`${contact.displayName}.vcf`" + icon="icon-download"> + {{ t('contacts', 'Download') }} + </ActionLink> + <!-- user can clone if there is at least one option available --> + <ActionButton v-if="isReadOnly && addressbooksOptions.length > 0" + ref="cloneAction" + :close-after-click="true" + icon="icon-clone" + @click="cloneContact"> {{ t('contacts', 'Clone contact') }} - </button> - </Modal> - </header> + </ActionButton> + <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> + </template> + </DetailsHeader> + + <!-- qrcode --> + <Modal v-if="qrcode" + id="qrcode-modal" + :clear-view-delay="-1" + :title="contact.displayName" + @close="closeQrModal"> + <img :src="`data:image/svg+xml;base64,${qrcode}`" + :alt="t('contacts', 'Contact vCard as QR code')" + class="qrcode" + width="400"> + </Modal> + + <!-- pick addressbook when cloning contact --> + <Modal v-if="showPickAddressbookModal" + id="pick-addressbook-modal" + :clear-view-delay="-1" + :title="t('contacts', 'Pick an address book')" + @close="closePickAddressbookModal"> + <Multiselect ref="pickAddressbook" + v-model="pickedAddressbook" + :allow-empty="false" + :options="addressbooksOptions" + :placeholder="t('contacts', 'Select address book')" + track-by="id" + label="name" /> + <button @click="closePickAddressbookModal"> + {{ t('contacts', 'Cancel') }} + </button> + <button class="primary" @click="cloneContact"> + {{ t('contacts', 'Clone contact') }} + </button> + </Modal> <!-- contact details loading --> <section v-if="loadingData" class="icon-loading contact-details" /> @@ -247,7 +242,6 @@ import { VueMasonryPlugin } from 'vue-masonry' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import ActionLink from '@nextcloud/vue/dist/Components/ActionLink' -import Actions from '@nextcloud/vue/dist/Components/Actions' import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails' import Modal from '@nextcloud/vue/dist/Components/Modal' import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' @@ -258,6 +252,7 @@ import validate from '../services/validate' import AddNewProp from './ContactDetails/ContactDetailsAddNewProp' import ContactAvatar from './ContactDetails/ContactDetailsAvatar' import ContactProperty from './ContactDetails/ContactDetailsProperty' +import DetailsHeader from './DetailsHeader' import EmptyContent from './EmptyContent' import PropertyGroups from './Properties/PropertyGroups' import PropertyRev from './Properties/PropertyRev' @@ -272,11 +267,11 @@ export default { components: { ActionButton, ActionLink, - Actions, AddNewProp, AppContentDetails, ContactAvatar, ContactProperty, + DetailsHeader, EmptyContent, Modal, Multiselect, @@ -532,14 +527,6 @@ export default { updateQueue.add(this.updateContact) }, 500), - // menu handling - closeMenu() { - this.openedMenu = false - }, - toggleMenu() { - this.openedMenu = !this.openedMenu - }, - /** * Generate a qrcode for the contact */ @@ -780,126 +767,58 @@ export default { } </script> -<style lang="scss"> +<style lang="scss" scoped> .app-content-details { flex: 1 1 100%; min-width: 0; +} - // Header with avatar, name, position, actions... - header { - display: flex; - align-items: center; - padding: 50px 0 20px; - font-weight: bold; - - // ORG-TITLE-NAME - .contact-header__infos { - display: flex; - flex: 1 1 auto; // shrink avatar before this one - flex-direction: column; - h2, - #details-org-container { - display: flex; - flex-wrap: wrap; - margin: 0; - } - input { - overflow: hidden; - flex: 1 1; - min-width: 100px; - max-width: 100%; - margin: 0; - padding: 4px 5px; - white-space: nowrap; - text-overflow: ellipsis; - border: none; - background: transparent; - font-size: inherit; - &#contact-fullname { - font-weight: bold; - } - } - #contact-org:placeholder-shown { - max-width: 20%; - } - } +// List of all properties +section.contact-details { + margin: 0 auto; + // Relative positioning for masonry + position: relative; - // ACTIONS - .contact-header__actions { - position: relative; - display: flex; - .header-menu { - margin-right: 10px; - } - .header-icon { - width: 44px; - height: 44px; - padding: 14px; - cursor: pointer; - opacity: .7; - border-radius: 22px; - background-size: 16px; - &:hover, - &:focus { - opacity: 1; - } - &.header-icon--pulse { - width: 16px; - height: 16px; - margin: 8px; - } - } - } + ::v-deep .property-masonry { + width: 350px; } - // List of all properties - section.contact-details { - margin: 0 auto; - // Relative positioning for masonry - position: relative; - - .property-masonry { - width: 350px; - } - - .property--rev { - position: fixed; - right: 22px; - bottom: 0; - height: 44px; - opacity: .5; - color: var(--color-text-lighter); - line-height: 44px; - } + .property--rev { + position: fixed; + right: 22px; + bottom: 0; + height: 44px; + opacity: .5; + color: var(--color-text-lighter); + line-height: 44px; } +} - #qrcode-modal { - .modal-container { - display: flex; - padding: 10px; - background-color: #fff; - .qrcode { - max-width: 100%; - } +#qrcode-modal { + ::v-deep .modal-container { + display: flex; + padding: 10px; + background-color: #fff; + .qrcode { + max-width: 100%; } } +} - #pick-addressbook-modal { - .modal-container { - display: flex; - overflow: visible; - flex-wrap: wrap; - justify-content: space-evenly; +#pick-addressbook-modal { + ::v-deep .modal-container { + display: flex; + overflow: visible; + flex-wrap: wrap; + justify-content: space-evenly; + margin-bottom: 20px; + padding: 10px; + background-color: #fff; + .multiselect { + flex: 1 1 100%; + width: 100%; margin-bottom: 20px; - padding: 10px; - background-color: #fff; - .multiselect { - flex: 1 1 100%; - width: 100%; - margin-bottom: 20px; - } } } } - </style> diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index 9d6208d6..e8708743 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -23,72 +23,71 @@ --> <template> - <div class="contact-header-avatar"> - <div v-click-outside="closeMenu" class="contact-header-avatar__wrapper"> - <input id="contact-avatar-upload" - ref="uploadInput" - type="file" - class="hidden" - accept="image/*" - @change="processFile"> |