From ea2d1b0a191ac95d12845efeb4710508d2260d34 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 23 Jul 2018 18:44:46 +0530 Subject: [PATCH] New page: marketplace - subpages: home, favourites, item, category --- erpnext/hub_node/__init__.py | 2 +- erpnext/public/js/hub/hub_factory.js | 32 +- erpnext/public/js/hub/hub_listing.js | 459 +++++++++++++++++++++++++-- erpnext/public/less/hub.less | 52 ++- 4 files changed, 503 insertions(+), 42 deletions(-) diff --git a/erpnext/hub_node/__init__.py b/erpnext/hub_node/__init__.py index 65b1386466..a5e4607122 100644 --- a/erpnext/hub_node/__init__.py +++ b/erpnext/hub_node/__init__.py @@ -21,7 +21,7 @@ def get_list(doctype, start=0, limit=20, fields=["*"], filters="{}", order_by=No response = connection.get_list(doctype, limit_start=start, limit_page_length=limit, - filters=filters, fields=fields) + filters=filters, fields=['name']) # Bad, need child tables in response listing = [] diff --git a/erpnext/public/js/hub/hub_factory.js b/erpnext/public/js/hub/hub_factory.js index 6451e1d6a9..d54787af36 100644 --- a/erpnext/public/js/hub/hub_factory.js +++ b/erpnext/public/js/hub/hub_factory.js @@ -1,6 +1,32 @@ frappe.provide('erpnext.hub.pages'); -frappe.views.HubFactory = frappe.views.Factory.extend({ +frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.Factory { + show() { + const page_name = frappe.get_route_str(); + + if (frappe.pages.marketplace) { + frappe.container.change_to('marketplace'); + erpnext.hub.marketplace.refresh(); + } else { + this.make('marketplace'); + } + } + + make(page_name) { + const assets = [ + '/assets/erpnext/js/hub/hub_listing.js' + ]; + + frappe.require(assets, () => { + erpnext.hub.marketplace = new erpnext.hub.Marketplace({ + parent: this.make_page(true, page_name) + }); + }); + } +} + +frappe.views.HubFactory = class HubFactory extends frappe.views.Factory { + make(route) { const page_name = frappe.get_route_str(); const page = route[1]; @@ -60,7 +86,7 @@ frappe.views.HubFactory = frappe.views.Factory.extend({ window.hub_page = erpnext.hub.pages[page_name]; } }); - }, + } render_offline_card() { let html = `
@@ -77,4 +103,4 @@ frappe.views.HubFactory = frappe.views.Factory.extend({ return; } -}); +} diff --git a/erpnext/public/js/hub/hub_listing.js b/erpnext/public/js/hub/hub_listing.js index 9ac1b848ea..a6a9ac975b 100644 --- a/erpnext/public/js/hub/hub_listing.js +++ b/erpnext/public/js/hub/hub_listing.js @@ -1,5 +1,404 @@ frappe.provide('erpnext.hub'); +erpnext.hub.Marketplace = class Marketplace { + constructor({ parent }) { + this.$parent = $(parent); + this.page = parent.page; + + 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); + }); + } + + make_sidebar() { + this.$sidebar = this.$parent.find('.layout-side-section'); + + this.$sidebar.append(` + + `); + + this.make_sidebar_categories(); + } + + make_sidebar_categories() { + frappe.call('erpnext.hub_node.get_categories') + .then(r => { + const categories = r.message.map(d => d.value).sort(); + const sidebar_items = [ + `
  • + ${__('Category')} +
  • `, + `
  • + ${__('All')} +
  • `, + ...categories.map(category => ` +
  • + ${__(category)} +
  • + `) + ]; + + this.$sidebar.append(` + + `); + + this.update_sidebar(); + }); + } + + make_body() { + this.$body = this.$parent.find('.layout-main-section'); + } + + 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 (!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 = $(`
    `).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.get_items() + .then(r => { + erpnext.hub.hub_item_cache = r.message; + this.render(r.message); + }); + } + + get_items() { + return frappe.call('erpnext.hub_node.get_list', { + doctype: 'Hub Item', + filters: { + image: ['like', 'http%'] + } + }); + } + + make_search_bar() { + const $search = $(` +
    + +
    ` + ); + 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.html(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) { + const html = get_item_card_container_html(items, __('Favourites')); + this.$wrapper.html(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) { + 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.html(html) + } +} + +erpnext.hub.Item = class Item extends SubPage { + refresh() { + const hub_item_code = frappe.get_route()[2]; + + this.get_item(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 = ''; + + const description = item.description || ''; + + const rating_html = get_rating_html(item); + const rating_count = item.reviews.length > 0 ? `(${item.reviews.length} reviews)` : ''; + + const html = ` +
    +
    +
    +
    + +
    +
    +
    +

    ${title}

    +
    +

    ${where}${dot_spacer}${when}

    +

    ${rating_html}${rating_count}

    +
    +
    +
    + ${description ? + `${__('Description')} +

    ${description}

    + ` : __('No description') + } +
    +
    +
    +
    +
    + Seller Information +
    +
    + +
    +
    + ${company} +

    + Contact Seller +

    +
    +
    +
    + `; + + this.$wrapper.html(html); + } +} + + + +function get_item_card_container_html(items, title) { + const html = (items || []).map(item => get_item_card_html(item)).join(''); + + return ` +
    +
    + ${title} +
    + ${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 route = `marketplace/item/${item.hub_item_code}`; + + let subtitle = [comment_when(item.creation)]; + const rating = get_rating(item); + if (rating > 0) { + subtitle.push(rating + ``) + } + subtitle.push(company_name); + + let dot_spacer = ''; + subtitle = subtitle.join(dot_spacer); + + const item_html = ` +
    +
    +
    +
    ${title}
    +
    ${subtitle}
    +
    +
    + +
    +
    +
    +
    + `; + + return item_html; +} + +function get_rating(item) { + const review_length = (item.reviews || []).length; + return review_length + ? item.reviews + .map(r => r.rating) + .reduce((a, b) => a + b, 0) / review_length + : 0; +} + +function get_rating_html(item) { + const rating = get_rating(item); + 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 += ``; + } + return rating_html; +} + erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { setup_defaults() { super.setup_defaults(); @@ -40,7 +439,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { get_meta() { return new Promise(resolve => - frappe.call('erpnext.hub_node.get_meta', {doctype: this.doctype}, resolve)); + frappe.call('erpnext.hub_node.get_meta', { doctype: this.doctype }, resolve)); } set_breadcrumbs() { } @@ -83,7 +482,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { } setup_view() { - if(frappe.route_options){ + if (frappe.route_options) { const filters = []; for (let field in frappe.route_options) { var value = frappe.route_options[field]; @@ -146,9 +545,9 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { {{ _("Payment Cancelled") }}
    -

    ${ __("Your payment is cancelled.") }

    +

    ${ __("Your payment is cancelled.")}

    - ${ __("Continue") }
    + ${ __("Continue")}
    `; let page = this.page.wrapper.find('.layout-side-section') @@ -172,7 +571,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { `); } - if(this.data.length) { + if (this.data.length) { this.doc = this.data[0]; } @@ -226,11 +625,11 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { } get_image_html(encoded_name, src, alt_text) { - return `${ alt_text }`; + return `${alt_text}`; } get_image_placeholder(title) { - return `${ frappe.get_abbr(title) }`; + return `${frappe.get_abbr(title)}`; } loadImage(item) { @@ -241,7 +640,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { let placeholder = this.get_image_placeholder(title); let $container = this.$result.find(`.image-field[data-name="${encoded_name}"]`); - if(!item[this.imageFieldName]) { + if (!item[this.imageFieldName]) { $container.prepend(placeholder); $container.addClass('no-image'); } @@ -262,7 +661,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { } setup_quick_view() { - if(this.quick_view) return; + if (this.quick_view) return; this.quick_view = new frappe.ui.Dialog({ title: 'Quick View', @@ -312,10 +711,10 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { } setup_like() { - if(this.setup_like_done) return; + if (this.setup_like_done) return; this.setup_like_done = 1; this.$result.on('click', '.btn.like-button', (e) => { - if($(e.target).hasClass('changing')) return; + if ($(e.target).hasClass('changing')) return; $(e.target).addClass('changing'); e.preventDefault(); @@ -326,13 +725,13 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { let values = this.data_dict[name]; let heart = $(e.target); - if(heart.hasClass('like-button')) { + if (heart.hasClass('like-button')) { heart = $(e.target).find('.octicon'); } let remove = 1; - if(heart.hasClass('liked')) { + if (heart.hasClass('liked')) { // unlike heart.removeClass('liked'); } else { @@ -349,7 +748,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { }, callback: (r) => { let message = __("Added to Favourites"); - if(remove) { + if (remove) { message = __("Removed from Favourites"); } frappe.show_alert(message); @@ -454,7 +853,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { label: 'All Categories', expandable: true, - args: {parent: this.current_category}, + args: { parent: this.current_category }, method: 'erpnext.hub_node.get_categories', on_click: (node) => { this.update_category(node.label); @@ -501,8 +900,8 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { .then(r => { const categories = r.message.map(d => d.value).sort(); const sidebar_items = [ - `
  • - Category + `
  • + ${__('Category')}
  • `, `
  • All @@ -523,7 +922,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { } update_category(label) { - this.current_category = (label=='All Categories') ? undefined : label; + this.current_category = (label == 'All Categories') ? undefined : label; this.refresh(); } @@ -573,14 +972,14 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { const ratingAverage = reviewLength ? item.reviews .map(r => r.rating) - .reduce((a, b) => a + b, 0)/reviewLength + .reduce((a, b) => a + b, 0) / reviewLength : -1; let ratingHtml = ``; - for(var i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { let starClass = 'fa-star'; - if(i >= ratingAverage) starClass = 'fa-star-o'; + if (i >= ratingAverage) starClass = 'fa-star-o'; ratingHtml += ``; } let dot_spacer = ''; @@ -608,7 +1007,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { (${reviewLength})
    @@ -651,7 +1050,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing { }; -erpnext.hub.Favourites = class Favourites extends erpnext.hub.ItemListing { +erpnext.hub.Favourites2 = class Favourites extends erpnext.hub.ItemListing { constructor(opts) { super(opts); this.show(); @@ -702,18 +1101,18 @@ erpnext.hub.Favourites = class Favourites extends erpnext.hub.ItemListing { } update_category(label) { - this.current_category = (label=='All Categories') ? undefined : label; + this.current_category = (label == 'All Categories') ? undefined : label; this.refresh(); } get_filters_for_args() { - if(!this.filter_area) return; + if (!this.filter_area) return; let filters = {}; this.filter_area.get().forEach(f => { let field = f[1] !== 'name' ? f[1] : 'item_name'; filters[field] = [f[2], f[3]]; }); - if(this.current_category) { + if (this.current_category) { filters['hub_category'] = this.current_category; } return filters; @@ -772,10 +1171,10 @@ erpnext.hub.CompanyListing = class CompanyListing extends erpnext.hub.HubListing get_filters_for_args() { let filters = {}; - this.filter_area.get().forEach(f => { - let field = f[1] !== 'name' ? f[1] : 'company_name'; - filters[field] = [f[2], f[3]]; - }); + // this.filter_area.get().forEach(f => { + // let field = f[1] !== 'name' ? f[1] : 'company_name'; + // filters[field] = [f[2], f[3]]; + // }); return filters; } diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less index ef08fde270..48d52ca5aa 100644 --- a/erpnext/public/less/hub.less +++ b/erpnext/public/less/hub.less @@ -1,18 +1,15 @@ @import "../../../../frappe/frappe/public/less/variables.less"; -body[data-route^="Hub/"] { - .hub-icon { - width: 40px; - height: 40px; +body[data-route^="marketplace/"] { + .layout-side-section { + padding-top: 25px; + padding-right: 25px; } .layout-main-section { border: none; - } - - .frappe-list { - padding-top: 25px; font-size: @text-medium; + padding-top: 25px; @media (max-width: @screen-xs) { padding-left: 20px; @@ -26,17 +23,32 @@ body[data-route^="Hub/"] { border-radius: 4px; overflow: hidden; cursor: pointer; + + &:hover .hub-card-overlay { + display: block; + } } .hub-card-header { padding: 12px 15px; height: 60px; + border-bottom: 1px solid @border-color; } .hub-card-body { + position: relative; height: 200px; } + .hub-card-overlay { + display: none; + position: absolute; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.1); + } + .hub-card-image { min-width: 100%; width: 100%; @@ -73,6 +85,30 @@ body[data-route^="Hub/"] { } } + .hub-item-image { + border: 1px solid @border-color; + border-radius: 4px; + overflow: hidden; + height: 200px; + width: 200px; + display: flex; + align-items: center; + } + + .hub-item-seller img { + width: 50px; + height: 50px; + border-radius: 4px; + border: 1px solid @border-color; + } +} + +body[data-route^="Hub/"] { + .hub-icon { + width: 40px; + height: 40px; + } + .img-wrapper { border: 1px solid #d1d8dd; border-radius: 3px;