[hub][vue] add empty state and dynamic publish button
diff --git a/erpnext/public/js/hub/components/EmptyState.vue b/erpnext/public/js/hub/components/EmptyState.vue
new file mode 100644
index 0000000..3e0cb0e
--- /dev/null
+++ b/erpnext/public/js/hub/components/EmptyState.vue
@@ -0,0 +1,23 @@
+<template>
+	<div class="empty-state flex align-center flex-column justify-center"
+		:class="{ bordered: bordered }"
+		:style="{ height: height + 'px' }"
+	>
+		<p class="text-muted">{{ message }}</p>
+	</div>
+</template>
+
+<script>
+
+export default {
+	name: 'empty-state',
+	props: {
+		message: String,
+		bordered: Boolean,
+		height: Number,
+		// action: Function
+	}
+}
+</script>
+
+<style scoped></style>
diff --git a/erpnext/public/js/hub/components/ItemCardsContainer.vue b/erpnext/public/js/hub/components/ItemCardsContainer.vue
index 4ccb9dc..01fd01e 100644
--- a/erpnext/public/js/hub/components/ItemCardsContainer.vue
+++ b/erpnext/public/js/hub/components/ItemCardsContainer.vue
@@ -1,5 +1,12 @@
 <template>
 	<div>
+		<empty-state
+			v-if="items.length === 0"
+			:message="empty_state_message"
+			:bordered="true"
+			:height="80"
+		>
+		</empty-state>
 		<item-card
 			v-for="item in items"
 			:key="item[item_id]"
@@ -12,12 +19,21 @@
 
 <script>
 import ItemCard from './ItemCard.vue';
+import EmptyState from './EmptyState.vue';
 
 export default {
 	name: 'item-cards-container',
-	props: ['items', 'is_local'],
+	props: {
+		'items': Array,
+		'is_local': Boolean,
+
+		'empty_state_message': String,
+		'empty_state_height': Number,
+		'empty_state_bordered': Boolean
+	},
 	components: {
-		ItemCard
+		ItemCard,
+		EmptyState
 	},
 	computed: {
 		item_id() {
diff --git a/erpnext/public/js/hub/components/NotificationMessage.vue b/erpnext/public/js/hub/components/NotificationMessage.vue
new file mode 100644
index 0000000..c77f15e
--- /dev/null
+++ b/erpnext/public/js/hub/components/NotificationMessage.vue
@@ -0,0 +1,20 @@
+<template>
+	<div class="subpage-message">
+        <p class="text-muted flex">
+            <span v-html="message"></span>
+            <i class="octicon octicon-x text-extra-muted"></i>
+        </p>
+    </div>
+</template>
+
+<script>
+
+export default {
+	name: ' notification-message',
+	props: {
+		message: String,
+	}
+}
+</script>
+
+<style scoped></style>
diff --git a/erpnext/public/js/hub/components/PublishPage.vue b/erpnext/public/js/hub/components/PublishPage.vue
index 2b2956f..670f010 100644
--- a/erpnext/public/js/hub/components/PublishPage.vue
+++ b/erpnext/public/js/hub/components/PublishPage.vue
@@ -3,15 +3,36 @@
 		class="marketplace-page"
 		:data-page-name="page_name"
 	>
+		<div class="flex justify-between align-flex-end">
+			<h5>{{ page_title }}</h5>
+
+			<button class="btn btn-primary btn-sm publish-items"
+				:disabled="no_selected_items">
+				<span>{{ publish_button_text }}</span>
+			</button>
+		</div>
+
+		<item-cards-container
+			:items="selected_items"
+			:is_local="true"
+			:empty_state_message="empty_state_message"
+			:empty_state_bordered="true"
+			:empty_state_height="80"
+		>
+		</item-cards-container>
+
+		<p class="text-muted">{{ valid_products_instruction }}</p>
+
 		<search-input
-			placeholder="Search Items ..."
+			:placeholder="search_placeholder"
 			:on_search="get_valid_items"
 			v-model="search_value"
 		>
 		</search-input>
+
 		<item-cards-container
 			:items="valid_items"
-			:is_local="1"
+			:is_local="true"
 		>
 		</item-cards-container>
 	</div>
@@ -27,13 +48,40 @@
 		return {
 			page_name: frappe.get_route()[1],
 			valid_items: [],
-			search_value: ''
+			selected_items: [],
+			search_value: '',
+
+			// Constants
+			page_title: __('Publish Products'),
+			search_placeholder: __('Search Items ...'),
+			empty_state_message: __(`No Items selected yet.
+				Browse and click on products below to publish.`),
+			valid_products_instruction: __(`Only products with an image, description
+				and category can be published. Please update them if an item in your
+				inventory does not appear.`)
 		};
 	},
 	components: {
 		SearchInput,
 		ItemCardsContainer
 	},
+	computed: {
+		no_selected_items() {
+			return this.selected_items.length === 0;
+		},
+
+		publish_button_text() {
+			const number = this.selected_items.length;
+			let text = 'Publish';
+			if(number === 1) {
+				text = 'Publish 1 Product';
+			}
+			if(number > 1) {
+				text = `Publish ${number} Products`;
+			}
+			return __(text);
+		}
+	},
 	created() {
 		this.get_valid_items();
 	},
diff --git a/erpnext/public/js/hub/pages/publish.js b/erpnext/public/js/hub/pages/publish.js
index 7b20907..78ca58d 100644
--- a/erpnext/public/js/hub/pages/publish.js
+++ b/erpnext/public/js/hub/pages/publish.js
@@ -4,6 +4,8 @@
 import { make_search_bar } from '../components/search_bar';
 import { get_publishing_header } from '../components/publishing_area';
 import { ItemPublishDialog } from '../components/item_publish_dialog';
+
+
 import PublishPage from '../components/PublishPage.vue';
 
 erpnext.hub.Publish = class Publish {
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index 5f497d9..71e9534 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -213,14 +213,13 @@
 
 	.empty-state {
 		height: 500px;
+		margin: 15px 0px;
 	}
 
-	.empty-items-container {
-		height: 80px;
+	.empty-state.bordered {
 		border-radius: 4px;
 		border: 1px solid @border-color;
 		border-style: dashed;
-		margin: 15px 0px;
 	}
 
 	.publish-area.filled {