diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2023-04-18 14:23:59 +0200 |
---|---|---|
committer | Richard Steinmetz <richard@steinmetz.cloud> | 2023-04-24 14:32:00 +0200 |
commit | 7a7a95e4e428f33a7cd4969961832b786806ee42 (patch) | |
tree | 640d5526576adbfda0cf078a2cb0cab2fbc3192f /src | |
parent | e24fb28686802e022fa504fa582b7a080d987037 (diff) |
feat(contacts): implement single column layout
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/ContactDetails.vue | 94 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAddNewProp.vue | 108 | ||||
-rw-r--r-- | src/components/Properties/PropertyGroups.vue | 28 | ||||
-rw-r--r-- | src/components/Properties/PropertyMultipleText.vue | 93 | ||||
-rw-r--r-- | src/components/Properties/PropertySelect.vue | 34 | ||||
-rw-r--r-- | src/components/Properties/PropertyText.vue | 23 | ||||
-rw-r--r-- | src/components/Properties/PropertyTitle.vue | 38 |
7 files changed, 142 insertions, 276 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index a0418733..eb5b2b94 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -192,21 +193,14 @@ <IconLoading v-if="loadingData" :size="20" class="contact-details" /> <!-- contact details --> - <section v-else - v-masonry="contactDetailsSelector" - class="contact-details" - :fit-width="true" - item-selector=".property-masonry" - :transition-duration="0"> + <section v-else class="contact-details"> <!-- properties iteration --> <!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key --> <!-- passing the debounceUpdateContact so that the contact-property component contains the function and allow us to use it on the rfcProps since the scope is forwarded to the actions --> <div v-for="(properties, name) in groupedProperties" - :key="name" - v-masonry-tile - class="property-masonry"> - <ContactProperty v-for="(property, index) in properties" + :key="name"> + <ContactDetailsProperty v-for="(property, index) in properties" :key="`${index}-${contact.key}-${property.name}`" :is-first-property="index===0" :is-last-property="index === properties.length - 1" @@ -215,37 +209,33 @@ :local-contact="localContact" :update-contact="debounceUpdateContact" :contacts="contacts" - :bus="bus" - @resize="debounceRedrawMasonry" /> + :bus="bus" /> </div> <!-- addressbook change select - no last property because class is not applied here, empty property because this is a required prop on regular property-select. But since we are hijacking this... (this is supposed to be used with a ICAL.property, but to avoid code duplication, we created a fake propModel and property with our own options here) --> - <PropertySelect v-masonry-tile - :prop-model="addressbookModel" + <PropertySelect :prop-model="addressbookModel" :options="addressbooksOptions" :value.sync="addressbook" :is-first-property="true" :is-last-property="true" :property="{}" - class="property-masonry property--addressbooks property--last property--without-actions" /> + :hide-actions="true" + class="property--addressbooks property--last" /> <!-- Groups always visible --> - <PropertyGroups v-masonry-tile - :prop-model="groupsModel" + <PropertyGroups :prop-model="groupsModel" :value.sync="groups" :contact="contact" :is-read-only="isReadOnly" - class="property-masonry property--groups property--last" /> + class="property--groups property--last" /> <!-- new property select --> <AddNewProp v-if="!isReadOnly" - v-masonry-tile :bus="bus" - :contact="contact" - class="property-masonry" /> + :contact="contact" /> <!-- Last modified--> <PropertyRev v-if="contact.rev" :value="contact.rev" /> @@ -262,7 +252,6 @@ import debounce from 'debounce' import PQueue from 'p-queue' import qr from 'qr-image' import Vue from 'vue' -import { VueMasonryPlugin } from 'vue-masonry' import ActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import ActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' @@ -283,13 +272,12 @@ import validate from '../services/validate.js' import AddNewProp from './ContactDetails/ContactDetailsAddNewProp.vue' import ContactAvatar from './ContactDetails/ContactDetailsAvatar.vue' -import ContactProperty from './ContactDetails/ContactDetailsProperty.vue' +import ContactDetailsProperty from './ContactDetails/ContactDetailsProperty.vue' import DetailsHeader from './DetailsHeader.vue' import PropertyGroups from './Properties/PropertyGroups.vue' import PropertyRev from './Properties/PropertyRev.vue' import PropertySelect from './Properties/PropertySelect.vue' -Vue.use(VueMasonryPlugin) const updateQueue = new PQueue({ concurrency: 1 }) export default { @@ -301,7 +289,7 @@ export default { AddNewProp, AppContentDetails, ContactAvatar, - ContactProperty, + ContactDetailsProperty, DetailsHeader, EmptyContent, IconContact, @@ -533,16 +521,9 @@ export default { if (this.contactKey && newContact !== oldContact) { this.selectContact(this.contactKey) } - - // Reflow grid - this.debounceRedrawMasonry() }, }, - updated() { - this.debounceRedrawMasonry() - }, - beforeMount() { // load the desired data if we already selected a contact if (this.contactKey) { @@ -824,14 +805,6 @@ export default { return propModel && propType !== 'unknown' }, - - /** - * debounce and redraw Masonry - */ - debounceRedrawMasonry: debounce(function() { - console.debug('Masonry reflow') - this.$redrawVueMasonry(this.contactDetailsSelector) - }, 100), }, } </script> @@ -839,14 +812,9 @@ export default { <style lang="scss" scoped> // List of all properties section.contact-details { - margin: 0 auto; - // Relative positioning for masonry - position: relative; - - ::v-deep .property-masonry { - width: 350px; - padding: 5px; - } + display: flex; + flex-direction: column; + gap: 40px; .property--rev { position: absolute; @@ -886,34 +854,4 @@ section.contact-details { } } } -.property--last { - margin-bottom: 40px; -} -.property { - position: relative; - width: 100%; - max-width: 414px; - justify-self: center; -} -section.contact-details .property-masonry { - width: 350px; -} -.property__label:not(.multiselect) { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - opacity: 0.7; -} -.property__label, .property__label.multiselect { - flex: 1 0; - width: 60px; - min-width: 60px !important; - max-width: 120px; - height: 34px; - margin: 3px 5px 3px 0 !important; - user-select: none; - text-align: right; - background-size: 16px; - line-height: 35px; -} </style> diff --git a/src/components/ContactDetails/ContactDetailsAddNewProp.vue b/src/components/ContactDetails/ContactDetailsAddNewProp.vue index 18b7346d..b6f8c5c1 100644 --- a/src/components/ContactDetails/ContactDetailsAddNewProp.vue +++ b/src/components/ContactDetails/ContactDetailsAddNewProp.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -21,56 +22,62 @@ --> <template> - <div> - <Actions menu-align="right" - event="" - type="secondary" - :menu-title="t('contacts', 'Add more info')" - @click.native.prevent> - <template #icon> - <IconAdd :size="20" /> - </template> - <template v-if="!moreActionsOpen"> - <ActionButton v-for="option in availablePrimaryProperties" - :key="option.id" - class="action--primary" - :close-after-click="true" - @click.prevent="addProp({id: option.id})"> - <template #icon> - <PropertyTitleIcon :icon="option.icon" /> - </template> - {{ option.name }} - </ActionButton> - <ActionButton :close-after-click="false" - @click="moreActionsOpen=true"> - <template #icon> - <DotsHorizontalIcon :title="t('contacts', 'More fields')" - :size="20" /> - </template> - {{ t('contacts', 'More fields') }} - </ActionButton> - </template> - <template v-if="moreActionsOpen"> - <ActionButton :close-after-click="false" - @click="moreActionsOpen=false"> - <template #icon> - <ChevronLeft :title="t('contacts', 'More fields')" - :size="20" /> + <div class="property__row property__row--without-actions"> + <!-- Dummy label to keep the layout --> + <div class="property__label" /> + + <!-- Extra div container to fix the poper position --> + <div class="property__value"> + <Actions menu-align="right" + event="" + type="secondary" + :menu-title="t('contacts', 'Add more info')" + @click.native.prevent> + <template #icon> + <IconAdd :size="20" /> + </template> + <template v-if="!moreActionsOpen"> + <ActionButton v-for="option in availablePrimaryProperties" + :key="option.id" + class="action--primary" + :close-after-click="true" + @click.prevent="addProp({id: option.id})"> + <template #icon> + <PropertyTitleIcon :icon="option.icon" /> + </template> + {{ option.name }} + </ActionButton> + <ActionButton :close-after-click="false" + @click="moreActionsOpen=true"> + <template #icon> + <DotsHorizontalIcon :title="t('contacts', 'More fields')" + :size="20" /> + </template> {{ t('contacts', 'More fields') }} - </template> - </ActionButton> - <ActionButton v-for="option in availableSecondaryProperties" - :key="option.id" - class="action--primary" - :close-after-click="true" - @click.prevent="addProp({id: option.id})"> - <template #icon> - <PropertyTitleIcon :icon="option.icon" /> - </template> - {{ option.name }} - </ActionButton> - </template> - </Actions> + </ActionButton> + </template> + <template v-if="moreActionsOpen"> + <ActionButton :close-after-click="false" + @click="moreActionsOpen=false"> + <template #icon> + <ChevronLeft :title="t('contacts', 'More fields')" + :size="20" /> + {{ t('contacts', 'More fields') }} + </template> + </ActionButton> + <ActionButton v-for="option in availableSecondaryProperties" + :key="option.id" + class="action--primary" + :close-after-click="true" + @click.prevent="addProp({id: option.id})"> + <template #icon> + <PropertyTitleIcon :icon="option.icon" /> + </template> + {{ option.name }} + </ActionButton> + </template> + </Actions> + </div> </div> </template> @@ -227,6 +234,3 @@ export default { }, } </script> -<style lang="scss" scoped> - -</style> diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue index 82c68ae1..d5933d1c 100644 --- a/src/components/Properties/PropertyGroups.vue +++ b/src/components/Properties/PropertyGroups.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -21,11 +22,11 @@ --> <template> - <div v-if="propModel" class="property property--without-actions"> + <div v-if="propModel" class="property"> <PropertyTitle icon="icon-contacts-dark" :readable-name="t('contacts', 'Contact groups')" /> - <div class="property__row"> + <div class="property__row property__row--without-actions"> <div class="property__label"> {{ propModel.readableName }} </div> @@ -185,27 +186,4 @@ export default { }, }, } - </script> -<style lang="scss" scoped> -.property__label:not(.multiselect) { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - opacity: 0.7; -} -.property__row { - position: relative; - display: flex; - align-items: center; -} -.property__label, .property__label.multiselect { - flex: 1 0; - width: 60px; - min-width: 60px !important; - max-width: 120px; - user-select: none; - text-align: right; - background-size: 16px; -} -</style> diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index 1bd9886f..a4f498a8 100644 --- a/src/components/Properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -21,14 +22,22 @@ --> <template> - <div v-if="propModel" class="property"> + <div v-if="propModel" class="property property--multiple-text"> <!-- title if first element --> <PropertyTitle v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" - :readable-name="propModel.readableName" - :has-actions="!isReadOnly" /> - - <div class="property__row"> + :readable-name="propModel.readableName"> + <template #actions> + <!-- props actions --> + <PropertyActions v-if="!showActionsInFirstRow && !isReadOnly" + class="property__actions" + :actions="actions" + :property-component="self" + @delete="deleteProperty" /> + </template> + </PropertyTitle> + + <div v-if="showActionsInFirstRow" class="property__row"> <!-- type selector --> <Multiselect v-if="propModel.options" v-model="localType" @@ -54,6 +63,9 @@ {{ isFirstProperty ? '' : propModel.readableName }} </div> + <!-- or an empty placeholder to keep the layout --> + <div v-else class="property__label" /> + <!-- show the first input if not a structured value --> <input v-if="!property.isStructuredValue" v-model.trim="localValue[0]" @@ -61,10 +73,12 @@ class="property__value" type="text" @input="updateValue"> + <!-- or an empty placeholder to keep the layout --> + <div v-else class="property__value" /> <!-- props actions --> - <PropertyActions v-if="!isReadOnly" - class="property__actions--floating" + <PropertyActions v-if="showActionsInFirstRow && !isReadOnly" + class="property__actions" :actions="actions" :property-component="this" @delete="deleteProperty" /> @@ -72,7 +86,9 @@ <!-- force order based on model --> <template v-if="propModel.displayOrder && propModel.readableValues"> - <div v-for="index in propModel.displayOrder" :key="index" class="property__row"> + <div v-for="index in propModel.displayOrder" + :key="index" + class="property__row property__row--without-actions"> <div class="property__label"> {{ propModel.readableValues[index] }} </div> @@ -88,7 +104,7 @@ <template v-else> <div v-for="(value, index) in filteredValue" :key="index" - class="property__row"> + class="property__row property__row--without-actions"> <div class="property__label" /> <input v-model.trim="filteredValue[index]" :readonly="isReadOnly" @@ -126,49 +142,30 @@ export default { }, computed: { + self() { + // It isn't possible to use "this" in a template slot so it needs to be aliased + // Ref https://stackoverflow.com/a/69485484 + return this + }, + filteredValue() { return this.localValue.filter((value, index) => index > 0) }, + + /** + * Show the actions menu in the first row (instead of the title). + * This is true for all props that either have a type select or a fixed/unknown type. + * Otherwise, show the actions menu next to the title to prevent an empty row with just an + * actions menu. + * + * @return {boolean} + */ + showActionsInFirstRow() { + return !!this.propModel.options + || !!this.selectType + || !this.property.isStructuredValue + } }, } </script> -<style lang="scss" scoped> -.property__label { - flex: 1 0; - width: 60px; - min-width: 60px !important; - max-width: 120px; - user-select: none; - text-align: right; - background-size: 16px; -} -.property__label:not(.multiselect) { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - opacity: 0.7; -} -.property__row { - position: relative; - display: flex; - align-items: center; -} -input:not([type='range']) { - margin: 3px 3px 3px 0; - padding: 7px 6px; - font-size: 13px; - background-color: var(--color-main-background); - color: var(--color-main-text); - outline: none; - border-radius: var(--border-radius); - cursor: text; -} -.property__value { - flex: 1 1; -} -::v-deep.property__label.multiselect .multiselect__tags { - border: none !important; -} - -</style> diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue index e899e3b1..a2bff67a 100644 --- a/src/components/Properties/PropertySelect.vue +++ b/src/components/Properties/PropertySelect.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -27,7 +28,8 @@ :icon="propModel.icon" :readable-name="propModel.readableName" /> - <div class="property__row"> + <div class="property__row" + :class="{'property__row--without-actions': isReadOnly || hideActions}"> <!-- if we do not support any type on our model but one is set anyway --> <div v-if="selectType" class="property__label"> {{ selectType.name }} @@ -48,7 +50,7 @@ @input="updateValue" /> <!-- props actions --> - <PropertyActions v-if="!isReadOnly" + <PropertyActions v-if="!isReadOnly && !hideActions" :actions="actions" :property-component="this" @delete="deleteProperty" /> @@ -79,6 +81,10 @@ export default { default: '', required: true, }, + hideActions: { + type: Boolean, + default: false, + }, }, computed: { @@ -123,28 +129,4 @@ export default { }, }, } - </script> -<style lang="scss" scoped> -.property__label:not(.multiselect) { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - opacity: 0.7; -} -.property__row { - position: relative; - display: flex; - align-items: center; -} -.property__label, .property__label.multiselect { - flex: 1 0; - width: 60px; - min-width: 60px !important; - max-width: 120px; - user-select: none; - text-align: right; - background-size: 16px; -} - -</style> diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue index 6aa938df..a531319c 100644 --- a/src/components/Properties/PropertyText.vue +++ b/src/components/Properties/PropertyText.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -203,25 +204,3 @@ export default { }, } </script> -<style lang="scss" scoped> -.property__label:not(.multiselect) { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - opacity: 0.7; -} -.property__row { - position: relative; - display: flex; - align-items: center; -} -.property__label, .property__label.multiselect { - flex: 1 0; - width: 60px; - min-width: 60px !important; - max-width: 120px; - user-select: none; - text-align: right; - background-size: 16px; -} -</style> diff --git a/src/components/Properties/PropertyTitle.vue b/src/components/Properties/PropertyTitle.vue index f45d2445..3fcf1e14 100644 --- a/src/components/Properties/PropertyTitle.vue +++ b/src/components/Properties/PropertyTitle.vue @@ -2,6 +2,7 @@ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> + - @author Richard Steinmetz <richard@steinmetz.cloud> - - @license GNU AGPL version 3 or any later version - @@ -21,13 +22,20 @@ --> <template> - <h3 class="property__title property__row" - :class="{'align-to-actions': hasActions}"> - <PropertyTitleIcon :icon="icon" /> - <div class="property__value property__title--right"> + <div class="property property--title"> + <div class="property__label"> + <PropertyTitleIcon :icon="icon" /> + </div> + <h3 class="property__value"> {{ readableName }} + </h3> + <div class="property__actions"> + <slot name="actions"> + <!-- empty placeholder to keep the layout --> + <div class="property__actions__empty" /> + </slot> </div> - </h3> + </div> </template> <script> @@ -48,26 +56,6 @@ export default { default: '', required: true, }, - hasActions: { - type: Boolean, - default: false, - }, }, } </script> -<style lang="scss" scoped> -.property__title { - display: flex; - align-items: center; - margin: 0; - user-select: none; - gap: 5px; -} -.property__title .property__title--right { - display: flex; - justify-content: space-between; -} -.align-to-actions { - padding-bottom: 10px; -} -</style> |