<!-- - @copyright 2023 Christopher Ng <chrng8@gmail.com> - - @author Christopher Ng <chrng8@gmail.com> - - @license AGPL-3.0-or-later - - 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> <NcHeaderMenu id="contactsmenu" class="contactsmenu" :aria-label="t('core', 'Search contacts')" @open="handleOpen"> <template #trigger> <Contacts :size="20" /> </template> <div class="contactsmenu__menu"> <div class="contactsmenu__menu__input-wrapper"> <NcTextField :value.sync="searchTerm" trailing-button-icon="close" ref="contactsMenuInput" :label="t('core', 'Search contacts')" :trailing-button-label="t('core','Reset search')" :show-trailing-button="searchTerm !== ''" :placeholder="t('core', 'Search contacts …')" id="contactsmenu__menu__search" class="contactsmenu__menu__search" @input="onInputDebounced" @trailing-button-click="onReset" /> </div> <NcEmptyContent v-if="error" :name="t('core', 'Could not load your contacts')"> <template #icon> <Magnify /> </template> </NcEmptyContent> <NcEmptyContent v-else-if="loadingText" :name="loadingText"> <template #icon> <NcLoadingIcon /> </template> </NcEmptyContent> <NcEmptyContent v-else-if="contacts.length === 0" :name="t('core', 'No contacts found')"> <template #icon> <Magnify /> </template> </NcEmptyContent> <div v-else class="contactsmenu__menu__content"> <div id="contactsmenu-contacts"> <ul> <Contact v-for="contact in contacts" :key="contact.id" :contact="contact" /> </ul> </div> <div v-if="contactsAppEnabled" class="contactsmenu__menu__content__footer"> <NcButton type="tertiary" :href="contactsAppURL"> {{ t('core', 'Show all contacts') }} </NcButton> </div> <div v-else-if="canInstallApp" class="contactsmenu__menu__content__footer"> <NcButton type="tertiary" :href="contactsAppMgmtURL"> {{ t('core', 'Install the Contacts app') }} </NcButton> </div> </div> </div> </NcHeaderMenu> </template> <script> import axios from '@nextcloud/axios' import Contacts from 'vue-material-design-icons/Contacts.vue' import debounce from 'debounce' import { getCurrentUser } from '@nextcloud/auth' import { generateUrl } from '@nextcloud/router' import Magnify from 'vue-material-design-icons/Magnify.vue' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import { translate as t } from '@nextcloud/l10n' import Contact from '../components/ContactsMenu/Contact.vue' import logger from '../logger.js' import Nextcloud from '../mixins/Nextcloud.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' export default { name: 'ContactsMenu', components: { Contact, Contacts, Magnify, NcButton, NcEmptyContent, NcHeaderMenu, NcLoadingIcon, NcTextField, }, mixins: [Nextcloud], data() { const user = getCurrentUser() return { contactsAppEnabled: false, contactsAppURL: generateUrl('/apps/contacts'), contactsAppMgmtURL: generateUrl('/settings/apps/social/contacts'), canInstallApp: user.isAdmin, contacts: [], loadingText: undefined, error: false, searchTerm: '', } }, methods: { async handleOpen() { await this.getContacts('') }, async getContacts(searchTerm) { if (searchTerm === '') { this.loadingText = t('core', 'Loading your contacts …') } else { this.loadingText = t('core', 'Looking for {term} …', { term: searchTerm, }) } // Let the user try a different query if the previous one failed this.error = false try { const { data: { contacts, contactsAppEnabled } } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: searchTerm, }) this.contacts = contacts this.contactsAppEnabled = contactsAppEnabled this.loadingText = undefined } catch (error) { logger.error('could not load contacts', { error, searchTerm, }) this.error = true } }, onInputDebounced: debounce(function() { this.getContacts(this.searchTerm) }, 500), /** * Reset the search state */ onReset() { this.searchTerm = '' this.contacts = [] this.focusInput() }, /** * Focus the search input on next tick */ focusInput() { this.$nextTick(() => { this.$refs.contactsMenuInput.focus() this.$refs.contactsMenuInput.select() }) }, }, } </script> <style lang="scss" scoped> .contactsmenu { overflow-y: hidden; &__menu { display: flex; flex-direction: column; overflow: hidden; height: calc(50px * 6 + 2px + 26px); max-height: inherit; label[for="contactsmenu__menu__search"] { font-weight: bold; font-size: 19px; margin-left: 13px; } &__input-wrapper { padding: 10px; z-index: 2; top: 0; } &__search { width: 100%; height: 34px; margin-top: 0!important; } &__content { overflow-y: auto; margin-top: 10px; flex: 1 1 auto; &__footer { display: flex; flex-direction: column; align-items: center; } } a { &:focus-visible { box-shadow: inset 0 0 0 2px var(--color-main-text) !important; // override rule in core/css/headers.scss #header a:focus-visible } } } } </style>