Break marketplace.js into multiple files
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index ed4ebab..7bcf99b 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -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",
diff --git a/erpnext/public/js/hub/helpers.js b/erpnext/public/js/hub/helpers.js
new file mode 100644
index 0000000..22e35c3
--- /dev/null
+++ b/erpnext/public/js/hub/helpers.js
@@ -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,
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/hub_call.js b/erpnext/public/js/hub/hub_call.js
new file mode 100644
index 0000000..e0fead3
--- /dev/null
+++ b/erpnext/public/js/hub/hub_call.js
@@ -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)
+ });
+}
diff --git a/erpnext/public/js/hub/hub_factory.js b/erpnext/public/js/hub/hub_factory.js
index c94edf4..b073720 100644
--- a/erpnext/public/js/hub/hub_factory.js
+++ b/erpnext/public/js/hub/hub_factory.js
@@ -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 @@
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.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;
- }
-}
diff --git a/erpnext/public/js/hub/hub_form.js b/erpnext/public/js/hub/hub_form.js
deleted file mode 100644
index 9287e6d..0000000
--- a/erpnext/public/js/hub/hub_form.js
+++ /dev/null
@@ -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">–</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);
- }
-}
diff --git a/erpnext/public/js/hub/hub_listing.js b/erpnext/public/js/hub/hub_listing.js
deleted file mode 100644
index 368c723..0000000
--- a/erpnext/public/js/hub/hub_listing.js
+++ /dev/null
@@ -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;
- }
-
-};
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 31cf5da..ab0d4f7 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -1,3 +1,19 @@
+// pages
+import './pages/home';
+import './pages/favourites';
+import './pages/search';
+import './pages/category';
+import './pages/item';
+import './pages/register';
+import './pages/profile';
+import './pages/publish';
+import './pages/published_products';
+import './pages/not_found';
+
+// helpers
+import './helpers';
+import './hub_call';
+
frappe.provide('hub');
frappe.provide('erpnext.hub');
@@ -180,1119 +196,3 @@
this.subpages[route[1]].show();
}
}
-
-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();
- }
-}
-
-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);
- }
- }
-}
-
-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)
- }
-}
-
-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.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);
- }
-}
-
-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">–</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.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`
- }
- }
- }
-}
-
-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
- }
- )
- }
-}
-
-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 });
- }
-}
-
-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>`
- ));
- }
-}
-
-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));
-}
-
-// caching
-
-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)
- });
-}
diff --git a/erpnext/public/js/hub/pages/base_page.js b/erpnext/public/js/hub/pages/base_page.js
new file mode 100644
index 0000000..70248da
--- /dev/null
+++ b/erpnext/public/js/hub/pages/base_page.js
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/category.js b/erpnext/public/js/hub/pages/category.js
new file mode 100644
index 0000000..21dcb32
--- /dev/null
+++ b/erpnext/public/js/hub/pages/category.js
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/favourites.js b/erpnext/public/js/hub/pages/favourites.js
new file mode 100644
index 0000000..9605eb1
--- /dev/null
+++ b/erpnext/public/js/hub/pages/favourites.js
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/home.js b/erpnext/public/js/hub/pages/home.js
new file mode 100644
index 0000000..ff37e81
--- /dev/null
+++ b/erpnext/public/js/hub/pages/home.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/item.js b/erpnext/public/js/hub/pages/item.js
new file mode 100644
index 0000000..dabddea
--- /dev/null
+++ b/erpnext/public/js/hub/pages/item.js
@@ -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">–</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>`;
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/not_found.js b/erpnext/public/js/hub/pages/not_found.js
new file mode 100644
index 0000000..a83d881
--- /dev/null
+++ b/erpnext/public/js/hub/pages/not_found.js
@@ -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>`
+ ));
+ }
+}
diff --git a/erpnext/public/js/hub/pages/profile.js b/erpnext/public/js/hub/pages/profile.js
new file mode 100644
index 0000000..6dd1f87
--- /dev/null
+++ b/erpnext/public/js/hub/pages/profile.js
@@ -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`
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/publish.js b/erpnext/public/js/hub/pages/publish.js
new file mode 100644
index 0000000..6e8caab
--- /dev/null
+++ b/erpnext/public/js/hub/pages/publish.js
@@ -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
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/published_products.js b/erpnext/public/js/hub/pages/published_products.js
new file mode 100644
index 0000000..f21c6fa
--- /dev/null
+++ b/erpnext/public/js/hub/pages/published_products.js
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/register.js b/erpnext/public/js/hub/pages/register.js
new file mode 100644
index 0000000..d8966f1
--- /dev/null
+++ b/erpnext/public/js/hub/pages/register.js
@@ -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');
+ });
+ });
+ }
+}
diff --git a/erpnext/public/js/hub/pages/search.js b/erpnext/public/js/hub/pages/search.js
new file mode 100644
index 0000000..276c9bc
--- /dev/null
+++ b/erpnext/public/js/hub/pages/search.js
@@ -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);
+ }
+}
\ No newline at end of file