Add marketplace back to hub_listing
diff --git a/erpnext/public/js/hub/hub_listing.js b/erpnext/public/js/hub/hub_listing.js
index 368c723..7a82a91 100644
--- a/erpnext/public/js/hub/hub_listing.js
+++ b/erpnext/public/js/hub/hub_listing.js
@@ -1,3 +1,848 @@
+frappe.provide('hub');
+frappe.provide('erpnext.hub');
+
+erpnext.hub.Marketplace = class Marketplace {
+ constructor({ parent }) {
+ this.$parent = $(parent);
+ this.page = parent.page;
+
+ frappe.db.get_doc('Hub Settings')
+ .then(doc => {
+ this.hub_settings = doc;
+ this.registered = doc.registered;
+
+ this.setup_header();
+ this.make_sidebar();
+ this.make_body();
+ this.setup_events();
+ this.refresh();
+ });
+ }
+
+ setup_header() {
+ this.page.set_title(__('Marketplace'));
+ }
+
+ setup_events() {
+ this.$parent.on('click', '[data-route]', (e) => {
+ const $target = $(e.currentTarget);
+ const route = $target.data().route;
+ frappe.set_route(route);
+
+ e.stopPropagation();
+ });
+ }
+
+ make_sidebar() {
+ this.$sidebar = this.$parent.find('.layout-side-section').addClass('hidden-xs');
+
+ this.make_sidebar_nav_buttons();
+ this.make_sidebar_categories();
+ }
+
+ make_sidebar_nav_buttons() {
+ let $nav_group = this.$sidebar.find('[data-nav-buttons]');
+ if (!$nav_group.length) {
+ $nav_group = $('<ul class="list-unstyled hub-sidebar-group" data-nav-buttons>').appendTo(this.$sidebar);
+ }
+ $nav_group.empty();
+
+ const user_specific_items_html = this.registered
+ ? `<li class="hub-sidebar-item text-muted" data-route="marketplace/profile">
+ ${__('Your Profile')}
+ </li>
+ <li class="hub-sidebar-item text-muted" data-route="marketplace/publish">
+ ${__('Publish Products')}
+ </li>`
+
+ : `<li class="hub-sidebar-item text-muted" data-route="marketplace/register">
+ ${__('Become a seller')}
+ </li>`;
+
+ $nav_group.append(`
+ <li class="hub-sidebar-item" data-route="marketplace/home">
+ ${__('Browse')}
+ </li>
+ <li class="hub-sidebar-item" data-route="marketplace/favourites">
+ ${__('Favorites')}
+ </li>
+ ${user_specific_items_html}
+ `);
+ }
+
+ make_sidebar_categories() {
+ frappe.call('erpnext.hub_node.get_categories')
+ .then(r => {
+ const categories = r.message.map(d => d.value).sort();
+ const sidebar_items = [
+ `<li class="hub-sidebar-item bold is-title">
+ ${__('Category')}
+ </li>`,
+ `<li class="hub-sidebar-item active" data-route="marketplace/home">
+ ${__('All')}
+ </li>`,
+ ...(this.registered
+ ? [`<li class="hub-sidebar-item active" data-route="marketplace/my-products">
+ ${__('Your Products')}
+ </li>`]
+ : []),
+ ...categories.map(category => `
+ <li class="hub-sidebar-item text-muted" data-route="marketplace/category/${category}">
+ ${__(category)}
+ </li>
+ `)
+ ];
+
+ this.$sidebar.append(`
+ <ul class="list-unstyled">
+ ${sidebar_items.join('')}
+ </ul>
+ `);
+
+ this.update_sidebar();
+ });
+ }
+
+ make_body() {
+ this.$body = this.$parent.find('.layout-main-section');
+
+ this.$body.on('seller-registered', () => {
+ this.registered = 1;
+ this.make_sidebar_nav_buttons();
+ });
+ }
+
+ update_sidebar() {
+ const route = frappe.get_route_str();
+ const $sidebar_item = this.$sidebar.find(`[data-route="${route}"]`);
+
+ const $siblings = this.$sidebar.find('[data-route]');
+ $siblings.removeClass('active').addClass('text-muted');
+
+ $sidebar_item.addClass('active').removeClass('text-muted');
+ }
+
+ refresh() {
+ const route = frappe.get_route();
+ this.subpages = this.subpages || {};
+
+ for (let page in this.subpages) {
+ this.subpages[page].hide();
+ }
+
+ if (route[1] === 'home' && !this.subpages.home) {
+ this.subpages.home = new erpnext.hub.Home(this.$body);
+ }
+
+ if (route[1] === 'favourites' && !this.subpages.favourites) {
+ this.subpages.favourites = new erpnext.hub.Favourites(this.$body);
+ }
+
+ if (route[1] === 'category' && route[2] && !this.subpages.category) {
+ this.subpages.category = new erpnext.hub.Category(this.$body);
+ }
+
+ if (route[1] === 'item' && route[2] && !this.subpages.item) {
+ this.subpages.item = new erpnext.hub.Item(this.$body);
+ }
+
+ if (route[1] === 'register' && !this.subpages.register) {
+ // if (this.registered) {
+ // frappe.set_route('marketplace', 'home');
+ // return;
+ // }
+ this.subpages.register = new erpnext.hub.Register(this.$body);
+ }
+
+ if (route[1] === 'publish' && !this.subpages.publish) {
+ this.subpages.publish = new erpnext.hub.Publish(this.$body);
+ }
+
+
+ if (!Object.keys(this.subpages).includes(route[1])) {
+ frappe.show_not_found();
+ return;
+ }
+
+ this.update_sidebar();
+ frappe.utils.scroll_to(0);
+ this.subpages[route[1]].show();
+ }
+}
+
+class SubPage {
+ constructor(parent) {
+ this.$parent = $(parent);
+ this.make_wrapper();
+ }
+
+ make_wrapper() {
+ const page_name = frappe.get_route()[1];
+ this.$wrapper = $(`<div class="marketplace-page" data-page-name="${page_name}">`).appendTo(this.$parent);
+ this.hide();
+ }
+
+ show() {
+ this.refresh();
+ this.$wrapper.show();
+ }
+
+ hide() {
+ this.$wrapper.hide();
+ }
+}
+
+erpnext.hub.Home = class Home extends SubPage {
+ make_wrapper() {
+ super.make_wrapper();
+ this.make_search_bar();
+ }
+
+ refresh() {
+ this.get_items_and_render();
+ }
+
+ get_items_and_render() {
+ this.$wrapper.find('.hub-card-container').empty();
+ this.get_items()
+ .then(items => {
+ this.render(items);
+ });
+ }
+
+ get_items() {
+ return hub.call('get_data_for_homepage');
+ }
+
+ make_search_bar() {
+ const $search = $(`
+ <div class="hub-search-container">
+ <input type="text" class="form-control" placeholder="Search for anything">
+ </div>`
+ );
+ this.$wrapper.append($search);
+ const $search_input = $search.find('input');
+
+ $search_input.on('keydown', frappe.utils.debounce((e) => {
+ if (e.which === frappe.ui.keyCode.ENTER) {
+ this.search_value = $search_input.val();
+ this.get_items_and_render();
+ }
+ }, 300));
+ }
+
+ render(items) {
+ const html = get_item_card_container_html(items, __('Recently Published'));
+ this.$wrapper.append(html)
+ }
+}
+
+erpnext.hub.Favourites = class Favourites extends SubPage {
+ refresh() {
+ this.get_favourites()
+ .then(r => {
+ this.render(r.message);
+ });
+ }
+
+ get_favourites() {
+ return frappe.call('erpnext.hub_node.get_item_favourites');
+ }
+
+ render(items) {
+ this.$wrapper.find('.hub-card-container').empty();
+ const html = get_item_card_container_html(items, __('Favourites'));
+ this.$wrapper.append(html)
+ }
+}
+
+erpnext.hub.Category = class Category extends SubPage {
+ refresh() {
+ this.category = frappe.get_route()[2];
+ this.get_items_for_category(this.category)
+ .then(r => {
+ this.render(r.message);
+ });
+ }
+
+ get_items_for_category(category) {
+ this.$wrapper.find('.hub-card-container').empty();
+ return frappe.call('erpnext.hub_node.get_list', {
+ doctype: 'Hub Item',
+ filters: {
+ hub_category: category
+ }
+ });
+ }
+
+ render(items) {
+ const html = get_item_card_container_html(items, __(this.category));
+ this.$wrapper.append(html)
+ }
+}
+
+erpnext.hub.Item = class Item extends SubPage {
+ refresh() {
+ this.hub_item_code = frappe.get_route()[2];
+
+ this.get_item(this.hub_item_code)
+ .then(item => {
+ this.render(item);
+ });
+ }
+
+ get_item(hub_item_code) {
+ return new Promise(resolve => {
+ const item = (erpnext.hub.hub_item_cache || []).find(item => item.name === hub_item_code)
+
+ if (item) {
+ resolve(item);
+ } else {
+ frappe.call('erpnext.hub_node.get_list', {
+ doctype: 'Hub Item',
+ filters: {
+ name: hub_item_code
+ }
+ })
+ .then(r => {
+ resolve(r.message[0]);
+ });
+ }
+ });
+ }
+
+ render(item) {
+ const title = item.item_name || item.name;
+ const company = item.company_name;
+
+ const who = __('Posted By {0}', [company]);
+ const when = comment_when(item.creation);
+
+ const city = item.seller_city ? item.seller_city + ', ' : '';
+ const country = item.country ? item.country : '';
+ const where = `${city}${country}`;
+
+ const dot_spacer = '<span aria-hidden="true"> · </span>';
+
+ const description = item.description || '';
+
+ const rating_html = get_rating_html(item);
+ const rating_count = item.reviews.length > 0 ? `(${item.reviews.length} reviews)` : '';
+
+ const html = `
+ <div class="hub-item-container">
+ <div class="row visible-xs">
+ <div class="col-xs-12 margin-bottom">
+ <button class="btn btn-xs btn-default" data-route="marketplace/home">Back to home</button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">
+ <div class="hub-item-image">
+ <img src="${item.image}">
+ </div>
+ </div>
+ <div class="col-md-6">
+ <h2>${title}</h2>
+ <div class="text-muted">
+ <p>${where}${dot_spacer}${when}</p>
+ <p>${rating_html}${rating_count}</p>
+ </div>
+ <hr>
+ <div class="hub-item-description">
+ ${description ?
+ `<b>${__('Description')}</b>
+ <p>${description}</p>
+ ` : __('No description')
+ }
+ </div>
+ </div>
+ </div>
+ <div class="row hub-item-seller">
+ <div class="col-md-12 margin-top margin-bottom">
+ <b class="text-muted">Seller Information</b>
+ </div>
+ <div class="col-md-1">
+ <img src="https://picsum.photos/200">
+ </div>
+ <div class="col-md-6">
+ <a href="#marketplace/seller/${company}" class="bold">${company}</a>
+ <p class="text-muted">
+ Contact Seller
+ </p>
+ </div>
+ </div>
+ <!-- review area -->
+ <div class="row hub-item-review-container">
+ <div class="col-md-12 form-footer">
+ <div class="form-comments">
+ <div class="timeline">
+ <div class="timeline-head"></div>
+ <div class="timeline-items"></div>
+ </div>
+ </div>
+ <div class="pull-right scroll-to-top">
+ <a onclick="frappe.utils.scroll_to(0)"><i class="fa fa-chevron-up text-muted"></i></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+
+ this.$wrapper.html(html);
+
+ this.make_review_area();
+ this.render_reviews(item);
+ }
+
+ make_review_area() {
+ this.comment_area = new frappe.ui.ReviewArea({
+ parent: this.$wrapper.find('.timeline-head').empty(),
+ mentions: [],
+ on_submit: (val) => {
+ val.user = frappe.session.user;
+ val.username = frappe.session.user_fullname;
+
+ frappe.call({
+ method: 'erpnext.hub_node.send_review',
+ args: {
+ hub_item_code: this.hub_item_code,
+ review: val
+ },
+ callback: (r) => {
+ console.log(r);
+ this.render_reviews(r.message);
+ this.comment_area.reset();
+ },
+ freeze: true
+ });
+ }
+ });
+ }
+
+ render_reviews(item) {
+ this.$wrapper.find('.timeline-items').empty();
+ item.reviews.forEach(review => this.render_review(review, item));
+ }
+
+ render_review(review, item) {
+ let username = review.username || review.user || __("Anonymous");
+
+ let image_html = review.user_image
+ ? `<div class="avatar-frame" style="background-image: url(${review.user_image})"></div>`
+ : `<div class="standard-image" style="background-color: #fafbfc">${frappe.get_abbr(username)}</div>`
+
+ let edit_html = review.own
+ ? `<div class="pull-right hidden-xs close-btn-container">
+ <span class="small text-muted">
+ ${'data.delete'}
+ </span>
+ </div>
+ <div class="pull-right edit-btn-container">
+ <span class="small text-muted">
+ ${'data.edit'}
+ </span>
+ </div>`
+ : '';
+
+ let rating_html = get_rating_html(item);
+
+ const $timeline_items = this.$wrapper.find('.timeline-items');
+
+ $(this.get_timeline_item(review, image_html, edit_html, rating_html))
+ .appendTo($timeline_items);
+ }
+
+ get_timeline_item(data, image_html, edit_html, rating_html) {
+ return `<div class="media timeline-item user-content" data-doctype="${''}" data-name="${''}">
+ <span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
+ ${image_html}
+ </span>
+ <div class="pull-left media-body">
+ <div class="media-content-wrapper">
+ <div class="action-btns">${edit_html}</div>
+
+ <div class="comment-header clearfix">
+ <span class="pull-left avatar avatar-small visible-xs">
+ ${image_html}
+ </span>
+
+ <div class="asset-details">
+ <span class="author-wrap">
+ <i class="octicon octicon-quote hidden-xs fa-fw"></i>
+ <span>${data.username}</span>
+ </span>
+ <a class="text-muted">
+ <span class="text-muted hidden-xs">–</span>
+ <span class="hidden-xs">${comment_when(data.modified)}</span>
+ </a>
+ </div>
+ </div>
+ <div class="reply timeline-content-show">
+ <div class="timeline-item-content">
+ <p class="text-muted">
+ ${rating_html}
+ </p>
+ <h6 class="bold">${data.subject}</h6>
+ <p class="text-muted">
+ ${data.content}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>`;
+ }
+}
+erpnext.hub.Register = class Register extends SubPage {
+ make_wrapper() {
+ super.make_wrapper();
+ this.$register_container = $(`<div class="row register-container">`)
+ .appendTo(this.$wrapper);
+ this.$form_container = $('<div class="col-md-8 col-md-offset-1 form-container">')
+ .appendTo(this.$wrapper);
+ }
+
+ refresh() {
+ this.$register_container.empty();
+ this.$form_container.empty();
+ this.render();
+ }
+
+ render() {
+ this.make_field_group();
+ }
+
+ make_field_group() {
+ const fields = [
+ {
+ fieldtype: 'Link',
+ fieldname: 'company',
+ label: __('Company'),
+ options: 'Company',
+ onchange: () => {
+ const value = this.field_group.get_value('company');
+
+ if (value) {
+ frappe.db.get_doc('Company', value)
+ .then(company => {
+ this.field_group.set_values({
+ country: company.country,
+ company_email: company.email,
+ currency: company.default_currency
+ });
+ });
+ }
+ }
+ },
+ {
+ fieldname: 'company_email',
+ label: __('Email'),
+ fieldtype: 'Data'
+ },
+ {
+ fieldname: 'country',
+ label: __('Country'),
+ fieldtype: 'Read Only'
+ },
+ {
+ fieldname: 'currency',
+ label: __('Currency'),
+ fieldtype: 'Read Only'
+ },
+ {
+ fieldtype: 'Text',
+ label: __('About your Company'),
+ fieldname: 'company_description'
+ }
+ ];
+
+ this.field_group = new frappe.ui.FieldGroup({
+ parent: this.$form_container,
+ fields
+ });
+
+ this.field_group.make();
+
+ const default_company = frappe.defaults.get_default('company');
+ this.field_group.set_value('company', default_company);
+
+ this.$form_container.find('.form-column').append(`
+ <div class="text-right">
+ <button type="submit" class="btn btn-primary btn-register btn-sm">${__('Submit')}</button>
+ </div>
+ `);
+
+ this.$form_container.find('.form-message').removeClass('hidden small').addClass('h4').text(__('Become a Seller'))
+
+ this.$form_container.on('click', '.btn-register', (e) => {
+ const form_values = this.field_group.get_values();
+
+ let values_filled = true;
+ const mandatory_fields = ['company', 'company_email', 'company_description'];
+ mandatory_fields.forEach(field => {
+ const value = form_values[field];
+ if (!value) {
+ this.field_group.set_df_property(field, 'reqd', 1);
+ values_filled = false;
+ }
+ });
+ if (!values_filled) return;
+
+ frappe.call({
+ method: 'erpnext.hub_node.doctype.hub_settings.hub_settings.register_seller',
+ args: form_values,
+ btn: $(e.currentTarget)
+ }).then(() => {
+ frappe.set_route('marketplace', 'publish');
+
+ // custom jquery event
+ this.$wrapper.trigger('seller-registered');
+ });
+ });
+ }
+}
+
+erpnext.hub.Publish = class Publish extends SubPage {
+ make_wrapper() {
+ super.make_wrapper();
+ const title_html = `<b>${__('Select Products to Publish')}</b>`;
+ const info = `<p class="text-muted">${__("Status decided by the 'Publish in Hub' field in Item.")}</p>`;
+ 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">
+ <i class="visible-xs octicon octicon-check"></i>
+ <span class="hidden-xs">Publish</span>
+ </button>`;
+
+ const select_all_button = `<button class="btn btn-secondary btn-default btn-xs margin-right select-all">Select All</button>`;
+ const deselect_all_button = `<button class="btn btn-secondary btn-default btn-xs deselect-all">Deselect All</button>`;
+
+ const search_html = `<div class="hub-search-container">
+ <input type="text" class="form-control" placeholder="Search Items">
+ </div>`;
+
+ const subpage_header = $(`
+ <div class='subpage-title flex'>
+ <div>
+ ${title_html}
+ ${subtitle_html}
+ </div>
+ ${publish_button_html}
+ </div>
+
+ ${search_html}
+
+ ${select_all_button}
+ ${deselect_all_button}
+ `);
+
+ this.$wrapper.append(subpage_header);
+
+ this.setup_events();
+ }
+
+ setup_events() {
+ this.$wrapper.find('.select-all').on('click', () => {
+ this.$wrapper.find('.hub-card').addClass('active');
+ });
+
+ this.$wrapper.find('.deselect-all').on('click', () => {
+ this.$wrapper.find('.hub-card').removeClass('active');
+ });
+
+ this.$wrapper.find('.publish-items').on('click', () => {
+ this.publish_selected_items()
+ .then(r => {
+ frappe.msgprint('check');
+ });
+ });
+
+ const $search_input = this.$wrapper.find('.hub-search-container input');
+ this.search_value = '';
+
+ $search_input.on('keydown', frappe.utils.debounce((e) => {
+ if (e.which === frappe.ui.keyCode.ENTER) {
+ this.search_value = $search_input.val();
+ this.get_items_and_render();
+ }
+ }, 300));
+ }
+
+ get_items_and_render() {
+ this.$wrapper.find('.hub-card-container').empty();
+ this.get_valid_items()
+ .then(r => {
+ this.render(r.message);
+ });
+ }
+
+ refresh() {
+ this.get_items_and_render();
+ }
+
+ render(items) {
+ const items_container = $(get_item_card_container_html(items));
+ items_container.addClass('static').on('click', '.hub-card', (e) => {
+ const $target = $(e.currentTarget);
+ $target.toggleClass('active');
+ });
+
+ this.$wrapper.append(items_container);
+ }
+
+ get_valid_items() {
+ return frappe.call(
+ 'erpnext.hub_node.get_valid_items',
+ {
+ search_value: this.search_value
+ }
+ );
+ }
+
+ publish_selected_items() {
+ const items_to_publish = [];
+ const items_to_unpublish = [];
+ this.$wrapper.find('.hub-card').map(function () {
+ const active = $(this).hasClass('active');
+
+ if(active) {
+ items_to_publish.push($(this).attr("data-id"));
+ } else {
+ items_to_unpublish.push($(this).attr("data-id"));
+ }
+ });
+
+ return frappe.call(
+ 'erpnext.hub_node.publish_selected_items',
+ {
+ items_to_publish: items_to_publish,
+ items_to_unpublish: items_to_unpublish
+ }
+ );
+ }
+}
+
+function get_item_card_container_html(items, title='') {
+ const items_html = (items || []).map(item => get_item_card_html(item)).join('');
+
+ const html = `<div class="row hub-card-container">
+ <div class="col-sm-12 margin-bottom">
+ <b>${title}</b>
+ </div>
+ ${items_html}
+ </div>`;
+
+ return html;
+}
+
+function get_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_name;
+
+ const 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>`)
+ }
+ subtitle.push(company_name);
+
+ let dot_spacer = '<span aria-hidden="true"> · </span>';
+ subtitle = subtitle.join(dot_spacer);
+
+ // Decide item link
+ const is_local = item.source_type === "local";
+ const route = !is_local
+ ? `marketplace/item/${item.hub_item_code}`
+ : `Form/Item/${item.item_name}`;
+
+ const card_route = is_local ? '' : `data-route='${route}'`;
+
+ const show_local_item_button = is_local
+ ? `<div class="overlay button-overlay" data-route='${route}' onclick="event.preventDefault();">
+ <button class="btn btn-default zoom-view">
+ <i class="octicon octicon-eye"></i>
+ </button>
+ </div>`
+ : '';
+
+ const item_html = `
+ <div class="col-md-3 col-sm-4 col-xs-6">
+ <div class="hub-card ${active ? 'active' : ''}" ${card_route} data-id="${id}">
+ <div class="hub-card-header">
+ <div class="title">
+ <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 ${item.image ? '' : 'no-image'}" src="${img_url}" />
+ <div class="overlay hub-card-overlay"></div>
+ ${show_local_item_button}
+ </div>
+ </div>
+ </div>
+ `;
+
+ return item_html;
+}
+
+function get_rating_html(item) {
+ const rating = item.average_rating;
+ let rating_html = ``;
+ for (var i = 0; i < 5; i++) {
+ let star_class = 'fa-star';
+ if (i >= rating) star_class = 'fa-star-o';
+ rating_html += `<i class='fa fa-fw ${star_class} star-icon' data-index=${i}></i>`;
+ }
+ return rating_html;
+}
+
+erpnext.hub.cache = {};
+hub.call = function call_hub_method(method, args={}) {
+ return new Promise((resolve, reject) => {
+
+ // cache
+ const key = method + JSON.stringify(args);
+ if (erpnext.hub.cache[key]) {
+ resolve(erpnext.hub.cache[key]);
+ }
+
+ // cache invalidation after 5 minutes
+ setTimeout(() => {
+ delete erpnext.hub.cache[key];
+ }, 5 * 60 * 1000);
+
+ frappe.call({
+ method: 'erpnext.hub_node.call_hub_method',
+ args: {
+ method,
+ params: args
+ }
+ })
+ .then(r => {
+ if (r.message) {
+ erpnext.hub.cache[key] = r.message;
+ resolve(r.message)
+ }
+ reject(r)
+ })
+ .fail(reject)
+ });
+}
+
erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
setup_defaults() {