Break marketplace.js into multiple files

This commit is contained in:
Faris Ansari 2018-08-01 14:09:07 +05:30
parent c0a22a689e
commit 709a4a3f4e
18 changed files with 1162 additions and 2495 deletions

View File

@ -7,6 +7,9 @@
"public/js/website_utils.js",
"public/js/shopping_cart.js"
],
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
],
"js/erpnext.min.js": [
"public/js/conf.js",
"public/js/utils.js",

View File

@ -0,0 +1,143 @@
function get_empty_state(message, action) {
return `<div class="empty-state flex align-center flex-column justify-center">
<p class="text-muted">${message}</p>
${action ? `<p>${action}</p>`: ''}
</div>`;
}
function get_item_card_container_html(items, title='', get_item_html = get_item_card_html) {
const items_html = (items || []).map(item => get_item_html(item)).join('');
const title_html = title
? `<div class="col-sm-12 margin-bottom">
<b>${title}</b>
</div>`
: '';
const html = `<div class="row hub-card-container">
${title_html}
${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;
// 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);
const item_html = `
<div class="col-md-3 col-sm-4 col-xs-6">
<div class="hub-card" data-route="marketplace/item/${item.hub_item_code}">
<div class="hub-card-header">
<div class="hub-card-title ellipsis bold">${title}</div>
<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
</div>
<div class="hub-card-body">
<img class="hub-card-image" src="${img_url}" />
<div class="overlay hub-card-overlay"></div>
</div>
</div>
</div>
`;
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>`)
}
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">
<div class="hub-card is-local ${is_active ? 'active' : ''}" data-id="${id}">
<div class="hub-card-header">
<div class="hub-card-title ellipsis bold">${title}</div>
<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</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_rating_html(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;
}
function make_search_bar({wrapper, on_search, placeholder = __('Search for anything')}) {
const $search = $(`
<div class="hub-search-container">
<input type="text" class="form-control" placeholder="${placeholder}">
</div>`
);
wrapper.append($search);
const $search_input = $search.find('input');
$search_input.on('keydown', frappe.utils.debounce((e) => {
if (e.which === frappe.ui.keyCode.ENTER) {
const search_value = $search_input.val();
on_search(search_value);
}
}, 300));
}
export {
get_empty_state,
get_item_card_container_html,
get_item_card_html,
get_local_item_card_html,
get_rating_html,
make_search_bar,
}

View File

@ -0,0 +1,44 @@
frappe.provide('hub');
frappe.provide('erpnext.hub');
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
const timeout = 5 * 60 * 1000;
setTimeout(() => {
delete erpnext.hub.cache[key];
}, timeout);
frappe.call({
method: 'erpnext.hub_node.call_hub_method',
args: {
method,
params: args
}
})
.then(r => {
if (r.message) {
if (r.message.error) {
frappe.throw({
title: __('Marketplace Error'),
message: r.message.error
});
}
erpnext.hub.cache[key] = r.message;
resolve(r.message)
}
reject(r)
})
.fail(reject)
});
}

View File

@ -1,9 +1,7 @@
frappe.provide('erpnext.hub.pages');
frappe.provide('erpnext.hub');
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();
@ -14,7 +12,7 @@ frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.
make(page_name) {
const assets = [
'/assets/erpnext/js/hub/marketplace.js'
'/assets/js/marketplace.min.js'
];
frappe.require(assets, () => {
@ -24,83 +22,3 @@ frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.
});
}
}
frappe.views.HubFactory = class HubFactory extends frappe.views.Factory {
make(route) {
const page_name = frappe.get_route_str();
const page = route[1];
const assets = {
'List': [
'/assets/erpnext/js/hub/hub_listing.js',
],
'Form': [
'/assets/erpnext/js/hub/hub_form.js'
]
};
frappe.model.with_doc('Hub Settings', 'Hub Settings', () => {
this.hub_settings = frappe.get_doc('Hub Settings');
if (!erpnext.hub.pages[page_name]) {
if(!frappe.is_online()) {
this.render_offline_card();
return;
}
if (!route[2]) {
frappe.require(assets['List'], () => {
if(page === 'Favourites') {
erpnext.hub.pages[page_name] = new erpnext.hub['Favourites']({
parent: this.make_page(true, page_name),
hub_settings: this.hub_settings
});
} else {
erpnext.hub.pages[page_name] = new erpnext.hub[page+'Listing']({
parent: this.make_page(true, page_name),
hub_settings: this.hub_settings
});
}
});
} else if (!route[3]){
frappe.require(assets['Form'], () => {
erpnext.hub.pages[page_name] = new erpnext.hub[page+'Page']({
unique_id: route[2],
doctype: route[2],
parent: this.make_page(true, page_name),
hub_settings: this.hub_settings
});
});
} else {
frappe.require(assets['List'], () => {
frappe.route_options = {};
frappe.route_options["company_name"] = route[2]
erpnext.hub.pages[page_name] = new erpnext.hub['ItemListing']({
parent: this.make_page(true, page_name),
hub_settings: this.hub_settings
});
});
}
window.hub_page = erpnext.hub.pages[page_name];
} else {
frappe.container.change_to(page_name);
window.hub_page = erpnext.hub.pages[page_name];
}
});
}
render_offline_card() {
let html = `<div class='page-card' style='margin: 140px auto;'>
<div class='page-card-head'>
<span class='indicator red'>${'Failed to connect'}</span>
</div>
<p>${ __("Please check your network connection.") }</p>
<div><a href='#Hub/Item' class='btn btn-primary btn-sm'>
${ __("Reload") }</a></div>
</div>`;
let page = $('#body_div');
page.append(html);
return;
}
}

View File

@ -1,493 +0,0 @@
frappe.provide('erpnext.hub');
erpnext.hub.HubDetailsPage = class HubDetailsPage extends frappe.views.BaseList {
setup_defaults() {
super.setup_defaults();
this.method = 'erpnext.hub_node.get_details';
const route = frappe.get_route();
// this.page_name = route[2];
}
setup_fields() {
return this.get_meta()
.then(r => {
this.meta = r.message.meta || this.meta;
this.categories = r.message.categories || [];
this.bootstrap_data(r.message);
this.getFormFields();
});
}
bootstrap_data() { }
get_meta() {
return new Promise(resolve =>
frappe.call('erpnext.hub_node.get_meta', {doctype: 'Hub ' + this.doctype}, resolve));
}
set_breadcrumbs() {
frappe.breadcrumbs.add({
label: __('Hub'),
route: '#Hub/' + this.doctype,
type: 'Custom'
});
}
setup_side_bar() {
this.sidebar = new frappe.ui.Sidebar({
wrapper: this.$page.find('.layout-side-section'),
css_class: 'hub-form-sidebar'
});
}
setup_filter_area() { }
setup_sort_selector() { }
// let category = this.quick_view.get_values().hub_category;
// return new Promise((resolve, reject) => {
// frappe.call({
// method: 'erpnext.hub_node.update_category',
// args: {
// hub_item_code: values.hub_item_code,
// category: category,
// },
// callback: (r) => {
// resolve();
// },
// freeze: true
// }).fail(reject);
// });
get_timeline() {
return `<div class="timeline">
<div class="timeline-head">
</div>
<div class="timeline-new-email">
<button class="btn btn-default btn-reply-email btn-xs">
${__("Reply")}
</button>
</div>
<div class="timeline-items"></div>
</div>`;
}
get_footer() {
return `<div class="form-footer">
<div class="after-save">
<div class="form-comments"></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>`;
}
get_args() {
return {
hub_sync_id: this.unique_id,
doctype: 'Hub ' + this.doctype
};
}
prepare_data(r) {
this.data = r.message;
}
update_data(r) {
this.data = r.message;
}
render() {
const image_html = this.data[this.image_field_name] ?
`<img src="${this.data[this.image_field_name]}">
<span class="helper"></span>` :
`<div class="standard-image">${frappe.get_abbr(this.page_title)}</div>`;
this.sidebar.remove_item('image');
this.sidebar.add_item({
name: 'image',
label: image_html
});
if(!this.form) {
let fields = this.formFields;
this.form = new frappe.ui.FieldGroup({
parent: this.$result,
fields
});
this.form.make();
}
if(this.data.hub_category) {
this.form.fields_dict.set_category.hide();
}
this.form.set_values(this.data);
this.$result.show();
this.$timelineList && this.$timelineList.empty();
if(this.data.reviews && this.data.reviews.length) {
this.data.reviews.map(review => {
this.addReviewToTimeline(review);
})
}
this.postRender()
}
postRender() {}
attachFooter() {
let footerHtml = `<div class="form-footer">
<div class="form-comments"></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>`;
let parent = $('<div>').appendTo(this.page.main.parent());
this.$footer = $(footerHtml).appendTo(parent);
}
attachTimeline() {
let timelineHtml = `<div class="timeline">
<div class="timeline-head">
</div>
<div class="timeline-new-email">
<button class="btn btn-default btn-reply-email btn-xs">
${ __("Reply") }
</button>
</div>
<div class="timeline-items"></div>
</div>`;
let parent = this.$footer.find(".form-comments");
this.$timeline = $(timelineHtml).appendTo(parent);
this.$timelineList = this.$timeline.find(".timeline-items");
}
attachReviewArea() {
this.comment_area = new frappe.ui.ReviewArea({
parent: this.$footer.find('.timeline-head'),
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.data.hub_item_code,
review: val
},
callback: (r) => {
this.refresh();
this.comment_area.reset();
},
freeze: true
});
}
});
}
addReviewToTimeline(data) {
let username = data.username || data.user || __("Anonymous")
let imageHtml = data.user_image
? `<div class="avatar-frame" style="background-image: url(${data.user_image})"></div>`
: `<div class="standard-image" style="background-color: #fafbfc">${frappe.get_abbr(username)}</div>`
let editHtml = data.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 ratingHtml = '';
for(var i = 0; i < 5; i++) {
let starIcon = 'fa-star-o'
if(i < data.rating) {
starIcon = 'fa-star';
}
ratingHtml += `<i class="fa fa-fw ${starIcon} star-icon" data-idx='${i}'></i>`;
}
$(this.getTimelineItem(data, imageHtml, editHtml, ratingHtml))
.appendTo(this.$timelineList);
}
getTimelineItem(data, imageHtml, editHtml, ratingHtml) {
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">
${imageHtml}
</span>
<div class="pull-left media-body">
<div class="media-content-wrapper">
<div class="action-btns">${editHtml}</div>
<div class="comment-header clearfix small ${'linksActive'}">
<span class="pull-left avatar avatar-small visible-xs">
${imageHtml}
</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 href="#Form/${''}" class="text-muted">
<span class="text-muted hidden-xs">&ndash;</span>
<span class="indicator-right ${'green'}
delivery-status-indicator">
<span class="hidden-xs">${data.pretty_date}</span>
</span>
</a>
<a class="text-muted reply-link pull-right timeline-content-show"
title="${__('Reply')}"> ${''} </a>
<span class="comment-likes hidden-xs">
<i class="octicon octicon-heart like-action text-extra-muted not-liked fa-fw">
</i>
<span class="likes-count text-muted">10</span>
</span>
</div>
</div>
<div class="reply timeline-content-show">
<div class="timeline-item-content">
<p class="text-muted small">
<b>${data.subject}</b>
</p>
<hr>
<p class="text-muted small">
${ratingHtml}
</p>
<hr>
<p>
${data.content}
</p>
</div>
</div>
</div>
</div>
</div>`;
}
prepareFormFields(fields, fieldnames) {
return fields
.filter(field => fieldnames.includes(field.fieldname))
.map(field => {
let {
label,
fieldname,
fieldtype,
} = field;
let read_only = 1;
return {
label,
fieldname,
fieldtype,
read_only,
};
});
}
};
erpnext.hub.ItemPage = class ItemPage extends erpnext.hub.HubDetailsPage {
constructor(opts) {
super(opts);
this.show();
}
setup_defaults() {
super.setup_defaults();
this.doctype = 'Item';
this.image_field_name = 'image';
}
setup_page_head() {
super.setup_page_head();
this.set_primary_action();
}
setup_side_bar() {
super.setup_side_bar();
this.attachFooter();
this.attachTimeline();
this.attachReviewArea();
}
set_primary_action() {
let item = this.data;
this.page.set_primary_action(__('Request a Quote'), () => {
this.show_rfq_modal()
.then(values => {
item.item_code = values.item_code;
delete values.item_code;
const supplier = values;
return [item, supplier];
})
.then(([item, supplier]) => {
return this.make_rfq(item, supplier, this.page.btn_primary);
})
.then(r => {
console.log(r);
if (r.message && r.message.rfq) {
this.page.btn_primary.addClass('disabled').html(`<span><i class='fa fa-check'></i> ${__('Quote Requested')}</span>`);
} else {
throw r;
}
})
.catch((e) => {
console.log(e); //eslint-disable-line
});
}, 'octicon octicon-plus');
}
prepare_data(r) {
super.prepare_data(r);
this.page.set_title(this.data["item_name"]);
}
make_rfq(item, supplier, btn) {
console.log(supplier);
return new Promise((resolve, reject) => {
frappe.call({
method: 'erpnext.hub_node.make_rfq_and_send_opportunity',
args: { item, supplier },
callback: resolve,
btn,
}).fail(reject);
});
}
postRender() {
this.categoryDialog = new frappe.ui.Dialog({
title: __('Suggest Category'),
fields: [
{
label: __('Category'),
fieldname: 'category',
fieldtype: 'Autocomplete',
options: this.categories,
reqd: 1
}
],
primary_action_label: __("Send"),
primary_action: () => {
let values = this.categoryDialog.get_values();
frappe.call({
method: 'erpnext.hub_node.update_category',
args: {
hub_item_code: this.data.hub_item_code,
category: values.category
},
callback: () => {
this.categoryDialog.hide();
this.refresh();
},
freeze: true
}).fail(() => {});
}
});
}
getFormFields() {
let colOneFieldnames = ['item_name', 'item_code', 'description'];
let colTwoFieldnames = ['seller', 'company_name', 'country'];
let colOneFields = this.prepareFormFields(this.meta.fields, colOneFieldnames);
let colTwoFields = this.prepareFormFields(this.meta.fields, colTwoFieldnames);
let miscFields = [
{
label: __('Category'),
fieldname: 'hub_category',
fieldtype: 'Data',
read_only: 1
},
{
label: __('Suggest Category?'),
fieldname: 'set_category',
fieldtype: 'Button',
click: () => {
this.categoryDialog.show();
}
},
{
fieldname: 'cb1',
fieldtype: 'Column Break'
}
];
this.formFields = colOneFields.concat(miscFields, colTwoFields);
}
show_rfq_modal() {
let item = this.data;
return new Promise(res => {
let fields = [
{ label: __('Item Code'), fieldtype: 'Data', fieldname: 'item_code', default: item.item_code },
{ fieldtype: 'Column Break' },
{ label: __('Item Group'), fieldtype: 'Link', fieldname: 'item_group', default: item.item_group },
{ label: __('Supplier Details'), fieldtype: 'Section Break' },
{ label: __('Supplier Name'), fieldtype: 'Data', fieldname: 'supplier_name', default: item.company_name },
{ label: __('Supplier Email'), fieldtype: 'Data', fieldname: 'supplier_email', default: item.seller },
{ fieldtype: 'Column Break' },
{ label: __('Supplier Group'), fieldname: 'supplier_group',
fieldtype: 'Link', options: 'Supplier Group' }
];
fields = fields.map(f => { f.reqd = 1; return f; });
const d = new frappe.ui.Dialog({
title: __('Request for Quotation'),
fields: fields,
primary_action_label: __('Send'),
primary_action: (values) => {
res(values);
d.hide();
}
});
d.show();
});
}
}
erpnext.hub.CompanyPage = class CompanyPage extends erpnext.hub.HubDetailsPage {
constructor(opts) {
super(opts);
this.show();
}
setup_defaults() {
super.setup_defaults();
this.doctype = 'Company';
this.image_field_name = 'company_logo';
}
prepare_data(r) {
super.prepare_data(r);
this.page.set_title(this.data["company_name"]);
}
getFormFields() {
let fieldnames = ['company_name', 'description', 'route', 'country', 'seller', 'site_name'];;
this.formFields = this.prepareFormFields(this.meta.fields, fieldnames);
}
}

View File

@ -1,802 +0,0 @@
erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
setup_defaults() {
super.setup_defaults();
this.page_title = __('');
this.method = 'erpnext.hub_node.get_list';
this.cache = {};
const route = frappe.get_route();
this.page_name = route[1];
this.menu_items = this.menu_items.concat(this.get_menu_items());
this.imageFieldName = 'image';
this.show_filters = 0;
}
set_title() {
const title = this.page_title;
let iconHtml = `<img class="hub-icon" src="assets/erpnext/images/hub_logo.svg">`;
let titleHtml = `<span class="hub-page-title">${title}</span>`;
this.page.set_title(titleHtml, '', false, title);
}
setup_fields() {
return this.get_meta()
.then(r => {
this.meta = r.message.meta || this.meta;
frappe.model.sync(this.meta);
this.bootstrap_data(r.message);
this.prepareFormFields();
});
}
setup_filter_area() { }
get_meta() {
return new Promise(resolve =>
frappe.call('erpnext.hub_node.get_meta', { doctype: this.doctype }, resolve));
}
set_breadcrumbs() { }
prepareFormFields() { }
bootstrap_data() { }
get_menu_items() {
const items = [
{
label: __('Hub Settings'),
action: () => frappe.set_route('Form', 'Hub Settings'),
standard: true
},
{
label: __('Favourites'),
action: () => frappe.set_route('Hub', 'Favourites'),
standard: true
}
];
return items;
}
setup_side_bar() {
this.sidebar = new frappe.ui.Sidebar({
wrapper: this.page.wrapper.find('.layout-side-section'),
css_class: 'hub-sidebar'
});
}
setup_sort_selector() {
// this.sort_selector = new frappe.ui.SortSelector({
// parent: this.filter_area.$filter_list_wrapper,
// doctype: this.doctype,
// args: this.order_by,
// onchange: () => this.refresh(true)
// });
}
setup_view() {
if (frappe.route_options) {
const filters = [];
for (let field in frappe.route_options) {
var value = frappe.route_options[field];
this.page.fields_dict[field].set_value(value);
}
}
const $hub_search = $(`
<div class="hub-search-container">
<input type="text" class="form-control" placeholder="Search for anything">
</div>`
);
this.$frappe_list.prepend($hub_search);
const $search_input = $hub_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.refresh();
}
}, 300));
}
get_args() {
return {
doctype: this.doctype,
start: this.start,
limit: this.page_length,
order_by: this.order_by,
// fields: this.fields,
filters: this.get_filters_for_args()
};
}
update_data(r) {
const data = r.message;
if (this.start === 0) {
this.data = data;
} else {
this.data = this.data.concat(data);
}
this.data_dict = {};
}
freeze(toggle) { }
render() {
this.data_dict = {};
this.render_image_view();
this.setup_quick_view();
this.setup_like();
}
render_offline_card() {
let html = `<div class='page-card'>
<div class='page-card-head'>
<span class='indicator red'>
{{ _("Payment Cancelled") }}</span>
</div>
<p>${ __("Your payment is cancelled.")}</p>
<div><a href='' class='btn btn-primary btn-sm'>
${ __("Continue")}</a></div>
</div>`;
let page = this.page.wrapper.find('.layout-side-section')
page.append(html);
return;
}
render_image_view() {
var html = this.data.map(this.item_html.bind(this)).join("");
if (this.start === 0) {
// ${this.getHeaderHtml()}
this.$result.html(`
<div class="row hub-card-container">
<div class="col-md-12 margin-bottom">
<b>Recently Published</b>
</div>
${html}
</div>
`);
}
if (this.data.length) {
this.doc = this.data[0];
}
this.data.map(this.loadImage.bind(this));
this.data_dict = {};
this.data.map(d => {
this.data_dict[d.hub_item_code] = d;
});
}
getHeaderHtml(title, image, content) {
// let company_html =
return `
<header class="list-row-head text-muted small">
<div style="display: flex;">
<div class="list-header-icon">
<img title="${title}" alt="${title}" src="${image}">
</div>
<div class="list-header-info">
<h5>
${title}
</h5>
<span class="margin-vertical-10 level-item">
${content}
</span>
</div>
</div>
</header>
`;
}
renderHeader() {
return ``;
}
get_image_html(encoded_name, src, alt_text) {
return `<img data-name="${encoded_name}" src="${src}" alt="${alt_text}">`;
}
get_image_placeholder(title) {
return `<span class="placeholder-text">${frappe.get_abbr(title)}</span>`;
}
loadImage(item) {
item._name = encodeURI(item.name);
const encoded_name = item._name;
const title = strip_html(item[this.meta.title_field || 'name']);
let placeholder = this.get_image_placeholder(title);
let $container = this.$result.find(`.image-field[data-name="${encoded_name}"]`);
if (!item[this.imageFieldName]) {
$container.prepend(placeholder);
$container.addClass('no-image');
}
frappe.load_image(item[this.imageFieldName],
(imageObj) => {
$container.prepend(imageObj)
},
() => {
$container.prepend(placeholder);
$container.addClass('no-image');
},
(imageObj) => {
imageObj.title = encoded_name;
imageObj.alt = title;
}
)
}
setup_quick_view() {
if (this.quick_view) return;
this.quick_view = new frappe.ui.Dialog({
title: 'Quick View',
fields: this.formFields
});
this.quick_view.set_primary_action(__('Request a Quote'), () => {
this.show_rfq_modal()
.then(values => {
item.item_code = values.item_code;
delete values.item_code;
const supplier = values;
return [item, supplier];
})
.then(([item, supplier]) => {
return this.make_rfq(item, supplier, this.page.btn_primary);
})
.then(r => {
console.log(r);
if (r.message && r.message.rfq) {
this.page.btn_primary.addClass('disabled').html(`<span><i class='fa fa-check'></i> ${__('Quote Requested')}</span>`);
} else {
throw r;
}
})
.catch((e) => {
console.log(e); //eslint-disable-line
});
}, 'octicon octicon-plus');
this.$result.on('click', '.btn.zoom-view', (e) => {
e.preventDefault();
e.stopPropagation();
var name = $(e.target).attr('data-name');
name = decodeURIComponent(name);
this.quick_view.set_title(name);
let values = this.data_dict[name];
this.quick_view.set_values(values);
let fields = [];
this.quick_view.show();
return false;
});
}
setup_like() {
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;
$(e.target).addClass('changing');
e.preventDefault();
e.stopPropagation();
var name = $(e.target).attr('data-name');
name = decodeURIComponent(name);
let values = this.data_dict[name];
let heart = $(e.target);
if (heart.hasClass('like-button')) {
heart = $(e.target).find('.octicon');
}
let remove = 1;
if (heart.hasClass('liked')) {
// unlike
heart.removeClass('liked');
} else {
// like
remove = 0;
heart.addClass('liked');
}
frappe.call({
method: 'erpnext.hub_node.update_wishlist_item',
args: {
item_name: values.hub_item_code,
remove: remove
},
callback: (r) => {
let message = __("Added to Favourites");
if (remove) {
message = __("Removed from Favourites");
}
frappe.show_alert(message);
},
freeze: true
});
$(e.target).removeClass('changing');
return false;
});
}
}
erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
constructor(opts) {
super(opts);
this.show();
}
setup_defaults() {
super.setup_defaults();
this.doctype = 'Hub Item';
this.page_title = __('Marketplace');
this.fields = ['name', 'hub_item_code', 'image', 'item_name', 'item_code', 'company_name', 'description', 'country'];
this.filters = [];
}
render() {
this.data_dict = {};
this.render_image_view();
this.setup_quick_view();
this.setup_like();
}
bootstrap_data(response) {
// let companies = response.companies.map(d => d.name);
// this.custom_filter_configs = [
// {
// fieldtype: 'Autocomplete',
// label: __('Select Company'),
// condition: 'like',
// fieldname: 'company_name',
// options: companies
// },
// {
// fieldtype: 'Link',
// label: __('Select Country'),
// options: 'Country',
// condition: 'like',
// fieldname: 'country'
// }
// ];
}
prepareFormFields() {
let fieldnames = ['item_name', 'description', 'company_name', 'country'];
this.formFields = this.meta.fields
.filter(field => fieldnames.includes(field.fieldname))
.map(field => {
let {
label,
fieldname,
fieldtype,
} = field;
let read_only = 1;
return {
label,
fieldname,
fieldtype,
read_only,
};
});
this.formFields.unshift({
label: 'image',
fieldname: 'image',
fieldtype: 'Attach Image'
});
}
setup_side_bar() {
super.setup_side_bar();
this.setup_new_sidebar();
return;
let $pitch = $(`<div class="border" style="
margin-top: 10px;
padding: 0px 10px;
border-radius: 3px;
">
<h5>Sell on HubMarket</h5>
<p>Over 2000 products listed. Register your company to start selling.</p>
</div>`);
this.sidebar.$sidebar.append($pitch);
this.category_tree = new frappe.ui.Tree({
parent: this.sidebar.$sidebar,
label: 'All Categories',
expandable: true,
args: { parent: this.current_category },
method: 'erpnext.hub_node.get_categories',
on_click: (node) => {
this.update_category(node.label);
}
});
this.sidebar.add_item({
label: __('Companies'),
on_click: () => frappe.set_route('Hub', 'Company')
}, undefined, true);
this.sidebar.add_item({
label: this.hub_settings.company,
on_click: () => frappe.set_route('Form', 'Company', this.hub_settings.company)
}, __("Account"));
this.sidebar.add_item({
label: __("Favourites"),
on_click: () => frappe.set_route('Hub', 'Favourites')
}, __("Account"));
this.sidebar.add_item({
label: __("Settings"),
on_click: () => frappe.set_route('Form', 'Hub Settings')
}, __("Account"));
}
setup_new_sidebar() {
this.sidebar.$sidebar.append(`
<ul class="list-unstyled hub-sidebar-group">
<li class="hub-sidebar-item bold active">
Browse
</li>
<li class="hub-sidebar-item text-muted">
Favorites
</li>
<li class="hub-sidebar-item text-muted">
Become a seller
</li>
</ul>
`);
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 text-muted is-title">
${__('Category')}
</li>`,
`<li class="hub-sidebar-item active">
All
</li>`,
...categories.map(category => `
<li class="hub-sidebar-item text-muted">
${category}
</li>
`)
];
this.sidebar.$sidebar.append(`
<ul class="list-unstyled">
${sidebar_items.join('')}
</ul>
`);
});
}
update_category(label) {
this.current_category = (label == 'All Categories') ? undefined : label;
this.refresh();
}
get_filters_for_args() {
const filter = {};
if (this.search_value) {
filter.item_name = ['like', `%${this.search_value}%`];
}
filter.image = ['like', 'http%'];
return filter;
// 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) {
// filters['hub_category'] = this.current_category;
// }
// return filters;
}
update_data(r) {
super.update_data(r);
this.data_dict = {};
this.data.map(d => {
this.data_dict[d.hub_item_code] = d;
});
}
item_html(item, index) {
item._name = encodeURI(item.name);
const encoded_name = item._name;
const title = strip_html(item[this.meta.title_field || 'name']);
const img_url = item[this.imageFieldName];
const no_image = !img_url;
const _class = no_image ? 'no-image' : '';
const route = `#Hub/Item/${item.hub_item_code}`;
const company_name = item['company_name'];
const reviewLength = (item.reviews || []).length;
const ratingAverage = reviewLength
? item.reviews
.map(r => r.rating)
.reduce((a, b) => a + b, 0) / reviewLength
: -1;
let ratingHtml = ``;
for (var i = 0; i < 5; i++) {
let starClass = 'fa-star';
if (i >= ratingAverage) starClass = 'fa-star-o';
ratingHtml += `<i class='fa fa-fw ${starClass} star-icon' data-index=${i}></i>`;
}
let dot_spacer = '<span aria-hidden="true"> · </span>';
let subtitle = '';
subtitle += comment_when(item.creation);
subtitle += dot_spacer;
if (ratingAverage > 0) {
subtitle += ratingAverage + `<i class='fa fa-fw fa-star-o'></i>`;
subtitle += dot_spacer;
}
subtitle += company_name;
let item_html = `
<div class="col-sm-3 col-xs-2">
<div class="hub-card">
<div class="hub-card-header">
<div class="list-row-col list-subject ellipsis level">
<span class="level-item bold ellipsis" title="McGuffin">
<a href="${route}">${title}</a>
</span>
</div>
<div class="text-muted small" style="margin: 5px 0px;">
${ratingHtml}
(${reviewLength})
</div>
<div class="list-row-col">
<a href="${'#Hub/Company/' + company_name + '/Items'}"><p>${company_name}</p></a>
</div>
</div>
<div class="hub-card-body">
<a data-name="${encoded_name}"
title="${encoded_name}"
href="${route}"
>
<div class="image-field ${_class}"
data-name="${encoded_name}"
>
<button class="btn btn-default zoom-view" data-name="${encoded_name}">
<i class="octicon octicon-eye" data-name="${encoded_name}"></i>
</button>
<button class="btn btn-default like-button" data-name="${encoded_name}">
<i class="octicon octicon-heart" data-name="${encoded_name}"></i>
</button>
</div>
</a>
</div>
</div>
</div>
`;
item_html = `
<div class="col-md-3 col-sm-4 col-xs-6">
<div class="hub-card">
<div class="hub-card-header">
<div class="hub-card-title ellipsis bold">${title}</div>
<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
</div>
<div class="hub-card-body">
<img class="hub-card-image ${no_image ? 'no-image' : ''}" src="${img_url}" />
</div>
</div>
</div>
`;
return item_html;
}
};
erpnext.hub.Favourites2 = class Favourites extends erpnext.hub.ItemListing {
constructor(opts) {
super(opts);
this.show();
}
setup_defaults() {
super.setup_defaults();
this.doctype = 'Hub Item';
this.page_title = __('Favourites');
this.fields = ['name', 'hub_item_code', 'image', 'item_name', 'item_code', 'company_name', 'description', 'country'];
this.filters = [];
this.method = 'erpnext.hub_node.get_item_favourites';
}
setup_filter_area() { }
setup_sort_selector() { }
// setupHe
getHeaderHtml() {
return '';
}
get_args() {
return {
start: this.start,
limit: this.page_length,
order_by: this.order_by,
fields: this.fields
};
}
bootstrap_data(response) { }
prepareFormFields() { }
setup_side_bar() {
this.sidebar = new frappe.ui.Sidebar({
wrapper: this.page.wrapper.find('.layout-side-section'),
css_class: 'hub-sidebar'
});
this.sidebar.add_item({
label: __('Back to Products'),
on_click: () => frappe.set_route('Hub', 'Item')
});
}
update_category(label) {
this.current_category = (label == 'All Categories') ? undefined : label;
this.refresh();
}
get_filters_for_args() {
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) {
filters['hub_category'] = this.current_category;
}
return filters;
}
update_data(r) {
super.update_data(r);
this.data_dict = {};
this.data.map(d => {
this.data_dict[d.hub_item_code] = d;
});
}
};
erpnext.hub.CompanyListing = class CompanyListing extends erpnext.hub.HubListing {
constructor(opts) {
super(opts);
this.show();
}
render() {
this.data_dict = {};
this.render_image_view();
}
setup_defaults() {
super.setup_defaults();
this.doctype = 'Hub Company';
this.page_title = __('Companies');
this.fields = ['company_logo', 'name', 'site_name', 'seller_city', 'seller_description', 'seller', 'country', 'company_name'];
this.filters = [];
this.custom_filter_configs = [
{
fieldtype: 'Link',
label: 'Country',
options: 'Country',
condition: 'like',
fieldname: 'country'
}
];
this.imageFieldName = 'company_logo';
}
setup_side_bar() {
this.sidebar = new frappe.ui.Sidebar({
wrapper: this.page.wrapper.find('.layout-side-section'),
css_class: 'hub-sidebar'
});
this.sidebar.add_item({
label: __('Back to Products'),
on_click: () => frappe.set_route('Hub', 'Item')
});
}
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]];
// });
return filters;
}
item_html(company) {
company._name = encodeURI(company.company_name);
const encoded_name = company._name;
const title = strip_html(company.company_name);
const _class = !company[this.imageFieldName] ? 'no-image' : '';
const company_name = company['company_name'];
const route = `#Hub/Company/${company_name}`;
let image_html = company.company_logo ?
`<img src="${company.company_logo}"><span class="helper"></span>` :
`<div class="standard-image">${frappe.get_abbr(company.company_name)}</div>`;
let item_html = `
<div class="image-view-item">
<div class="image-view-header">
<div class="list-row-col list-subject ellipsis level">
<span class="level-item bold ellipsis" title="McGuffin">
<a href="${route}">${title}</a>
</span>
</div>
</div>
<div class="image-view-body">
<a data-name="${encoded_name}"
title="${encoded_name}"
href="${route}">
<div class="image-field ${_class}"
data-name="${encoded_name}">
</div>
</a>
</div>
</div>
`;
return item_html;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
export default class SubPage {
constructor(parent, options) {
this.$parent = $(parent);
this.make_wrapper(options);
// handle broken images after every render
if (this.render) {
this._render = this.render.bind(this);
this.render = (...args) => {
this._render(...args);
frappe.dom.handle_broken_images(this.$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();
}
empty() {
this.$wrapper.empty();
}
show() {
this.refresh();
this.$wrapper.show();
}
hide() {
this.$wrapper.hide();
}
}

View File

@ -0,0 +1,27 @@
import SubPage from './base_page';
import { get_item_card_container_html } from '../helpers';
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)
}
}

View File

@ -0,0 +1,21 @@
import SubPage from './base_page';
import { get_item_card_container_html } from '../helpers';
erpnext.hub.Favourites = class Favourites extends SubPage {
refresh() {
this.get_favourites()
.then(items => {
this.render(items);
});
}
get_favourites() {
return hub.call('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)
}
}

View File

@ -0,0 +1,41 @@
import SubPage from './base_page';
import { make_search_bar, get_item_card_container_html } from '../helpers';
erpnext.hub.Home = class Home extends SubPage {
make_wrapper() {
super.make_wrapper();
make_search_bar({
wrapper: this.$wrapper,
on_search: keyword => {
frappe.set_route('marketplace', 'search', keyword);
}
});
}
refresh() {
this.get_items_and_render();
}
get_items_and_render() {
this.$wrapper.find('.hub-card-container').empty();
this.get_data()
.then(data => {
this.render(data);
});
}
get_data() {
return hub.call('get_data_for_homepage', { country: frappe.defaults.get_user_default('country') });
}
render(data) {
let html = get_item_card_container_html(data.random_items, __('Explore'));
this.$wrapper.append(html);
if (data.items_by_country.length) {
html = get_item_card_container_html(data.items_by_country, __('Near you'));
this.$wrapper.append(html);
}
}
}

View File

@ -0,0 +1,327 @@
import SubPage from './base_page';
import { get_rating_html } from '../helpers';
erpnext.hub.Item = class Item extends SubPage {
make_wrapper() {
super.make_wrapper();
this.setup_events();
}
refresh() {
this.show_skeleton();
this.hub_item_code = frappe.get_route()[2];
this.own_item = false;
this.get_item(this.hub_item_code)
.then(item => {
this.own_item = item.hub_seller === hub.settings.company_email;
this.item = item;
this.render(item);
});
}
show_skeleton() {
const skeleton = `<div class="hub-item-container">
<div class="row">
<div class="col-md-3">
<div class="hub-item-skeleton-image"></div>
</div>
<div class="col-md-6">
<h2 class="hub-skeleton" style="width: 75%;">Name</h2>
<div class="text-muted">
<p class="hub-skeleton" style="width: 35%;">Details</p>
<p class="hub-skeleton" style="width: 50%;">Ratings</p>
</div>
<hr>
<div class="hub-item-description">
<p class="hub-skeleton">Desc</p>
<p class="hub-skeleton" style="width: 85%;">Desc</p>
</div>
</div>
</div>
</div>`;
this.$wrapper.html(skeleton);
}
setup_events() {
this.$wrapper.on('click', '.btn-contact-seller', () => {
const d = new frappe.ui.Dialog({
title: __('Send a message'),
fields: [
{
fieldname: 'to',
fieldtype: 'Read Only',
label: __('To'),
default: this.item.company
},
{
fieldtype: 'Text',
fieldname: 'message',
label: __('Message')
}
]
});
d.show();
});
}
get_item(hub_item_code) {
return hub.call('get_item_details', { hub_item_code });
}
render(item) {
const title = item.item_name || item.name;
const seller = item.company;
const who = __('Posted By {0}', [seller]);
const when = comment_when(item.creation);
const city = item.city ? item.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.average_rating);
const rating_count = item.no_of_ratings > 0 ? `${item.no_of_ratings} reviews` : __('No reviews yet');
let edit_buttons_html = '';
if(this.own_item) {
edit_buttons_html = `<div style="margin-top: 20px">
<button class="btn btn-secondary btn-default btn-xs margin-right edit-item">Edit Details</button>
<button class="btn btn-secondary btn-danger btn-xs unpublish">Unpublish</button>
</div>`;
}
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-8">
<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>
` : `<p>${__('No description')}<p>`
}
</div>
${edit_buttons_html}
</div>
<div class="col-md-1">
<div class="dropdown pull-right hub-item-dropdown">
<a class="dropdown-toggle btn btn-xs btn-default" data-toggle="dropdown">
<span class="caret"></span>
</a>
<ul class="dropdown-menu dropdown-right" role="menu">
<li><a>Edit Details</a></li>
<li><a>Unpublish</a></li>
</ul>
</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-8">
<div class="margin-bottom"><a href="#marketplace/seller/${seller}" class="bold">${seller}</a></div>
<button class="btn btn-xs btn-default text-muted btn-contact-seller">
${__('Contact Seller')}
</button>
</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);
if(this.own_item) {
this.bind_edit_buttons();
}
this.make_review_area();
this.get_reviews()
.then(reviews => {
this.reviews = reviews;
this.render_reviews(reviews);
});
}
bind_edit_buttons() {
this.edit_dialog = new frappe.ui.Dialog({
title: "Edit Your Product",
fields: []
});
this.$wrapper.find('.edit-item').on('click', this.on_edit.bind(this));
this.$wrapper.find('.unpublish').on('click', this.on_unpublish.bind(this));
}
on_edit() {
this.edit_dialog.show();
}
on_unpublish() {
if(!this.unpublish_dialog) {
this.unpublish_dialog = new frappe.ui.Dialog({
title: "Edit Your Product",
fields: []
});
}
this.unpublish_dialog.show();
}
make_review_area() {
this.comment_area = new frappe.ui.ReviewArea({
parent: this.$wrapper.find('.timeline-head').empty(),
mentions: [],
on_submit: (values) => {
values.user = frappe.session.user;
values.username = frappe.session.user_fullname;
hub.call('add_item_review', {
hub_item_code: this.hub_item_code,
review: JSON.stringify(values)
})
.then(review => {
this.reviews = this.reviews || [];
this.reviews.push(review);
this.render_reviews(this.reviews);
this.comment_area.reset();
});
}
});
}
get_reviews() {
return hub.call('get_item_reviews', { hub_item_code: this.hub_item_code }).catch(() => {});
}
render_reviews(reviews=[]) {
this.$wrapper.find('.timeline-items').empty();
reviews.sort((a, b) => {
if (a.modified > b.modified) {
return -1;
}
if (a.modified < b.modified) {
return 1;
}
return 0;
});
reviews.forEach(review => this.render_review(review));
}
render_review(review) {
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(review.rating);
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">&ndash;</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>`;
}
}

View File

@ -0,0 +1,10 @@
import SubPage from './base_page';
erpnext.hub.NotFound = class NotFound extends SubPage {
refresh() {
this.$wrapper.html(get_empty_state(
__('Sorry! I could not find what you were looking for.'),
`<button class="btn btn-default btn-xs" data-route="marketplace/home">${__('Back to home')}</button>`
));
}
}

View File

@ -0,0 +1,98 @@
import SubPage from './base_page';
erpnext.hub.Profile = class Profile extends SubPage {
make_wrapper() {
super.make_wrapper();
}
refresh() {
this.get_hub_seller_profile(this.keyword)
.then(profile => this.render(profile));
}
get_hub_seller_profile() {
return hub.call('get_hub_seller_profile', { hub_seller: hub.settings.company_email });
}
render(profile) {
const p = profile;
const content_by_log_type = this.get_content_by_log_type();
let activity_logs = (p.hub_seller_activity || []).sort((a, b) => {
return new Date(b.creation) - new Date(a.creation);
});
const timeline_items_html = activity_logs
.map(log => {
const stats = JSON.parse(log.stats);
const no_of_items = stats && stats.push_update || '';
const content = content_by_log_type[log.type];
const message = content.get_message(no_of_items);
const icon = content.icon;
return this.get_timeline_log_item(log.pretty_date, message, icon);
})
.join('');
const profile_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="${p.logo}">
</div>
</div>
<div class="col-md-6">
<h2>${p.company}</h2>
<div class="text-muted">
<p>${p.country}</p>
<p>${p.site_name}</p>
</div>
<hr>
<div class="hub-item-description">
${'description'
? `<p>${p.company_description}</p>`
: `<p>__('No description')</p`
}
</div>
</div>
</div>
<div class="timeline">
<div class="timeline-items">
${timeline_items_html}
</div>
</div>
</div>`;
this.$wrapper.html(profile_html);
}
get_timeline_log_item(pretty_date, message, icon) {
return `<div class="media timeline-item notification-content">
<div class="small">
<i class="octicon ${icon} fa-fw"></i>
<span title="Administrator"><b>${pretty_date}</b> ${message}</span>
</div>
</div>`;
}
get_content_by_log_type() {
return {
"Created": {
icon: 'octicon-heart',
get_message: () => 'Joined Marketplace'
},
"Items Publish": {
icon: 'octicon-bookmark',
get_message: (no_of_items) =>
`Published ${no_of_items} product${no_of_items > 1 ? 's' : ''} to Marketplace`
}
}
}
}

View File

@ -0,0 +1,228 @@
import SubPage from './base_page';
import { make_search_bar, get_item_card_container_html, get_local_item_card_html } from '../helpers';
erpnext.hub.Publish = class Publish extends SubPage {
make_wrapper() {
super.make_wrapper();
this.items_to_publish = [];
this.unpublished_items = [];
this.fetched_items = [];
frappe.realtime.on("items-sync", (data) => {
this.$wrapper.find('.progress-bar').css('width', data.progress_percent+'%');
if(data.progress_percent === 100 || data.progress_percent === '100') {
setTimeout(() => {
hub.settings.sync_in_progress = 0;
frappe.db.get_doc('Hub Settings')
.then(doc => {
hub.settings = doc;
this.refresh();
});
}, 500);
}
});
}
refresh() {
if(!hub.settings.sync_in_progress) {
this.make_publish_ready_state();
} else {
this.make_publish_in_progress_state();
}
}
make_publish_ready_state() {
this.$wrapper.empty();
this.$wrapper.append(this.get_publishing_header());
make_search_bar({
wrapper: this.$wrapper,
on_search: keyword => {
this.search_value = keyword;
this.get_items_and_render();
},
placeholder: __('Search Items')
});
this.setup_publishing_events();
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 = `<b>${__('Select Products to Publish')}</b>`;
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>`;
return $(`
<div class='subpage-title flex'>
<div>
${title_html}
${subtitle_html}
</div>
${publish_button_html}
</div>
`);
}
setup_publishing_events() {
this.$wrapper.find('.publish-items').on('click', () => {
this.publish_selected_items()
.then(this.refresh.bind(this))
});
this.$wrapper.on('click', '.hub-card', (e) => {
const $target = $(e.currentTarget);
$target.toggleClass('active');
// Get total items
const total_items = this.$wrapper.find('.hub-card.active').length;
let button_label;
if (total_items > 0) {
const more_than_one = total_items > 1;
button_label = __('Publish {0} item{1}', [total_items, more_than_one ? 's' : '']);
} else {
button_label = __('Publish');
}
this.$wrapper.find('.publish-items')
.text(button_label)
.prop('disabled', total_items === 0);
});
}
show_message(message) {
const $message = $(`<div class="subpage-message">
<p class="text-muted flex">
<span>
${message}
</span>
<i class="octicon octicon-x text-extra-muted"></i>
</p>
</div>`);
$message.find('.octicon-x').on('click', () => {
$message.remove();
});
this.$wrapper.prepend($message);
}
make_publish_in_progress_state() {
this.$wrapper.empty();
this.$wrapper.append(this.show_publish_progress());
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>`;
this.$wrapper.append(subtitle_html);
// Show search list with only desctiption, and don't set any events
make_search_bar({
wrapper: this.$wrapper,
on_search: keyword => {
this.search_value = keyword;
this.get_items_and_render();
},
placeholder: __('Search Items')
});
this.get_items_and_render();
}
show_publish_progress() {
const items_to_publish = this.items_to_publish.length
? this.items_to_publish
: JSON.parse(hub.settings.custom_data);
const $publish_progress = $(`<div class="sync-progress">
<p><b>${__(`Syncing ${items_to_publish.length} Products`)}</b></p>
<div class="progress">
<div class="progress-bar" style="width: 1%"></div>
</div>
</div>`);
const items_to_publish_container = $(get_item_card_container_html(
items_to_publish, '', get_local_item_card_html));
items_to_publish_container.find('.hub-card').addClass('active');
$publish_progress.append(items_to_publish_container);
return $publish_progress;
}
get_items_and_render(wrapper = this.$wrapper) {
wrapper.find('.results').remove();
const items = this.get_valid_items();
if(!items.then) {
this.render(items, wrapper);
} else {
items.then(r => {
this.fetched_items = r.message;
this.render(r.message, wrapper);
});
}
}
render(items, wrapper) {
const items_container = $(get_item_card_container_html(items, '', get_local_item_card_html));
items_container.addClass('results');
wrapper.append(items_container);
}
get_valid_items() {
if(this.unpublished_items.length) {
return this.unpublished_items;
}
return frappe.call(
'erpnext.hub_node.get_valid_items',
{
search_value: this.search_value
}
);
}
publish_selected_items() {
const item_codes_to_publish = [];
this.$wrapper.find('.hub-card.active').map(function () {
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;
return frappe.call(
'erpnext.hub_node.publish_selected_items',
{
items_to_publish: item_codes_to_publish
}
)
}
}

View File

@ -0,0 +1,23 @@
import SubPage from './base_page';
import { get_item_card_container_html } from '../helpers';
erpnext.hub.PublishedProducts = class PublishedProducts extends SubPage {
get_items_and_render() {
this.$wrapper.find('.hub-card-container').empty();
this.get_published_products()
.then(items => this.render(items));
}
refresh() {
this.get_items_and_render();
}
render(items) {
const items_container = $(get_item_card_container_html(items, __('Your Published Products')));
this.$wrapper.append(items_container);
}
get_published_products() {
return hub.call('get_items_by_seller', { hub_seller: hub.settings.company_email });
}
}

View File

@ -0,0 +1,110 @@
import SubPage from './base_page';
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');
});
});
}
}

View File

@ -0,0 +1,34 @@
import SubPage from './base_page';
import { make_search_bar, get_item_card_container_html } from '../helpers';
erpnext.hub.SearchPage = class SearchPage extends SubPage {
make_wrapper() {
super.make_wrapper();
make_search_bar({
wrapper: this.$wrapper,
on_search: keyword => {
frappe.set_route('marketplace', 'search', keyword);
}
});
}
refresh() {
this.keyword = frappe.get_route()[2] || '';
this.$wrapper.find('input').val(this.keyword);
this.get_items_by_keyword(this.keyword)
.then(items => this.render(items));
}
get_items_by_keyword(keyword) {
return hub.call('get_items_by_keyword', { keyword });
}
render(items) {
this.$wrapper.find('.hub-card-container').remove();
const title = this.keyword ? __('Search results for "{0}"', [this.keyword]) : '';
const html = get_item_card_container_html(items, title);
this.$wrapper.append(html);
}
}