[feat] new component: ItemPublishDialog
diff --git a/erpnext/public/js/hub/components/item_publish_dialog.js b/erpnext/public/js/hub/components/item_publish_dialog.js
new file mode 100644
index 0000000..f751be0
--- /dev/null
+++ b/erpnext/public/js/hub/components/item_publish_dialog.js
@@ -0,0 +1,39 @@
+function ItemPublishDialog(primary_action, secondary_action) {
+    let dialog = new frappe.ui.Dialog({
+        title: __('Edit Publishing Details'),
+        fields: [
+            {
+                "label": "Item Code",
+                "fieldname": "item_code",
+                "fieldtype": "Data",
+                "read_only": 1
+            },
+            {
+                "label": "Hub Category",
+                "fieldname": "hub_category",
+                "fieldtype": "Autocomplete",
+                "options": ["Agriculture", "Books", "Chemicals", "Clothing",
+                    "Electrical", "Electronics", "Energy", "Fashion", "Food and Beverage",
+                    "Health", "Home", "Industrial", "Machinery", "Packaging and Printing",
+                    "Sports", "Transportation"
+                ],
+                "reqd": 1
+            },
+            {
+                "label": "Images",
+                "fieldname": "image_list",
+                "fieldtype": "MultiSelect",
+                "options": [],
+                "reqd": 1
+            }
+        ],
+        primary_action_label: primary_action.label || __('Set Details'),
+        primary_action: primary_action.fn,
+        secondary_action: secondary_action.fn
+    });
+    return dialog;
+}
+
+export {
+    ItemPublishDialog
+}
diff --git a/erpnext/public/js/hub/components/publishing_area.js b/erpnext/public/js/hub/components/publishing_area.js
new file mode 100644
index 0000000..4775c3b
--- /dev/null
+++ b/erpnext/public/js/hub/components/publishing_area.js
@@ -0,0 +1,36 @@
+function get_publishing_header() {
+    const title_html = `<h5>${__('Select Products to Publish')}</h5>`;
+
+    const subtitle_html = `<p class="text-muted">
+        ${__(`Only products with an image, description and category can be published.
+        Please update them if an item in your inventory does not appear.`)}
+    </p>`;
+
+    const publish_button_html = `<button class="btn btn-primary btn-sm publish-items" disabled>
+        <i class="visible-xs octicon octicon-check"></i>
+        <span class="hidden-xs">${__('Publish')}</span>
+    </button>`;
+
+    return $(`
+        <div class="publish-area empty">
+            <div class="flex justify-between align-flex-end">
+                ${title_html}
+                ${publish_button_html}
+            </div>
+            <div class="empty-items-container flex align-center flex-column justify-center">
+                <p class="text-muted">${__('No Items Selected')}</p>
+            </div>
+            <div class="row hub-items-container selected-items"></div>
+        </div>
+
+        <div class='subpage-title flex'>
+            <div>
+                ${subtitle_html}
+            </div>
+        </div>
+    `);
+}
+
+export {
+    get_publishing_header
+}
diff --git a/erpnext/public/js/hub/pages/publish.js b/erpnext/public/js/hub/pages/publish.js
index 8de7b38..98b0a61 100644
--- a/erpnext/public/js/hub/pages/publish.js
+++ b/erpnext/public/js/hub/pages/publish.js
@@ -2,12 +2,13 @@
 import { get_item_card_container_html } from '../components/items_container';
 import { get_local_item_card_html } from '../components/item_card';
 import { make_search_bar } from '../components/search_bar';
-
+import { get_publishing_header } from '../components/publishing_area';
+import { ItemPublishDialog } from '../components/item_publish_dialog';
 
 erpnext.hub.Publish = class Publish extends SubPage {
 	make_wrapper() {
 		super.make_wrapper();
-		this.items_to_publish = {};
+		this.items_data_to_publish = {};
 		this.unpublished_items = [];
 		this.fetched_items = [];
 		this.fetched_items_dict = {};
@@ -41,7 +42,7 @@
 
 	make_publish_ready_state() {
 		this.$wrapper.empty();
-		this.$wrapper.append(this.get_publishing_header());
+		this.$wrapper.append(get_publishing_header());
 
 		make_search_bar({
 			wrapper: this.$wrapper,
@@ -53,46 +54,15 @@
 		});
 
 		this.setup_publishing_events();
+		this.show_last_sync_message();
+		this.get_items_and_render();
+	}
 
+	show_last_sync_message() {
 		if(hub.settings.last_sync_datetime) {
 			this.show_message(`Last sync was <a href="#marketplace/profile">${comment_when(hub.settings.last_sync_datetime)}</a>.
 				<a href="#marketplace/my-products">See your Published Products</a>.`);
 		}
-
-		this.get_items_and_render();
-	}
-
-	get_publishing_header() {
-		const title_html = `<h5>${__('Select Products to Publish')}</h5>`;
-
-		const subtitle_html = `<p class="text-muted">
-			${__(`Only products with an image, description and category can be published.
-			Please update them if an item in your inventory does not appear.`)}
-		</p>`;
-
-		const publish_button_html = `<button class="btn btn-primary btn-sm publish-items" disabled>
-			<i class="visible-xs octicon octicon-check"></i>
-			<span class="hidden-xs">${__('Publish')}</span>
-		</button>`;
-
-		return $(`
-			<div class="publish-area empty">
-				<div class="flex justify-between align-flex-end">
-					${title_html}
-					${publish_button_html}
-				</div>
-				<div class="empty-items-container flex align-center flex-column justify-center">
-					<p class="text-muted">${__('No Items Selected')}</p>
-				</div>
-				<div class="row hub-items-container selected-items"></div>
-			</div>
-
-			<div class='subpage-title flex'>
-				<div>
-					${subtitle_html}
-				</div>
-			</div>
-		`);
 	}
 
 	setup_publishing_events() {
@@ -118,69 +88,54 @@
 	}
 
 	make_publishing_dialog() {
-		this.publishing_dialog = new frappe.ui.Dialog({
-			title: __('Edit Publishing Details'),
-			fields: [
-				{
-					"label": "Item Code",
-					"fieldname": "item_code",
-					"fieldtype": "Data",
-					"read_only": 1
-				},
-				{
-					"label": "Hub Category",
-					"fieldname": "hub_category",
-					"fieldtype": "Autocomplete",
-					"options": ["Agriculture", "Books", "Chemicals", "Clothing",
-						"Electrical", "Electronics", "Energy", "Fashion", "Food and Beverage",
-						"Health", "Home", "Industrial", "Machinery", "Packaging and Printing",
-						"Sports", "Transportation"
-					],
-					"reqd": 1
-				},
-				{
-					"label": "Images",
-					"fieldname": "image_list",
-					"fieldtype": "MultiSelect",
-					"options": [],
-					"reqd": 1
+		this.item_publish_dialog = ItemPublishDialog(
+			{
+				fn: (values) => {
+					this.add_item_to_publish(values);
+					this.item_publish_dialog.hide();
 				}
-			],
-			primary_action_label: __('Set Details'),
-			primary_action: () => {
-				const values = this.publishing_dialog.get_values(true);
-				this.items_to_publish[values.item_code] = values;
-
-				this.$current_selected_card.appendTo(this.selected_items_container);
-				this.$current_selected_card.find('.hub-card').toggleClass('active');
-
-				this.update_selected_items_count();
-
-				this.publishing_dialog.hide();
 			},
-			secondary_action: () => {
-				const values = this.publishing_dialog.get_values(true);
-				this.items_to_publish[values.item_code] = values;
+			{
+				fn: () => {
+					const values = this.item_publish_dialog.get_values(true);
+					this.update_items_data_to_publish(values);
+				}
 			}
-		});
+		);
+	}
+
+	add_item_to_publish(values) {
+		this.update_items_data_to_publish(values);
+		this.select_current_card()
+	}
+
+	update_items_data_to_publish(values) {
+		this.items_data_to_publish[values.item_code] = values;
+	}
+
+	select_current_card() {
+		this.$current_selected_card.appendTo(this.selected_items_container);
+		this.$current_selected_card.find('.hub-card').toggleClass('active');
+
+		this.update_selected_items_count();
 	}
 
 	show_publishing_dialog_for_item(item_code) {
-		let item_data = this.items_to_publish[item_code];
+		let item_data = this.items_data_to_publish[item_code];
 
 		if(!item_data) { item_data = { item_code }; };
 
-		this.publishing_dialog.clear();
+		this.item_publish_dialog.clear();
 
 		const item_doc = this.fetched_items_dict[item_code];
 		if(item_doc) {
-			this.publishing_dialog.fields_dict.image_list.set_data(
+			this.item_publish_dialog.fields_dict.image_list.set_data(
 				item_doc.attachments.map(attachment => attachment.file_url)
 			);
 		}
 
-		this.publishing_dialog.set_values(item_data);
-		this.publishing_dialog.show();
+		this.item_publish_dialog.set_values(item_data);
+		this.item_publish_dialog.show();
 	}
 
 	update_selected_items_count() {
@@ -204,14 +159,6 @@
 		this.$wrapper.find('.publish-area').toggleClass('filled', !is_empty);
 	}
 
-	add_item_to_publish() {
-		//
-	}
-
-	remove_item_from_publish() {
-		//
-	}
-
 	make_publish_in_progress_state() {
 		this.$wrapper.empty();
 
@@ -238,8 +185,8 @@
 	}
 
 	show_publish_progress() {
-		const items_to_publish = this.items_to_publish.length
-			? this.items_to_publish
+		const items_to_publish = this.items_data_to_publish.length
+			? this.items_data_to_publish
 			: JSON.parse(hub.settings.custom_data);
 
 		const $publish_progress = $(`<div class="sync-progress">
@@ -302,17 +249,7 @@
 			item_codes_to_publish.push($(this).attr("data-id"));
 		});
 
-		// this.unpublished_items = this.fetched_items.filter(item => {
-		// 	return !item_codes_to_publish.includes(item.item_code);
-		// });
-
-		// const items_to_publish = this.fetched_items.filter(item => {
-		// 	return item_codes_to_publish.includes(item.item_code);
-		// });
-
-		// this.items_to_publish = items_to_publish;
-
-		const items_data_to_publish = item_codes_to_publish.map(item_code => this.items_to_publish[item_code])
+		const items_data_to_publish = item_codes_to_publish.map(item_code => this.items_data_to_publish[item_code])
 
 		return frappe.call(
 			'erpnext.hub_node.api.publish_selected_items',