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