[hub][vue] Saved Products Page, remote item card

- also undo unfavouriting items
diff --git a/erpnext/public/js/hub/components/ItemCard.vue b/erpnext/public/js/hub/components/ItemCard.vue
index 4d72344..6e8b7ea 100644
--- a/erpnext/public/js/hub/components/ItemCard.vue
+++ b/erpnext/public/js/hub/components/ItemCard.vue
@@ -1,24 +1,24 @@
 <template>
-	<div class="col-md-3 col-sm-4 col-xs-6 hub-card-container">
-		<div class="hub-card is_local"
+	<div v-if="item.seen" class="col-md-3 col-sm-4 col-xs-6 hub-card-container">
+		<div class="hub-card"
 			@click="on_click(item_id)"
 		>
 			<div class="hub-card-header flex justify-between">
-				<div>
+				<div class="ellipsis" :style="{ width: '85%' }">
 					<div class="hub-card-title ellipsis bold">{{ title }}</div>
 					<div class="hub-card-subtitle ellipsis text-muted" v-html='subtitle'></div>
 				</div>
 				<i v-if="allow_clear"
 					class="octicon octicon-x text-extra-muted"
-					@click="$emit('remove-item', item_id)"
+					@click.stop="$emit('remove-item', item_id)"
 				>
 				</i>
 			</div>
 			<div class="hub-card-body">
 				<img class="hub-card-image" :src="item.image"/>
 				<div class="hub-card-overlay">
-					<div class="hub-card-overlay-body">
-						<div class="hub-card-overlay-button" style="right: 15px; bottom: 15px;">
+					<div v-if="is_local" class="hub-card-overlay-body">
+						<div class="hub-card-overlay-button">
 							<button class="btn btn-default zoom-view">
 								<i class="octicon octicon-pencil text-muted"></i>
 							</button>
@@ -34,13 +34,28 @@
 
 export default {
 	name: 'item-card',
-	props: ['item', 'item_id_fieldname', 'on_click', 'allow_clear'],
+	props: ['item', 'item_id_fieldname', 'is_local', 'on_click', 'allow_clear'],
 	computed: {
 		title() {
-			return this.item.item_name
+			const item_name = this.item.item_name || this.item.name;
+			return strip_html(item_name);
 		},
 		subtitle() {
-			return comment_when(this.item.creation);
+			const dot_spacer = '<span aria-hidden="true"> · </span>';
+			if(this.is_local){
+				return comment_when(this.item.creation);
+			} else {
+				let subtitle_items = [comment_when(this.item.creation)];
+				const rating = this.item.average_rating;
+
+				if (rating > 0) {
+					subtitle_items.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
+				}
+
+				subtitle_items.push(this.item.company);
+
+				return subtitle_items.join(dot_spacer);
+			}
 		},
 		item_id() {
 			return this.item[this.item_id_fieldname];
@@ -58,6 +73,7 @@
 		border: 1px solid @border-color;
 		border-radius: 4px;
 		overflow: hidden;
+		cursor: pointer;
 
 		&:hover .hub-card-overlay {
 			display: block;
diff --git a/erpnext/public/js/hub/components/ItemCardsContainer.vue b/erpnext/public/js/hub/components/ItemCardsContainer.vue
index 0e75795..d558175 100644
--- a/erpnext/public/js/hub/components/ItemCardsContainer.vue
+++ b/erpnext/public/js/hub/components/ItemCardsContainer.vue
@@ -12,6 +12,7 @@
 			:key="item[item_id_fieldname]"
 			:item="item"
 			:item_id_fieldname="item_id_fieldname"
+			:is_local="is_local"
 			:on_click="on_click"
 			:allow_clear="editable"
 			@remove-item="$emit('remove-item', item[item_id_fieldname])"
@@ -29,6 +30,7 @@
 	props: {
 		items: Array,
 		item_id_fieldname: String,
+		is_local: Boolean,
 		on_click: Function,
 		editable: Boolean,
 
diff --git a/erpnext/public/js/hub/components/PublishPage.vue b/erpnext/public/js/hub/components/PublishPage.vue
index 2068813..5178553 100644
--- a/erpnext/public/js/hub/components/PublishPage.vue
+++ b/erpnext/public/js/hub/components/PublishPage.vue
@@ -23,6 +23,7 @@
 		<item-cards-container
 			:items="selected_items"
 			:item_id_fieldname="item_id_fieldname"
+			:is_local="true"
 			:editable="true"
 			@remove-item="remove_item_from_selection"
 
@@ -44,6 +45,7 @@
 		<item-cards-container
 			:items="valid_items"
 			:item_id_fieldname="item_id_fieldname"
+			:is_local="true"
 			:on_click="show_publishing_dialog_for_item"
 		>
 		</item-cards-container>
diff --git a/erpnext/public/js/hub/components/SavedProductsPage.vue b/erpnext/public/js/hub/components/SavedProductsPage.vue
new file mode 100644
index 0000000..d10f550
--- /dev/null
+++ b/erpnext/public/js/hub/components/SavedProductsPage.vue
@@ -0,0 +1,120 @@
+<template>
+	<div
+		class="marketplace-page"
+		:data-page-name="page_name"
+	>
+		<h5>{{ page_title }}</h5>
+
+		<item-cards-container
+			:items="items"
+			:item_id_fieldname="item_id_fieldname"
+			:on_click="go_to_item_details_page"
+			:editable="true"
+			@remove-item="on_item_remove"
+			:empty_state_message="empty_state_message"
+		>
+		</item-cards-container>
+	</div>
+</template>
+
+<script>
+import ItemCardsContainer from './ItemCardsContainer.vue';
+
+export default {
+	name: 'saved-products-page',
+	data() {
+		return {
+			page_name: frappe.get_route()[1],
+			items: [],
+			all_items: [],
+			item_id_fieldname: 'hub_item_code',
+
+			// Constants
+			page_title: __('Saved Products'),
+			empty_state_message: __(`You haven't favourited any items yet.`)
+		};
+	},
+	components: {
+		ItemCardsContainer
+	},
+	created() {
+		this.get_items();
+	},
+	methods: {
+		get_items() {
+			hub.call(
+				'get_favourite_items_of_seller',
+				{
+					hub_seller: hub.settings.company_email
+				},
+				'action:item_favourite'
+			)
+			.then((items) => {
+				this.items = items.map(item => {
+					item.seen = true;
+					return item;
+				});
+			})
+		},
+
+		go_to_item_details_page(hub_item_code) {
+			frappe.set_route(`marketplace/item/${hub_item_code}`);
+		},
+
+		on_item_remove(hub_item_code) {
+			const grace_period = 5000;
+			let reverted = false;
+			let alert;
+
+			const undo_remove = () => {
+				this.toggle_item(hub_item_code);;
+				reverted = true;
+				alert.hide();
+				return false;
+			}
+
+			alert = frappe.show_alert(__(`<span>${hub_item_code} removed.
+				<a href="#" class="undo-remove" data-action="undo-remove"><b>Undo</b></a></span>`),
+				grace_period/1000,
+				{
+					'undo-remove': undo_remove.bind(this)
+				}
+			);
+
+			this.toggle_item(hub_item_code, false);
+
+			setTimeout(() => {
+				if(!reverted) {
+					this.remove_item_from_saved_products(hub_item_code);
+				}
+			}, grace_period);
+		},
+
+		remove_item_from_saved_products(hub_item_code) {
+			erpnext.hub.trigger('action:item_favourite');
+			hub.call('remove_item_from_seller_favourites', {
+				hub_item_code,
+				hub_seller: hub.settings.company_email
+			})
+			.then(() => {
+				this.get_items();
+			})
+			.catch(e => {
+				console.log(e);
+			});
+		},
+
+		// By default show
+		toggle_item(hub_item_code, show=true) {
+			this.items = this.items.map(item => {
+				if(item.hub_item_code === hub_item_code) {
+					item.seen = show;
+				}
+				return item;
+			});
+		}
+	}
+}
+</script>
+
+<style scoped></style>
diff --git a/erpnext/public/js/hub/components/item_card.js b/erpnext/public/js/hub/components/item_card.js
index e2546e6..a2c8553 100644
--- a/erpnext/public/js/hub/components/item_card.js
+++ b/erpnext/public/js/hub/components/item_card.js
@@ -2,6 +2,7 @@
 	if (item.recent_message) {
 		return get_item_message_card_html(item);
 	}
+
 	const item_name = item.item_name || item.name;
 	const title = strip_html(item_name);
 	const img_url = item.image;
@@ -14,7 +15,7 @@
 	if (rating > 0) {
 		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
 	}
-	
+
 	subtitle.push(company_name);
 
 	let dot_spacer = '<span aria-hidden="true"> · </span>';
@@ -51,61 +52,6 @@
 	return item_html;
 }
 
-function get_local_item_card_html(item) {
-	const item_name = item.item_name || item.name;
-	const title = strip_html(item_name);
-	const img_url = item.image;
-	const company_name = item.company;
-
-	const is_active = item.publish_in_hub;
-	const id = item.hub_item_code || item.item_code;
-
-	// Subtitle
-	let subtitle = [comment_when(item.creation)];
-	const rating = item.average_rating;
-
-	if (rating > 0) {
-		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
-	}
-
-	if (company_name) {
-		subtitle.push(company_name);
-	}
-
-	let dot_spacer = '<span aria-hidden="true"> · </span>';
-	subtitle = subtitle.join(dot_spacer);
-
-	const edit_item_button = `<div class="hub-card-overlay-button" style="right: 15px; bottom: 15px;" data-route="Form/Item/${item.item_name}">
-		<button class="btn btn-default zoom-view">
-			<i class="octicon octicon-pencil text-muted"></i>
-		</button>
-	</div>`;
-
-	const item_html = `
-		<div class="col-md-3 col-sm-4 col-xs-6 hub-card-container">
-			<div class="hub-card is-local ${is_active ? 'active' : ''}" data-id="${id}">
-				<div class="hub-card-header flex">
-					<div class="ellipsis">
-						<div class="hub-card-title ellipsis bold">${title}</div>
-						<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
-					</div>
-					<i class="octicon octicon-check text-success"></i>
-				</div>
-				<div class="hub-card-body">
-					<img class="hub-card-image" src="${img_url}" />
-					<div class="hub-card-overlay">
-						<div class="hub-card-overlay-body">
-							${edit_item_button}
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	`;
-
-	return item_html;
-}
-
 function get_item_message_card_html(item) {
 	const item_name = item.item_name || item.name;
 	const title = strip_html(item_name);
@@ -138,6 +84,5 @@
 }
 
 export {
-	get_item_card_html,
-	get_local_item_card_html
+	get_item_card_html
 }
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 42b2dda..803babb 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -80,8 +80,8 @@
 		$nav_group.empty();
 
 		const user_specific_items_html = this.registered
-			? `<li class="hub-sidebar-item" data-route="marketplace/favourites">
-					${__('Favorites')}
+			? `<li class="hub-sidebar-item" data-route="marketplace/saved-products">
+					${__('Saved Products')}
 				</li>
 				<li class="hub-sidebar-item text-muted" data-route="marketplace/profile">
 					${__('Your Profile')}
@@ -196,8 +196,8 @@
 		}
 
 		// registered seller routes
-		if (route[1] === 'favourites' && !this.subpages.favourites) {
-			this.subpages.favourites = new erpnext.hub.Favourites(this.$body);
+		if (route[1] === 'saved-products' && !this.subpages['saved-products']) {
+			this.subpages['saved-products'] = new erpnext.hub.SavedProducts(this.$body);
 		}
 
 		if (route[1] === 'profile' && !this.subpages.profile) {
@@ -221,7 +221,7 @@
 		}
 
 		// dont allow unregistered users to access registered routes
-		const registered_routes = ['favourites', 'profile', 'publish', 'my-products', 'messages'];
+		const registered_routes = ['saved-products', 'profile', 'publish', 'my-products', 'messages'];
 		if (!hub.settings.registered && registered_routes.includes(route[1])) {
 			frappe.set_route('marketplace', 'home');
 			return;
diff --git a/erpnext/public/js/hub/pages/favourites.js b/erpnext/public/js/hub/pages/favourites.js
index ae08a68..547f815 100644
--- a/erpnext/public/js/hub/pages/favourites.js
+++ b/erpnext/public/js/hub/pages/favourites.js
@@ -1,82 +1,20 @@
-import SubPage from './subpage';
-import { get_item_card_container_html } from '../components/items_container';
+import SavedProductsPage from '../components/SavedProductsPage.vue';
+import Vue from 'vue/dist/vue.js';
 
-erpnext.hub.Favourites = class Favourites extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-		this.bind_events();
+erpnext.hub.SavedProducts = class {
+	constructor(parent) {
+		this.$wrapper = $(`<div id="vue-area-saved">`).appendTo($(parent));
+
+		new Vue({
+			render: h => h(SavedProductsPage)
+		}).$mount('#vue-area-saved');
 	}
 
-	bind_events() {
-		this.$wrapper.on('click', '.hub-card', (e) => {
-			const $target = $(e.target);
-			if($target.hasClass('octicon-x')) {
-				e.stopPropagation();
-				const hub_item_code = $target.attr('data-hub-item-code');
-				this.on_item_remove(hub_item_code);
-			}
-		});
+	show() {
+		$('[data-page-name="saved-products"]').show();
 	}
 
-	refresh() {
-		this.get_favourites()
-			.then(items => {
-				this.render(items);
-			});
+	hide() {
+		$('[data-page-name="saved-products"]').hide();
 	}
-
-	get_favourites() {
-		return hub.call('get_favourite_items_of_seller', {
-			hub_seller: hub.settings.company_email
-		}, 'action:item_favourite');
-	}
-
-	render(items) {
-		this.$wrapper.find('.hub-items-container').empty();
-		const html = get_item_card_container_html(items, __('Favourites'));
-		this.$wrapper.html(html);
-		this.$wrapper.find('.hub-card').addClass('closable');
-
-		if (!items.length) {
-			this.render_empty_state();
-		}
-	}
-
-	render_empty_state() {
-		this.$wrapper.find('.hub-items-container').append(`
-			<div class="col-md-12">${__("You don't have any favourites yet.")}</div>
-		`)
-	}
-
-	on_item_remove(hub_item_code, $hub_card = '') {
-		const $message = $(__(`<span>${hub_item_code} removed.
-			<a href="#" data-action="undo-remove"><b>Undo</b></a></span>`));
-
-		frappe.show_alert($message);
-
-		$hub_card = this.$wrapper.find(`.hub-card[data-hub-item-code="${hub_item_code}"]`);
-
-		$hub_card.hide();
-
-		const grace_period = 5000;
-
-		setTimeout(() => {
-			this.remove_item(hub_item_code, $hub_card);
-		}, grace_period);
-	}
-
-	remove_item(hub_item_code, $hub_card) {
-		hub.call('remove_item_from_seller_favourites', {
-			hub_item_code,
-			hub_seller: hub.settings.company_email
-		})
-		.then(() => {
-			$hub_card.remove();
-		})
-		.catch(e => {
-			console.log(e);
-		});
-	}
-
-	undo_remove(hub_item_code) { }
 }
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index bcf57f4..720a9d8 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -44,18 +44,6 @@
 		&:hover .hub-card-overlay {
 			display: block;
 		}
-
-		.octicon-x {
-			display: none;
-			margin-left: 10px;
-			font-size: 20px;
-		}
-	}
-
-	.hub-card.closable {
-		.octicon-x {
-			display: block;
-		}
 	}
 
 	.hub-card.is-local {