summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2023-04-18 14:23:59 +0200
committerRichard Steinmetz <richard@steinmetz.cloud>2023-04-24 14:32:00 +0200
commit7a7a95e4e428f33a7cd4969961832b786806ee42 (patch)
tree640d5526576adbfda0cf078a2cb0cab2fbc3192f /src
parente24fb28686802e022fa504fa582b7a080d987037 (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.vue94
-rw-r--r--src/components/ContactDetails/ContactDetailsAddNewProp.vue108
-rw-r--r--src/components/Properties/PropertyGroups.vue28
-rw-r--r--src/components/Properties/PropertyMultipleText.vue93
-rw-r--r--src/components/Properties/PropertySelect.vue34
-rw-r--r--src/components/Properties/PropertyText.vue23
-rw-r--r--src/components/Properties/PropertyTitle.vue38
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>