diff --git a/.eslintrc.js b/.eslintrc.js
index 7c652642b..8aabc2d3b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -27,6 +27,7 @@ module.exports = {
 		'plugin:node/recommended',
 		'plugin:vue/essential',
 		'plugin:vue/recommended',
+		'plugin:nextcloud/recommended',
 		'standard'
 	],
 	settings: {
@@ -73,8 +74,14 @@ module.exports = {
 		// es6 import/export and require
 		'node/no-unpublished-require': ['off'],
 		'node/no-unsupported-features/es-syntax': ['off'],
-		// kebab case components for vuejs
+		// PascalCase components names for vuejs
+		// https://vuejs.org/v2/style-guide/#Single-file-component-filename-casing-strongly-recommended
 		'vue/component-name-in-template-casing': ['error', 'PascalCase'],
+		// force name
+		'vue/match-component-file-name': ['error', {
+			'extensions': ['jsx', 'vue', 'js'],
+			'shouldMatchCase': true
+		 }],
 		// space before self-closing elements
 		'vue/html-closing-bracket-spacing': 'error',
 		// no ending html tag on a new line
diff --git a/css/ContactDetails.scss b/css/ContactDetails.scss
index 6c4e53f7d..85796c73a 100644
--- a/css/ContactDetails.scss
+++ b/css/ContactDetails.scss
@@ -21,19 +21,20 @@
  */
 
 #contact-details {
-
+	$grid-column-gap: 20px;
+	$grid-column-width: 350px;
 	// header
 	header {
-		height: 100px;
 		display: flex;
-		font-weight: bold;
 		align-items: center;
+		height: 100px;
+		font-weight: bold;
 
 		// ORG-TITLE-NAME
 		#contact-header-infos {
 			display: flex;
-			flex-direction: column;
 			flex: 1 1 auto; // shrink avatar before this one
+			flex-direction: column;
 			h2,
 			#details-org-container {
 				display: flex;
@@ -41,22 +42,22 @@
 				margin: 0;
 			}
 			input {
-				font-size: inherit;
-				color: #fff !important;
-				text-shadow: 0 0 2px var(--color-box-shadow);
-				background: transparent;
-				text-overflow: ellipsis;
 				overflow: hidden;
-				white-space: nowrap;
-				border: none;
-				margin: 0;
-				padding: 4px 5px;
 				flex: 1 1;
 				min-width: 100px;
 				max-width: 100%;
+				margin: 0;
+				padding: 4px 5px;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+				color: #fff !important;
+				border: none;
+				background: transparent;
+				text-shadow: 0 0 2px var(--color-box-shadow);
+				font-size: inherit;
 				&::placeholder {
-					color: #fff !important;
 					opacity: .8;
+					color: #fff !important;
 				}
 			}
 			#contact-org {
@@ -76,45 +77,43 @@
 				}
 			}
 			.header-icon {
-				height: 44px;
 				width: 44px;
+				height: 44px;
 				padding: 14px;
-				border-radius: 22px;
 				cursor: pointer;
-				background-size: 16px;
 				opacity: .7;
+				border-radius: 22px;
+				background-size: 16px;
 				&:hover,
 				&:focus {
 					opacity: 1;
 				}
 				&.header-icon--pulse {
-					margin: 8px;
 					width: 16px;
 					height: 16px;
+					margin: 8px;
 				}
 			}
 		}
 	}
 
-	$grid-column-gap: 20px;
-	$grid-column-width: 350px;
-
 	// contact details
 	section.contact-details {
 		display: grid;
+		min-height: 200px;
+		padding: 20px $grid-column-gap;
 		/* unquote is a strange hack to avoid removal of the comma by the scss compiler */
 		grid-template-columns: repeat(auto-fit, minmax(unquote('#{$grid-column-width}'), 1fr));
 		grid-column-gap: $grid-column-gap;
-		padding: 20px $grid-column-gap;
-		min-height: 200px;
 	}
 
 	// single column fix, better visual
 	@media only screen and (max-width: $navigation-width + $list-min-width + 2 * $grid-column-gap +$grid-column-width) {
 		section.contact-details {
+			padding: 10px;
+
 			grid-template-columns: 1fr;
 			grid-column-gap: 10px;
-			padding: 10px;
 		}
 	}
 }
@@ -124,9 +123,9 @@
 	right: 22px;
 	bottom: 0;
 	height: 44px;
-	line-height: 44px;
-	color: var(--color-text-lighter);
 	opacity: .5;
+	color: var(--color-text-lighter);
+	line-height: 44px;
 }
 
 #qrcode-modal {
diff --git a/css/ContactDetailsAvatar.scss b/css/ContactDetailsAvatar.scss
index 6ec6cfe81..d2fa69bc8 100644
--- a/css/ContactDetailsAvatar.scss
+++ b/css/ContactDetailsAvatar.scss
@@ -37,37 +37,37 @@
 		margin-left: auto;
 	}
 	&__background {
-		opacity: .2;
 		z-index: 0;
-		left: 0;
 		top: 50px;
+		left: 0;
+		opacity: .2;
 	}
 	&__photo,
 	&__options {
+		overflow: hidden;
 		width: 100%;
 		height: 100%;
 		border-radius: 50%;
-		overflow: hidden;
 	}
 	&__photo {
 		z-index: 10;
-		background-size: cover;
+		cursor: pointer;
 		background-repeat: no-repeat;
 		background-position: center;
-		cursor: pointer;
+		background-size: cover;
 	}
 	&__options {
-		top: 0;
-		z-index: 2;
 		position: absolute;
-		background-color: rgba(0, 0, 0, 0.2);
+		z-index: 2;
+		top: 0;
+		background-color: rgba(0, 0, 0, .2);
 	}
 	.contact-avatar-options {
+		display: block;
 		width: 100%;
 		height: 100%;
-		display: block;
 		opacity: .5;
-		background-color: rgba(0, 0, 0, 0.2);
+		background-color: rgba(0, 0, 0, .2);
 		&:hover,
 		&:active,
 		&:focus {
@@ -103,4 +103,3 @@
 		}
 	}
 }
-
diff --git a/css/ContactsList.scss b/css/ContactsList.scss
index 0851c4479..670c1231a 100644
--- a/css/ContactsList.scss
+++ b/css/ContactsList.scss
@@ -22,16 +22,16 @@
 
 #app-details-toggle {
 	position: fixed;
-	display: inline-block;
+	z-index: 149;
 	left: 0;
+	display: inline-block;
 	width: 44px;
 	height: 44px;
-	z-index: 149;
-	background-color: var(--color-background-darker);
+	margin-top: 44px; // under the show navigation button
 	cursor: pointer;
-	opacity: 0.6;
 	transform: rotate(180deg);
-	margin-top: 44px; // under the show navigation button
+	opacity: .6;
+	background-color: var(--color-background-darker);
 }
 
 
@@ -41,10 +41,10 @@
 }
 
 .vue-recycle-scroller__item-view {
-	// same as app-content-list-item
-	height: 68px;
 	// TODO: find better solution?
 	// https://github.com/Akryum/vue-virtual-scroller/issues/70
 	// hack to not show the transition
 	overflow: hidden;
-}
\ No newline at end of file
+	// same as app-content-list-item
+	height: 68px;
+}
diff --git a/css/ContactsListItem.scss b/css/ContactsListItem.scss
index 4fcad0df4..87e1cb066 100644
--- a/css/ContactsListItem.scss
+++ b/css/ContactsListItem.scss
@@ -45,7 +45,7 @@
 		left: 0;
 		width: 100%;
 	}
-	
+
 	&.delete-slide-left-enter,
 	&.delete-slide-left-leave-to {
 		left: 100%;
@@ -58,9 +58,9 @@
 		position: absolute;
 		top: 0;
 		left: 0;
-		height: inherit;
 		width: inherit;
-		background-size: cover;
+		height: inherit;
 		cursor: pointer;
+		background-size: cover;
 	}
 }
diff --git a/css/ImportScreen.scss b/css/ImportScreen.scss
index 4899060f5..0eb9cd258 100644
--- a/css/ImportScreen.scss
+++ b/css/ImportScreen.scss
@@ -21,9 +21,9 @@
  */
 
 .import-screen {
-	margin: 50px;
 	width: auto;
 	min-width: 30vw;
+	margin: 50px;
 	&__header {
 		padding-top: 20px;
 	}
diff --git a/css/Properties/Properties.scss b/css/Properties/Properties.scss
index 9ba2f0d32..318a74513 100644
--- a/css/Properties/Properties.scss
+++ b/css/Properties/Properties.scss
@@ -24,15 +24,15 @@ $property-label-max-width: 2 * $property-label-min-width;
 $property-value-max-width: 250px;
 
 .property {
-	@include generate-grid-span(1);
 	position: relative;
-	padding-right: 44px; // actions menu / button
+	width: 100%;
 	// we need this to keep the alignment of the ext and delete/action button
 	// The flex grow will never go over those values. Therefore we can set
 	// the max width and keep the right alignment
 	max-width: $property-label-max-width + $property-value-max-width + 44px;
+
+	@include generate-grid-span(1);
 	justify-self: center;
-	width: 100%;
 
 	&--last {
 		margin-bottom: $grid-height-unit;
@@ -42,25 +42,30 @@ $property-value-max-width: 250px;
 		display: none !important;
 	}
 
+	&--without-actions {
+		padding-right: 44px; // actions menu / button
+	}
+
 	// property row
 	&__row {
+		position: relative;
 		display: flex;
 		align-items: center;
-		position: relative;
 	}
 
 	// property label or multiselect within row
 	&__label,
 	&__label.multiselect {
-		margin: $grid-input-margin 5px $grid-input-margin 0 !important; // override multiselect
 		flex: 1 0; // min width is 60px, let's grow until 120px
-		height: $grid-input-height-with-margin;
 		width: $property-label-min-width;
 		min-width: $property-label-min-width !important; // override multiselect
 		max-width: $property-label-max-width;
-
+		height: $grid-input-height-with-margin;
+		margin: $grid-input-margin 5px $grid-input-margin 0 !important; // override multiselect
 		user-select: none;
+		text-align: right;
 		background-size: 16px;
+		line-height: $grid-input-height-with-margin + 1px;
 
 		&,
 		.multiselect__input {
@@ -68,17 +73,18 @@ $property-value-max-width: 250px;
 				text-align: right;
 			}
 			+ .multiselect__single {
+				overflow: hidden;
 				white-space: nowrap;
 				text-overflow: ellipsis;
-				overflow: hidden;
 			}
 		}
 
 		&:not(.multiselect) {
-			text-overflow: ellipsis;
-			white-space: nowrap;
 			overflow: hidden;
 			overflow-x: hidden;
+			white-space: nowrap;
+			text-overflow: ellipsis;
+			opacity: .7;
 		}
 
 		// mouse feedback
@@ -89,8 +95,8 @@ $property-value-max-width: 250px;
 		&:focus,
 		&:active {
 			.multiselect__tags {
-				border-color: var(--color-border-dark);
 				opacity: 1;
+				border-color: var(--color-border-dark);
 			}
 		}
 
@@ -98,8 +104,8 @@ $property-value-max-width: 250px;
 		&.multiselect--disabled {
 			&, .multiselect__single {
 				&, &:hover, &:focus &:active {
-					background-color: var(--color-main-background) !important;
 					border-color: transparent !important;
+					background-color: var(--color-main-background) !important;
 				}
 			}
 		}
@@ -109,29 +115,28 @@ $property-value-max-width: 250px;
 		.multiselect__tags {
 			border: none !important; // override multiselect
 			.multiselect__single {
+				padding-right: 24px;
 				background-repeat: no-repeat;
 				background-position: center right 4px;
-				padding-right: 24px;
 			}
 		}
 		.multiselect__content-wrapper {
-			min-width: $property-label-max-width; // improve readability on narrow screens
-			width: auto !important; // grow bigger if content is bigger than the original 100%
 			right: 0; // align right
+			width: auto !important; // grow bigger if content is bigger than the original 100%
+			min-width: $property-label-max-width; // improve readability on narrow screens
 		}
 		@media only screen and (max-width: 768px) {
 			// align left of screen on narrow views
 			.multiselect__content-wrapper {
-				left: 0;
 				right: auto;
+				left: 0;
 			}
 		}
 	}
 
 	// Property value within row, after label
 	&__value {
-		flex: 1 1 $property-value-max-width;
-		max-width: $property-value-max-width;
+		flex: 1 1;
 
 		textarea& {
 			align-self: flex-start;
@@ -157,7 +162,7 @@ $property-value-max-width: 250px;
 	}
 
 	// show ext button on full row hover
-	&:hover &__ext{
+	&:hover &__ext {
 		opacity: .5;
 	}
 
@@ -176,13 +181,14 @@ $property-value-max-width: 250px;
 
 	// Delete property button + actions
 	&__actions {
-		position: absolute !important;
-		top: 0;
-		left: 100%;
-		margin: -2px 2px; // align with line because of the 44x44px size
-		border: 0;
-		background-color: transparent;
 		z-index: 10;
+		margin-left: auto !important;
+		// floating actions next to the title
+		&--floating {
+			position: absolute !important;
+			right: 0;
+			bottom: 0;
+		}
 	}
 	.property__value {
 		margin-right: 0;
diff --git a/css/Properties/PropertyTitle.scss b/css/Properties/PropertyTitle.scss
index aaa923041..eed164965 100644
--- a/css/Properties/PropertyTitle.scss
+++ b/css/Properties/PropertyTitle.scss
@@ -24,8 +24,8 @@
 	display: flex;
 	align-items: center;
 	margin: 0;
-	opacity: 0.6;
 	user-select: none;
+	opacity: .6;
 
 	.property__title--right {
 		display: flex;
diff --git a/css/SettingsSection.scss b/css/SettingsSection.scss
index 09953f398..0f01dff3e 100644
--- a/css/SettingsSection.scss
+++ b/css/SettingsSection.scss
@@ -45,9 +45,10 @@
 		margin: 0;
 		.multiselect__single {
 			padding-right: 24px !important;
-			@include icon-color('triangle-s', 'actions', $color-black, 1, true);
 			background-repeat: no-repeat;
 			background-position: right 4px center;
+
+			@include icon-color('triangle-s', 'actions', $color-black, 1, true);
 		}
 	}
 }
@@ -57,13 +58,13 @@
 	display: flex;
 	flex-direction: column;
 	&__multiselect-label {
+		z-index: 2;
 		width: 100%;
+		margin: 0;
 		padding: 6px 12px;
 		padding-left: 34px;
-		margin: 0;
 		border-radius: var(--border-radius) var(--border-radius) 0 0;
 		background-position: left 9px center;
-		z-index: 2;
 		&--no-select {
 			border-radius: var(--border-radius);
 		}
diff --git a/css/contacts.scss b/css/contacts.scss
index 0b458fd48..55602537c 100644
--- a/css/contacts.scss
+++ b/css/contacts.scss
@@ -25,7 +25,7 @@ $grid-height-unit: 40px;
 $grid-input-padding: 7px;
 $grid-input-margin: 3px;
 $grid-column-width: 380px;
-$grid-input-height-with-margin: #{$grid-height-unit - $grid-input-margin * 2};
+$grid-input-height-with-margin: $grid-height-unit - $grid-input-margin * 2;
 
 @mixin generate-grid-span($default-unit) {
 	// we only supports 10 props of the same type
diff --git a/css/icons.scss b/css/icons.scss
index a543346ff..4f74e7d4a 100644
--- a/css/icons.scss
+++ b/css/icons.scss
@@ -20,29 +20,14 @@
  *
  */
 
-.icon-social {
-	@include icon-color('social', 'contacts', $color-black, 1);
-}
-
-.icon-qrcode {
-	@include icon-color('qrcode', 'contacts', $color-black, 2);
-}
-
-.icon-address-book {
-	@include icon-color('address-book', 'contacts', $color-black, 1);
-}
 
-.icon-phone {
-	@include icon-color('phone', 'contacts', $color-black, 1);
-}
-
-.icon-eye-white {
-	@include icon-color('eye', 'contacts', $color-white, 1);
-}
-
-.icon-up {
-	@include icon-color('up', 'contacts', $color-black, 1);
-}
+@include icon-black-white('social', 'contacts', 1);
+@include icon-black-white('qrcode', 'contacts', 1);
+@include icon-black-white('address-book', 'contacts', 1);
+@include icon-black-white('phone', 'contacts', 1);
+@include icon-black-white('eye', 'contacts', 1);
+@include icon-black-white('up', 'contacts', 1);
+@include icon-black-white('no-calendar', 'contacts', 1);
 
 .icon-up-force-white {
 	// using #fffffe to trick the accessibility dark theme icon invert
diff --git a/img/no-calendar.svg b/img/no-calendar.svg
new file mode 100644
index 000000000..6a328ca16
--- /dev/null
+++ b/img/no-calendar.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M4 1c-.5 0-1 .5-1 1v2c0 .5.5 1 1 1s1-.5 1-1V2c0-.5-.5-1-1-1zm8 0c-.5 0-1 .5-1 1v2c0 .223.11.439.264.617L13 2.881V2c0-.5-.5-1-1-1zM5.5 3v1c0 .831-.5 1.5-1.5 1.5S2.5 5 2.5 4v-.938A1.998 1.998 0 0 0 1 5v8c0 .524.202.996.53 1.352L3 12.88V8h4.88l2.942-2.941C10.61 4.81 10.5 4.46 10.5 4V3h-5zM15 5.123L12.123 8H13v5H7.123l-2 2H13c1.108 0 2-.892 2-2V5.123z"/><path d="M13.773 2.756L1.903 14.627 3.274 16 15.146 4.129z" paint-order="fill markers stroke"/></svg>
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6b1fa06db..fc4a86322 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4053,6 +4053,15 @@
         }
       }
     },
+    "eslint-plugin-nextcloud": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-nextcloud/-/eslint-plugin-nextcloud-0.3.0.tgz",
+      "integrity": "sha512-LUD2qdirGL0BRt4uaMDGxen17mWVq9JwuGDt7P7Celz7bzdu0X48RrS8mhXn9e0w78+nYN5kPoULG2Bw04r4HA==",
+      "dev": true,
+      "requires": {
+        "requireindex": "~1.2.0"
+      }
+    },
     "eslint-plugin-node": {
       "version": "9.2.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz",
@@ -5763,7 +5772,7 @@
       "dependencies": {
         "readable-stream": {
           "version": "3.0.6",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz",
           "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==",
           "dev": true,
           "requires": {
@@ -8810,7 +8819,7 @@
     },
     "readable-stream": {
       "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
       "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
       "dev": true,
       "requires": {
@@ -9127,6 +9136,12 @@
       "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
       "dev": true
     },
+    "requireindex": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+      "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+      "dev": true
+    },
     "resolve": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
@@ -9946,7 +9961,7 @@
     },
     "stream-browserify": {
       "version": "2.0.1",
-      "resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
       "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
       "dev": true,
       "requires": {
@@ -10012,7 +10027,7 @@
     },
     "string_decoder": {
       "version": "1.1.1",
-      "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
       "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
       "dev": true,
       "requires": {
@@ -10752,7 +10767,7 @@
     },
     "tty-browserify": {
       "version": "0.0.0",
-      "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
       "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
       "dev": true
     },
diff --git a/package.json b/package.json
index 8467372db..3e2358e22 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
 		"nextcloud-dialogs": "0.0.3",
 		"nextcloud-l10n": "0.1.0",
 		"nextcloud-router": "0.0.8",
-		"nextcloud-vue": "^0.12.1",
+		"nextcloud-vue": "^0.12.2",
 		"p-limit": "^2.2.1",
 		"p-queue": "^6.1.1",
 		"qr-image": "^3.2.0",
@@ -81,6 +81,7 @@
 		"eslint-import-resolver-webpack": "^0.11.1",
 		"eslint-loader": "^3.0.0",
 		"eslint-plugin-import": "^2.18.2",
+		"eslint-plugin-nextcloud": "^0.3.0",
 		"eslint-plugin-node": "^9.2.0",
 		"eslint-plugin-promise": "^4.2.1",
 		"eslint-plugin-standard": "^4.0.1",
diff --git a/src/App.vue b/src/ContactsRoot.vue
similarity index 100%
rename from src/App.vue
rename to src/ContactsRoot.vue
diff --git a/src/components/Actions/ActionCopyNtoFN.vue b/src/components/Actions/ActionCopyNtoFN.vue
new file mode 100644
index 000000000..ccce1fb84
--- /dev/null
+++ b/src/components/Actions/ActionCopyNtoFN.vue
@@ -0,0 +1,51 @@
+<!--
+  - @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>
+	<ActionButton icon="icon-up" @click="copyNtoFN">
+		{{ t('contacts', 'Copy to full name') }}
+	</ActionButton>
+</template>
+<script>
+import { ActionButton } from 'nextcloud-vue'
+import ActionsMixin from 'Mixins/ActionsMixin'
+
+export default {
+	name: 'ActionCopyNtoFN',
+	components: {
+		ActionButton
+	},
+	mixins: [ActionsMixin],
+	methods: {
+		copyNToFN() {
+			console.info(this.component)
+			if (this.component.contact.vCard.hasProperty('n')) {
+				// Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.
+				// -> John Stevenson
+				const n = this.component.contact.vCard.getFirstPropertyValue('n')
+				this.component.contact.fullName = n.slice(0, 2).reverse().join(' ')
+				this.component.updateContact()
+			}
+		}
+	}
+}
+</script>
diff --git a/src/components/Actions/ActionToggleYear.vue b/src/components/Actions/ActionToggleYear.vue
new file mode 100644
index 000000000..9c204fbd4
--- /dev/null
+++ b/src/components/Actions/ActionToggleYear.vue
@@ -0,0 +1,79 @@
+<!--
+  - @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>
+	<ActionCheckbox :checked="omitYear"
+		@check="removeYear" @uncheck="addYear">
+		{{ t('contacts', 'Omit year') }}
+	</ActionCheckbox>
+</template>
+<script>
+import { ActionCheckbox } from 'nextcloud-vue'
+import ActionsMixin from 'Mixins/ActionsMixin'
+
+export default {
+	name: 'ActionToggleYear',
+	components: {
+		ActionCheckbox
+	},
+	mixins: [ActionsMixin],
+	data() {
+		return {
+			omitYear: false
+		}
+	},
+
+	beforeMount() {
+		this.omitYear = !!this.component.property.getFirstParameter('x-apple-omit-year')
+				|| !this.component.value.year // if null
+	},
+
+	methods: {
+		removeYear() {
+			const dateObject = this.component.localValue.toJSON()
+
+			// year was already displayed: removing it
+			// and use --0124 format
+			if (this.component.localContact.version === '4.0') {
+				dateObject.year = null
+				this.component.updateValue(dateObject)
+			} else {
+				// --0124 format is only for vcards 4.0
+				// using x-apple-omit-year custom parameter
+				const year = this.component.value.year
+				if (this.component.value.year) {
+					this.component.property.setParameter('x-apple-omit-year', parseInt(year).toString())
+					this.$nextTick(() => {
+						this.component.updateValue(dateObject)
+					})
+				}
+			}
+			this.omitYear = !this.omitYear
+		},
+		addYear() {
+			const dateObject = this.component.localValue.toJSON()
+			this.component.updateValue(dateObject, true)
+			this.omitYear = !this.omitYear
+		}
+	}
+}
+</script>
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index 9c8b63175..86b0333d4 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -128,7 +128,7 @@
 					:prop-model="addressbookModel" :value.sync="addressbook"
 					:is-first-property="true" :is-last-property="true"
 					:property="{}"
-					class="property--addressbooks property--last" />
+					class="property--addressbooks property--last property--without-actions" />
 
 				<!-- Groups always visible -->
 				<PropertyGroups :prop-model="groupsModel" :value.sync="groups" :contact="contact"
diff --git a/src/components/ContactDetails/ContactDetailsAddNewProp.vue b/src/components/ContactDetails/ContactDetailsAddNewProp.vue
index 8cbe98417..1400d624b 100644
--- a/src/components/ContactDetails/ContactDetailsAddNewProp.vue
+++ b/src/components/ContactDetails/ContactDetailsAddNewProp.vue
@@ -21,7 +21,7 @@
   -->
 
 <template>
-	<div class="grid-span-3 property property--last">
+	<div class="grid-span-3 property property--without-actions property--last">
 		<!-- title -->
 		<PropertyTitle :icon="'icon-add'" :readable-name="t('contacts', 'Add new property')" />
 
@@ -57,11 +57,11 @@ export default {
 	computed: {
 
 		/**
-		 * Rfc props scoped
+		 * Rfc props
 		 * @returns {Object}
 		 */
 		properties() {
-			return rfcProps.properties(this)
+			return rfcProps.properties
 		},
 
 		/**
diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue
index 2f0f587aa..fbfea2505 100644
--- a/src/components/ContactDetails/ContactDetailsAvatar.vue
+++ b/src/components/ContactDetails/ContactDetailsAvatar.vue
@@ -82,7 +82,7 @@ import { generateRemoteUrl } from 'nextcloud-router'
 const axios = () => import('axios')
 
 export default {
-	name: 'ContactAvatar',
+	name: 'ContactDetailsAvatar',
 
 	components: {
 		ActionLink,
diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue
index 0a4aaf382..ac2452f52 100644
--- a/src/components/ContactDetails/ContactDetailsProperty.vue
+++ b/src/components/ContactDetails/ContactDetailsProperty.vue
@@ -22,11 +22,11 @@
 
 <template>
 	<!-- If not in the rfcProps then we don't want to display it -->
-	<component :is="componentInstance" v-if="propModel && propType !== 'unknown'" :select-type.sync="selectType"
-		:prop-model="propModel" :value.sync="value" :is-first-property="isFirstProperty"
-		:property="property" :is-last-property="isLastProperty" :class="{'property--last': isLastProperty}"
-		:local-contact="localContact" :prop-name="propName" :prop-type="propType"
-		:options="sortedModelOptions" :is-read-only="isReadOnly"
+	<component :is="componentInstance" v-if="propModel && propType !== 'unknown'" ref="component"
+		:select-type.sync="selectType" :prop-model="propModel" :value.sync="value"
+		:is-first-property="isFirstProperty" :property="property" :is-last-property="isLastProperty"
+		:class="{'property--last': isLastProperty}" :local-contact="localContact" :prop-name="propName"
+		:prop-type="propType" :options="sortedModelOptions" :is-read-only="isReadOnly"
 		@delete="deleteProp" @update="updateContact" />
 </template>
 
@@ -93,10 +93,8 @@ export default {
 		},
 
 		// rfc properties list
-		// passing this to properties to allow us to scope the properties object
-		// this make possible defining actions there
 		properties() {
-			return rfcProps.properties(this)
+			return rfcProps.properties
 		},
 		fieldOrder() {
 			return rfcProps.fieldOrder
diff --git a/src/components/Properties/PropertyActions.vue b/src/components/Properties/PropertyActions.vue
index 8a611d377..bbf908ed1 100644
--- a/src/components/Properties/PropertyActions.vue
+++ b/src/components/Properties/PropertyActions.vue
@@ -25,10 +25,8 @@
 		<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 :is="action" v-for="(action, index) in actions" :key="index"
+			:component="propertyComponent" />
 	</Actions>
 </template>
 
@@ -46,6 +44,10 @@ export default {
 		actions: {
 			type: Array,
 			default: () => []
+		},
+		propertyComponent: {
+			type: Object,
+			required: true
 		}
 	},
 
diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue
index 71561c425..83b1a314b 100644
--- a/src/components/Properties/PropertyDateTime.vue
+++ b/src/components/Properties/PropertyDateTime.vue
@@ -43,14 +43,14 @@
 				{{ propModel.readableName }}
 			</div>
 
-			<!-- props actions -->
-			<PropertyActions :actions="actions" @delete="deleteProperty" />
-
 			<!-- Real input where the picker shows -->
 			<DatetimePicker :value="vcardTimeLocalValue.toJSDate()" :minute-step="10" :lang="lang"
 				:clearable="false" :first-day-of-week="firstDay" :type="inputType"
 				:readonly="isReadOnly" :format="dateFormat" class="property__value"
-				confirm @confirm="updateValue" />
+				confirm @confirm="debounceUpdateValue" />
+
+			<!-- props actions -->
+			<PropertyActions :actions="actions" :property-component="this" @delete="deleteProperty" />
 		</div>
 	</div>
 </template>
@@ -159,42 +159,67 @@ export default {
 		/**
 		 * Debounce and send update event to parent
 		 */
-		updateValue: debounce(function(e) {
+		debounceUpdateValue: debounce(function(date) {
 			const objMap = ['year', 'month', 'day', 'hour', 'minute', 'second']
-			let rawArray = moment(e).toArray()
+			const rawArray = moment(date).toArray()
 
-			const rawObject = rawArray.reduce((acc, cur, index) => {
+			let dateObject = rawArray.reduce((acc, cur, index) => {
 				acc[objMap[index]] = cur
 				return acc
 			}, {})
 
+			/**
+			 * VCardTime starts months at 1
+			 * but moment and js starts at 0
+			 * ! since we use moment to generate our time array
+			 * ! we need to make sure the conversion to VCardTime is done well
+			 */
+			dateObject.month++
+
+			this.updateValue(dateObject)
+		}, 500),
+
+		updateValue(dateObject, forceYear) {
+			const ignoreYear = this.property.getParameter('x-apple-omit-year')
+
+			/**
+			 * If forceYear, we add back the year!
+			 * taken from x-apple-omit-year parameter
+			 * of from the current year if we don't have
+			 * any other appropriate year data
+			 */
+			if (forceYear) {
+				this.property.removeParameter('x-apple-omit-year')
+				dateObject.year = parseInt(ignoreYear) ? ignoreYear : moment().year()
+			} else
+
 			/**
 			 * Use the current year to ensure we do not lose
 			 * the year data on v4.0 since we currently have
 			 * no options to remove the year selection.
 			 * ! using this.value since this.localValue reflect the current change
 			 * ! so we need to make sure we do not use the updated data
-			 * TODO: add option to omit year and not use already existing data
+			 * If we force the removal of the year (vcard 4.0 only)
+			 * year is still valid on the apple format x-apple-omit-year
 			 */
-			if (this.value.year === null) {
-				rawObject.year = null
+			if (!this.value.year) {
+				dateObject.year = null
+			} else
+
+			// Apple style omit year parameter
+			// if year changed and we were already
+			// ignoring the year, we update the parameter
+			if (ignoreYear && dateObject.year) {
+				this.property.setParameter('x-apple-omit-year', parseInt(dateObject.year).toString())
 			}
 
-			/**
-			 * VCardTime starts months at 1
-			 * but moment and js starts at 0
-			 * ! since we use moment to generate our time array
-			 * ! we need to make sure the conversion to VCardTime is done well
-			 */
-			rawObject.month++
-
 			// reset the VCardTime component to the selected date/time
-			this.localValue = new VCardTime(rawObject, null, this.propType)
+			this.localValue = new VCardTime(dateObject, null, this.propType)
 
 			// https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
 			// Use moment to convert the JsDate to Object
 			this.$emit('update:value', this.localValue)
-		}, 500),
+		},
 
 		/**
 		 * Format time with locale to display only
@@ -211,6 +236,11 @@ export default {
 			let datetimeData = this.vcardTimeLocalValue.toJSON()
 			let datetime = ''
 
+			const ignoreYear = this.property.getParameter('x-apple-omit-year')
+			if (ignoreYear) {
+				datetimeData.year = null
+			}
+
 			// FUN FACT: JS date starts month at zero!
 			datetimeData.month--
 
diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue
index 4cdf296b3..c6e6c419c 100644
--- a/src/components/Properties/PropertyGroups.vue
+++ b/src/components/Properties/PropertyGroups.vue
@@ -21,7 +21,7 @@
   -->
 
 <template>
-	<div v-if="propModel" class="grid-span-2 property">
+	<div v-if="propModel" class="grid-span-2 property property--without-actions">
 		<!-- NO title if first element for groups -->
 
 		<div class="property__row">
diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue
index d14e83e47..84753871e 100644
--- a/src/components/Properties/PropertyMultipleText.vue
+++ b/src/components/Properties/PropertyMultipleText.vue
@@ -45,12 +45,14 @@
 				{{ isFirstProperty ? '' : propModel.readableName }}
 			</div>
 
-			<!-- show the first input if not -->
+			<!-- show the first input if not a structured value -->
 			<input v-if="!property.isStructuredValue" v-model.trim="localValue[0]" :readonly="isReadOnly"
 				class="property__value" type="text" @input="updateValue">
 
 			<!-- props actions -->
-			<PropertyActions :actions="actions" @delete="deleteProperty" />
+			<PropertyActions class="property__actions--floating"
+				:actions="actions" :property-component="this"
+				@delete="deleteProperty" />
 		</div>
 
 		<!-- force order based on model -->
@@ -82,7 +84,7 @@ import PropertyTitle from './PropertyTitle'
 import PropertyActions from './PropertyActions'
 
 export default {
-	name: 'PropertyText',
+	name: 'PropertyMultipleText',
 
 	components: {
 		PropertyTitle,
diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue
index 559aebc84..4b5893d34 100644
--- a/src/components/Properties/PropertySelect.vue
+++ b/src/components/Properties/PropertySelect.vue
@@ -37,12 +37,12 @@
 				{{ propModel.readableName }}
 			</div>
 
-			<!-- props 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"
 				label="name" @input="updateValue" />
+
+			<!-- props actions -->
+			<PropertyActions :actions="actions" :property-component="this" @delete="deleteProperty" />
 		</div>
 	</div>
 </template>
diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue
index 8ccaf9691..05ec9861b 100644
--- a/src/components/Properties/PropertyText.vue
+++ b/src/components/Properties/PropertyText.vue
@@ -60,7 +60,7 @@
 				target="_blank" />
 
 			<!-- props actions -->
-			<PropertyActions :actions="actions" @delete="deleteProperty" />
+			<PropertyActions :actions="actions" :property-component="this" @delete="deleteProperty" />
 		</div>
 	</div>
 </template>
diff --git a/src/components/Settings/SettingsAddressbookShare.vue b/src/components/Settings/SettingsAddressbookShare.vue
index a54e8d1f7..eb3c29ca0 100644
--- a/src/components/Settings/SettingsAddressbookShare.vue
+++ b/src/components/Settings/SettingsAddressbookShare.vue
@@ -52,7 +52,7 @@ import addressBookSharee from './SettingsAddressbookSharee'
 import debounce from 'debounce'
 
 export default {
-	name: 'SettingsShareAddressbook',
+	name: 'SettingsAddressbookShare',
 	components: {
 		addressBookSharee
 	},
diff --git a/src/components/Settings/SettingsAddressbookSharee.vue b/src/components/Settings/SettingsAddressbookSharee.vue
index 93a3ee71c..d7bea0244 100644
--- a/src/components/Settings/SettingsAddressbookSharee.vue
+++ b/src/components/Settings/SettingsAddressbookSharee.vue
@@ -53,7 +53,7 @@
 <script>
 
 export default {
-	name: 'SettingsShareSharee',
+	name: 'SettingsAddressbookSharee',
 
 	props: {
 		addressbook: {
diff --git a/src/main.js b/src/main.js
index 021ef9663..9f5addf8e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,3 +1,4 @@
+/* eslint-disable vue/match-component-file-name */
 /**
  * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  *
@@ -24,7 +25,7 @@ import 'core-js/stable'
 import 'regenerator-runtime/runtime'
 
 import Vue from 'vue'
-import App from './App'
+import App from './ContactsRoot'
 import router from './router'
 import store from './store'
 import { sync } from 'vuex-router-sync'
diff --git a/src/mixins/ActionsMixin.js b/src/mixins/ActionsMixin.js
new file mode 100644
index 000000000..829efc248
--- /dev/null
+++ b/src/mixins/ActionsMixin.js
@@ -0,0 +1,32 @@
+/**
+ * @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/>.
+ *
+ */
+
+export default {
+	props: {
+		// The current component root
+		component: {
+			type: Object,
+			default: () => {},
+			required: true
+		}
+	}
+}
diff --git a/src/mixins/PropertyMixin.js b/src/mixins/PropertyMixin.js
index 9fa3ed0a0..36c57bf5e 100644
--- a/src/mixins/PropertyMixin.js
+++ b/src/mixins/PropertyMixin.js
@@ -89,6 +89,9 @@ export default {
 	computed: {
 		actions() {
 			return this.propModel.actions ? this.propModel.actions : []
+		},
+		haveAction() {
+			return this.actions && this.actions.length > 0
 		}
 	},
 
diff --git a/src/models/rfcProps.js b/src/models/rfcProps.js
index ef8f4baca..16c766a22 100644
--- a/src/models/rfcProps.js
+++ b/src/models/rfcProps.js
@@ -20,18 +20,10 @@
  *
  */
 import { VCardTime } from 'ical.js'
+import ActionCopyNtoFN from '../components/Actions/ActionCopyNtoFN'
+import ActionToggleYear from '../components/Actions/ActionToggleYear'
 
-const copyNtoFN = ({ contact, updateContact }) => () => {
-	if (contact.vCard.hasProperty('n')) {
-		// Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.
-		// -> John Stevenson
-		const n = contact.vCard.getFirstPropertyValue('n')
-		contact.fullName = n.slice(0, 2).reverse().join(' ')
-		updateContact()
-	}
-}
-
-const properties = component => ({
+const properties = {
 	nickname: {
 		readableName: t('contacts', 'Nickname'),
 		icon: 'icon-user'
@@ -51,11 +43,7 @@ const properties = component => ({
 		},
 		icon: 'icon-user',
 		actions: [
-			{
-				text: t('contacts', 'Copy to full name'),
-				icon: 'icon-up',
-				action: copyNtoFN(component)
-			}
+			ActionCopyNtoFN
 		]
 	},
 	note: {
@@ -113,7 +101,10 @@ const properties = component => ({
 		force: 'date', // most ppl prefer date for birthdays, time is usually irrelevant
 		defaultValue: {
 			value: new VCardTime(null, null, 'date').fromJSDate(new Date())
-		}
+		},
+		actions: [
+			ActionToggleYear
+		]
 	},
 	anniversary: {
 		readableName: t('contacts', 'Anniversary'),
@@ -289,7 +280,7 @@ const properties = component => ({
 			{ id: 'U', name: t('contacts', 'Unknown') }
 		]
 	}
-})
+}
 
 const fieldOrder = [
 	'org',
diff --git a/src/services/checks/badGenderType.js b/src/services/checks/badGenderType.js
index a113ad8e4..6b6584eb0 100644
--- a/src/services/checks/badGenderType.js
+++ b/src/services/checks/badGenderType.js
@@ -33,7 +33,7 @@ export default {
 	fix: contact => {
 		const gender = contact.vCard.getFirstProperty('gender')
 		const type = gender.getFirstParameter('type')
-		const option = Object.values(rfcProps.properties({}).gender.options).find(opt => opt.id === type)
+		const option = Object.values(rfcProps.properties.gender.options).find(opt => opt.id === type)
 		if (option) {
 			gender.removeParameter('type')
 			gender.setValue(option.id)
diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js
index 85e23379f..2576311bc 100644
--- a/src/store/addressbooks.js
+++ b/src/store/addressbooks.js
@@ -355,6 +355,7 @@ const actions = {
 							contacts.push(contact)
 						} catch (error) {
 							// PARSING FAILED
+							console.error('Error reading contact', item.url, item.data)
 							console.error(error)
 							failed++
 						}
diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue
index d5c0deb2c..a6fc224fd 100644
--- a/src/views/Contacts.vue
+++ b/src/views/Contacts.vue
@@ -324,7 +324,7 @@ export default {
 			contact.rev = rev
 
 			// itterate over all properties (filter is not usable on objects and we need the key of the property)
-			const properties = rfcProps.properties(this)
+			const properties = rfcProps.properties
 			for (let name in properties) {
 				if (properties[name].default) {
 					let defaultData = properties[name].defaultValue