[hub][vue] Publish Page
diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py
index 85693c7..c4a7925 100644
--- a/erpnext/hub_node/api.py
+++ b/erpnext/hub_node/api.py
@@ -39,6 +39,7 @@
item['doctype'] = 'Hub Item'
item['hub_seller'] = hub_seller
+ item.pop('attachments', None)
return items
@@ -71,24 +72,19 @@
if not len(items_to_publish):
frappe.throw('No items to publish')
- publishing_items = []
-
- for item_additional_info in items_to_publish:
- item_code = item_additional_info.get('item_code')
+ for item in items_to_publish:
+ item_code = item.get('item_code')
frappe.db.set_value('Item', item_code, 'publish_in_hub', 1)
frappe.get_doc({
'doctype': 'Hub Tracked Item',
'item_code': item_code,
- 'hub_category': item_additional_info.get('hub_category'),
- 'image_list': item_additional_info.get('image_list')
+ 'hub_category': item.get('hub_category'),
+ 'image_list': item.get('image_list')
}).insert()
- item_data = frappe.get_doc("Item", item_code).as_dict().update(item_additional_info)
- publishing_items.append(item_data)
-
- items = map_fields(publishing_items)
+ items = map_fields(items_to_publish)
try:
item_sync_preprocess()
diff --git a/erpnext/public/js/hub/components/EmptyState.vue b/erpnext/public/js/hub/components/EmptyState.vue
index 1f36c3c..c0ad971 100644
--- a/erpnext/public/js/hub/components/EmptyState.vue
+++ b/erpnext/public/js/hub/components/EmptyState.vue
@@ -25,7 +25,7 @@
.empty-state {
height: 500px;
- margin: 15px 0px;
+ margin: 15px;
}
.empty-state.bordered {
diff --git a/erpnext/public/js/hub/components/ItemCard.vue b/erpnext/public/js/hub/components/ItemCard.vue
index 7a7f821..4d72344 100644
--- a/erpnext/public/js/hub/components/ItemCard.vue
+++ b/erpnext/public/js/hub/components/ItemCard.vue
@@ -1,11 +1,18 @@
<template>
<div class="col-md-3 col-sm-4 col-xs-6 hub-card-container">
- <div class="hub-card is_local">
- <div class="hub-card-header flex">
+ <div class="hub-card is_local"
+ @click="on_click(item_id)"
+ >
+ <div class="hub-card-header flex justify-between">
<div>
<div class="hub-card-title ellipsis bold">{{ title }}</div>
<div class="hub-card-subtitle ellipsis text-muted" v-html='subtitle'></div>
</div>
+ <i v-if="allow_clear"
+ class="octicon octicon-x text-extra-muted"
+ @click="$emit('remove-item', item_id)"
+ >
+ </i>
</div>
<div class="hub-card-body">
<img class="hub-card-image" :src="item.image"/>
@@ -27,16 +34,93 @@
export default {
name: 'item-card',
- props: ['item', 'is_local'],
+ props: ['item', 'item_id_fieldname', 'on_click', 'allow_clear'],
computed: {
title() {
return this.item.item_name
},
subtitle() {
return comment_when(this.item.creation);
+ },
+ item_id() {
+ return this.item[this.item_id_fieldname];
}
}
}
</script>
-<style scoped></style>
+<style lang="less" scoped>
+ @import "../../../../../../frappe/frappe/public/less/variables.less";
+
+ .hub-card {
+ margin-bottom: 25px;
+ position: relative;
+ border: 1px solid @border-color;
+ border-radius: 4px;
+ overflow: hidden;
+
+ &:hover .hub-card-overlay {
+ display: block;
+ }
+
+ .octicon-x {
+ display: block;
+ font-size: 20px;
+ margin-left: 10px;
+ cursor: pointer;
+ }
+ }
+
+ .hub-card.closable {
+ .octicon-x {
+ display: block;
+ }
+ }
+
+ .hub-card.is-local {
+ &.active {
+ .hub-card-header {
+ background-color: #f4ffe5;
+ }
+ }
+ }
+
+ .hub-card-header {
+ position: relative;
+ padding: 12px 15px;
+ height: 60px;
+ border-bottom: 1px solid @border-color;
+ }
+
+ .hub-card-body {
+ position: relative;
+ height: 200px;
+ }
+
+ .hub-card-overlay {
+ display: none;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+
+ .hub-card-overlay-body {
+ position: relative;
+ height: 100%;
+ }
+
+ .hub-card-overlay-button {
+ position: absolute;
+ right: 15px;
+ bottom: 15px;
+ }
+
+ .hub-card-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+</style>
diff --git a/erpnext/public/js/hub/components/ItemCardsContainer.vue b/erpnext/public/js/hub/components/ItemCardsContainer.vue
index 01fd01e..0e75795 100644
--- a/erpnext/public/js/hub/components/ItemCardsContainer.vue
+++ b/erpnext/public/js/hub/components/ItemCardsContainer.vue
@@ -1,5 +1,5 @@
<template>
- <div>
+ <div class="item-cards-container">
<empty-state
v-if="items.length === 0"
:message="empty_state_message"
@@ -9,9 +9,12 @@
</empty-state>
<item-card
v-for="item in items"
- :key="item[item_id]"
+ :key="item[item_id_fieldname]"
:item="item"
- :is_local="is_local"
+ :item_id_fieldname="item_id_fieldname"
+ :on_click="on_click"
+ :allow_clear="editable"
+ @remove-item="$emit('remove-item', item[item_id_fieldname])"
>
</item-card>
</div>
@@ -24,28 +27,31 @@
export default {
name: 'item-cards-container',
props: {
- 'items': Array,
- 'is_local': Boolean,
+ items: Array,
+ item_id_fieldname: String,
+ on_click: Function,
+ editable: Boolean,
- 'empty_state_message': String,
- 'empty_state_height': Number,
- 'empty_state_bordered': Boolean
+ empty_state_message: String,
+ empty_state_height: Number,
+ empty_state_bordered: Boolean
},
components: {
ItemCard,
EmptyState
},
- computed: {
- item_id() {
- return this.is_local ? 'item_code' : 'hub_item_code';
- }
- },
watch: {
items() {
+ // TODO: handling doesn't work
frappe.dom.handle_broken_images($(this.$el));
}
}
}
</script>
-<style scoped></style>
+<style scoped>
+ .item-cards-container {
+ margin: 0 -15px;
+ overflow: overlay;
+ }
+</style>
diff --git a/erpnext/public/js/hub/components/PublishPage.vue b/erpnext/public/js/hub/components/PublishPage.vue
index 2eaf295..2068813 100644
--- a/erpnext/public/js/hub/components/PublishPage.vue
+++ b/erpnext/public/js/hub/components/PublishPage.vue
@@ -13,14 +13,19 @@
<h5>{{ page_title }}</h5>
<button class="btn btn-primary btn-sm publish-items"
- :disabled="no_selected_items">
+ :disabled="no_selected_items"
+ @click="publish_selected_items"
+ >
<span>{{ publish_button_text }}</span>
</button>
</div>
<item-cards-container
:items="selected_items"
- :is_local="true"
+ :item_id_fieldname="item_id_fieldname"
+ :editable="true"
+ @remove-item="remove_item_from_selection"
+
:empty_state_message="empty_state_message"
:empty_state_bordered="true"
:empty_state_height="80"
@@ -38,7 +43,8 @@
<item-cards-container
:items="valid_items"
- :is_local="true"
+ :item_id_fieldname="item_id_fieldname"
+ :on_click="show_publishing_dialog_for_item"
>
</item-cards-container>
</div>
@@ -48,6 +54,7 @@
import SearchInput from './SearchInput.vue';
import ItemCardsContainer from './ItemCardsContainer.vue';
import NotificationMessage from './NotificationMessage.vue';
+import { ItemPublishDialog } from './item_publish_dialog';
export default {
name: 'publish-page',
@@ -56,7 +63,9 @@
page_name: frappe.get_route()[1],
valid_items: [],
selected_items: [],
+ items_data_to_publish: {},
search_value: '',
+ item_id_fieldname: 'item_code',
// Constants
page_title: __('Publish Products'),
@@ -95,10 +104,20 @@
text = `Publish ${number} Products`;
}
return __(text);
- }
+ },
+
+ items_dict() {
+ let items_dict = {};
+ this.valid_items.map(item => {
+ items_dict[item[this.item_id_fieldname]] = item
+ })
+
+ return items_dict;
+ },
},
created() {
this.get_valid_items();
+ this.make_publishing_dialog();
},
methods: {
get_valid_items() {
@@ -113,8 +132,86 @@
})
},
+ publish_selected_items() {
+ frappe.call(
+ 'erpnext.hub_node.api.publish_selected_items',
+ {
+ items_to_publish: this.selected_items
+ }
+ )
+ .then((r) => {
+ this.selected_items = [];
+ return frappe.db.get_doc('Hub Settings');
+ })
+ .then(doc => {
+ hub.settings = doc;
+ this.add_last_sync_message();
+ });
+ },
+
+ add_last_sync_message() {
+ this.last_sync_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>.`);
+ },
+
clear_last_sync_message() {
this.last_sync_message = '';
+ },
+
+ remove_item_from_selection(item_code) {
+ this.selected_items = this.selected_items
+ .filter(item => item.item_code !== item_code);
+ },
+
+ make_publishing_dialog() {
+ this.item_publish_dialog = ItemPublishDialog(
+ {
+ fn: (values) => {
+ this.add_item_to_publish(values);
+ this.item_publish_dialog.hide();
+ }
+ },
+ {
+ fn: () => {
+ const values = this.item_publish_dialog.get_values(true);
+ this.update_items_data_to_publish(values);
+ }
+ }
+ );
+ },
+
+ add_item_to_publish(values) {
+ this.update_items_data_to_publish(values);
+
+ const item_code = values.item_code;
+ let item_doc = this.items_dict[item_code];
+
+ const item_to_publish = Object.assign({}, item_doc, values);
+ this.selected_items.push(item_to_publish);
+ },
+
+ update_items_data_to_publish(values) {
+ this.items_data_to_publish[values.item_code] = values;
+ },
+
+ show_publishing_dialog_for_item(item_code) {
+ let item_data = this.items_data_to_publish[item_code];
+ if(!item_data) { item_data = { item_code }; };
+
+ this.item_publish_dialog.clear();
+
+ const item_doc = this.items_dict[item_code];
+ if(item_doc) {
+ this.item_publish_dialog.fields_dict.image_list.set_data(
+ item_doc.attachments.map(attachment => attachment.file_url)
+ );
+ }
+
+ this.item_publish_dialog.set_values(item_data);
+ this.item_publish_dialog.show();
}
}
}
diff --git a/erpnext/public/js/hub/components/item_publish_dialog.js b/erpnext/public/js/hub/components/item_publish_dialog.js
index a5d312c..e49ba53 100644
--- a/erpnext/public/js/hub/components/item_publish_dialog.js
+++ b/erpnext/public/js/hub/components/item_publish_dialog.js
@@ -1,54 +1,42 @@
function ItemPublishDialog(primary_action, secondary_action) {
- let dialog = new frappe.ui.Dialog({
- title: __('Edit Publishing Details'),
- fields: [
- {
- "label": "Item Code",
- "fieldname": "item_code",
- "fieldtype": "Data",
- "read_only": 1
- },
- {
- "label": "Hub Category",
- "fieldname": "hub_category",
- "fieldtype": "Autocomplete",
- "options": [],
- "reqd": 1
- },
- {
- "label": "Images",
- "fieldname": "image_list",
- "fieldtype": "MultiSelect",
- "options": [],
- "reqd": 1
- }
- ],
- primary_action_label: primary_action.label || __('Set Details'),
- primary_action: primary_action.fn,
- secondary_action: secondary_action.fn
- });
+ let dialog = new frappe.ui.Dialog({
+ title: __('Edit Publishing Details'),
+ fields: [
+ {
+ "label": "Item Code",
+ "fieldname": "item_code",
+ "fieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "label": "Hub Category",
+ "fieldname": "hub_category",
+ "fieldtype": "Autocomplete",
+ "options": [],
+ "reqd": 1
+ },
+ {
+ "label": "Images",
+ "fieldname": "image_list",
+ "fieldtype": "MultiSelect",
+ "options": [],
+ "reqd": 1
+ }
+ ],
+ primary_action_label: primary_action.label || __('Set Details'),
+ primary_action: primary_action.fn,
+ secondary_action: secondary_action.fn
+ });
+ hub.call('get_categories')
+ .then(categories => {
+ categories = categories.map(d => d.name);
+ dialog.fields_dict.hub_category.set_data(categories);
+ });
- function set_hub_category_options(data) {
- dialog.fields_dict.hub_category.set_data(
- data.map(d => d.name)
- );
- }
-
- const hub_call_key = 'get_categories{}';
- const categories_cache = erpnext.hub.cache[hub_call_key];
-
- if(categories_cache) {
- set_hub_category_options(categories_cache);
- }
-
- erpnext.hub.on(`response:${hub_call_key}`, (data) => {
- set_hub_category_options(data.response);
- });
-
- return dialog;
+ return dialog;
}
export {
- ItemPublishDialog
+ ItemPublishDialog
}
diff --git a/erpnext/public/js/hub/components/publishing_area.js b/erpnext/public/js/hub/components/publishing_area.js
deleted file mode 100644
index d510ad4..0000000
--- a/erpnext/public/js/hub/components/publishing_area.js
+++ /dev/null
@@ -1,37 +0,0 @@
-function get_publishing_header() {
- const title_html = `<h5>${__('Select Products to Publish')}</h5>`;
-
- 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" disabled>
- <i class="visible-xs octicon octicon-check"></i>
- <span class="hidden-xs">${__('Publish')}</span>
- </button>`;
-
- return $(`
- <div class="publish-area empty">
- <div class="publish-area-head">
- ${title_html}
- ${publish_button_html}
- </div>
- <div id="vue-area"></div>
- <div class="empty-items-container flex align-center flex-column justify-center">
- <p class="text-muted">${__('No Items Selected')}</p>
- </div>
- <div class="row hub-items-container selected-items"></div>
- </div>
-
- <div class='subpage-title flex'>
- <div>
- ${subtitle_html}
- </div>
- </div>
- `);
-}
-
-export {
- get_publishing_header
-}
diff --git a/erpnext/public/js/hub/pages/publish.js b/erpnext/public/js/hub/pages/publish.js
index 725fdc8..59b6c38 100644
--- a/erpnext/public/js/hub/pages/publish.js
+++ b/erpnext/public/js/hub/pages/publish.js
@@ -1,11 +1,3 @@
-import SubPage from './subpage';
-import { get_item_card_container_html } from '../components/items_container';
-import { get_local_item_card_html } from '../components/item_card';
-import { make_search_bar } from '../components/search_bar';
-import { get_publishing_header } from '../components/publishing_area';
-import { ItemPublishDialog } from '../components/item_publish_dialog';
-
-
import PublishPage from '../components/PublishPage.vue';
erpnext.hub.Publish = class Publish {
@@ -25,259 +17,4 @@
$('[data-page-name="publish"]').hide();
}
-
-
-
-
-
- // this.items_data_to_publish = {};
- // this.unpublished_items = [];
- // this.fetched_items = [];
- // this.fetched_items_dict = {};
-
-
- show_message(message) {
- this.$wrapper.prepend(NotificationMessage(message));
- }
-
- 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(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();
- this.show_last_sync_message();
- this.get_items_and_render();
- }
-
- // show_last_sync_message() {
- // 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>.`);
- // }
- // }
-
- setup_publishing_events() {
- this.$wrapper.find('.publish-items').on('click', () => {
- this.publish_selected_items()
- .then(this.refresh.bind(this))
- });
-
- this.selected_items_container = this.$wrapper.find('.selected-items');
-
- this.$current_selected_card = null;
-
- this.make_publishing_dialog();
-
- this.$wrapper.on('click', '[data-route]', (e) => {
- e.stopPropagation();
- const $target = $(e.currentTarget);
- const route = $target.data().route;
- frappe.set_route(route);
- });
-
- this.$wrapper.on('click', '.hub-card', (e) => {
- const $target = $(e.currentTarget);
- const item_code = $target.attr('data-id');
- this.show_publishing_dialog_for_item(item_code);
- this.$current_selected_card = $target.parent();
- });
- }
-
- make_publishing_dialog() {
- this.item_publish_dialog = ItemPublishDialog(
- {
- fn: (values) => {
- this.add_item_to_publish(values);
- this.item_publish_dialog.hide();
- }
- },
- {
- fn: () => {
- const values = this.item_publish_dialog.get_values(true);
- this.update_items_data_to_publish(values);
- }
- }
- );
- }
-
- add_item_to_publish(values) {
- this.update_items_data_to_publish(values);
- this.select_current_card()
- }
-
- update_items_data_to_publish(values) {
- // Add item additional data
- this.items_data_to_publish[values.item_code] = values;
- }
-
- select_current_card() {
- this.$current_selected_card.appendTo(this.selected_items_container);
- this.$current_selected_card.find('.hub-card').toggleClass('active');
-
- this.update_selected_items_count();
- }
-
- show_publishing_dialog_for_item(item_code) {
- let item_data = this.items_data_to_publish[item_code];
-
- if(!item_data) { item_data = { item_code }; };
-
- this.item_publish_dialog.clear();
-
- const item_doc = this.fetched_items_dict[item_code];
- if(item_doc) {
- this.item_publish_dialog.fields_dict.image_list.set_data(
- item_doc.attachments.map(attachment => attachment.file_url)
- );
- }
-
- this.item_publish_dialog.set_values(item_data);
- this.item_publish_dialog.show();
- }
-
- update_selected_items_count() {
- const total_items = this.$wrapper.find('.hub-card.active').length;
-
- const is_empty = total_items === 0;
-
- 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', is_empty);
-
- this.$wrapper.find('.publish-area').toggleClass('empty', is_empty);
- this.$wrapper.find('.publish-area').toggleClass('filled', !is_empty);
- }
-
- 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 description, 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_data_to_publish.length
- ? this.items_data_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);
-
- items.map(item => {
- this.fetched_items_dict[item.item_code] = item;
- });
-
- // remove the items which doesn't have a valid image
- setTimeout(() => {
- items_container.find('.no-image').each(function() {
- $(this).closest('.hub-card-container').remove();
- });
- }, 1000);
- }
-
- get_valid_items() {
- if(this.unpublished_items.length) {
- return this.unpublished_items;
- }
- return frappe.call(
- 'erpnext.hub_node.api.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"));
- });
-
- // Retrieve item data
- const items_data_to_publish = item_codes_to_publish.map(item_code => this.items_data_to_publish[item_code])
-
- return frappe.call(
- 'erpnext.hub_node.api.publish_selected_items',
- {
- items_to_publish: items_data_to_publish
- }
- )
- }
}
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index 5fac085..bcf57f4 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -25,16 +25,6 @@
font-size: @text-medium;
}
- // .btn-primary {
- // background-color: #89da28;
- // border-color: #61ca23;
- // }
-
- // .btn-primary:hover {
- // background-color: #61ca23;
- // border-color: #59b81c;
- // }
-
.progress-bar {
background-color: #89da28;
}