Merge branch 'hub-redesign' of https://github.com/frappe/erpnext into hub-redesign
diff --git a/erpnext/public/js/hub/PageContainer.vue b/erpnext/public/js/hub/PageContainer.vue
index 456da9a..ca4c63a 100644
--- a/erpnext/public/js/hub/PageContainer.vue
+++ b/erpnext/public/js/hub/PageContainer.vue
@@ -10,11 +10,18 @@
import Category from './pages/Category.vue';
import Search from './pages/Search.vue';
import PublishedProducts from './pages/PublishedProducts.vue';
+import Buying from './pages/Buying.vue';
+import BuyingMessages from './pages/BuyingMessages.vue';
const route_map = {
'marketplace/home': Home,
'marketplace/saved-products': SavedProducts,
- 'marketplace/publish': Publish
+ 'marketplace/my-products': PublishedProducts,
+ 'marketplace/publish': Publish,
+ 'marketplace/category/:category': Category,
+ 'marketplace/search/:keyword': Search,
+ 'marketplace/buying': Buying,
+ 'marketplace/buying/:item': BuyingMessages
}
export default {
@@ -33,7 +40,41 @@
this.current_page = this.get_current_page();
},
get_current_page() {
- return route_map[frappe.get_route_str()];
+ const curr_route = frappe.get_route_str();
+ let route = Object.keys(route_map).filter(route => route == curr_route)[0];
+
+ if (!route) {
+ // find route by matching it with dynamic part
+ const curr_route_parts = curr_route.split('/');
+ const weighted_routes = Object.keys(route_map)
+ .map(route_str => route_str.split('/'))
+ .filter(route_parts => route_parts.length === curr_route_parts.length)
+ .reduce((obj, route_parts) => {
+ const key = route_parts.join('/');
+ let weight = 0;
+ route_parts.forEach((part, i) => {
+ const curr_route_part = curr_route_parts[i];
+ if (part === curr_route_part || curr_route_part.includes(':')) {
+ weight += 1;
+ }
+ });
+
+ obj[key] = weight;
+ return obj;
+ }, {});
+
+ // get the route with the highest weight
+ let weight = 0
+ for (let key in weighted_routes) {
+ const route_weight = weighted_routes[key];
+ if (route_weight > weight) {
+ route = key;
+ weight = route_weight;
+ }
+ }
+ }
+
+ return route_map[route];
}
}
}
diff --git a/erpnext/public/js/hub/components/CommentInput.vue b/erpnext/public/js/hub/components/CommentInput.vue
new file mode 100644
index 0000000..cc430d0
--- /dev/null
+++ b/erpnext/public/js/hub/components/CommentInput.vue
@@ -0,0 +1,38 @@
+<template>
+ <div>
+ <div ref="comment-input"></div>
+ <div class="level">
+ <div class="level-left">
+ <span class="text-muted">{{ __('Ctrl + Enter to submit') }}</span>
+ </div>
+ <div class="level-right">
+ <button class="btn btn-primary btn-xs" @click="submit_input">{{ __('Submit') }}</button>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+export default {
+ mounted() {
+ this.make_input();
+ },
+ methods: {
+ make_input() {
+ this.message_input = new frappe.ui.CommentArea({
+ parent: this.$refs['comment-input'],
+ on_submit: (message) => {
+ this.message_input.reset();
+ this.$emit('change', message);
+ },
+ no_wrapper: true
+ });
+ },
+ submit_input() {
+ if (!this.message_input) return;
+ const value = this.message_input.val();
+ if (!value) return;
+ this.message_input.submit();
+ }
+ }
+}
+</script>
diff --git a/erpnext/public/js/hub/components/ItemCard.vue b/erpnext/public/js/hub/components/ItemCard.vue
index 7975868..f34fddc 100644
--- a/erpnext/public/js/hub/components/ItemCard.vue
+++ b/erpnext/public/js/hub/components/ItemCard.vue
@@ -15,7 +15,7 @@
</i>
</div>
<div class="hub-card-body">
- <img class="hub-card-image" :src="item.image"/>
+ <img class="hub-card-image" v-img-src="item.image"/>
<div class="hub-card-overlay">
<div v-if="is_local" class="hub-card-overlay-body">
<div class="hub-card-overlay-button">
diff --git a/erpnext/public/js/hub/components/ItemCardsContainer.vue b/erpnext/public/js/hub/components/ItemCardsContainer.vue
index 5af82de..748c272 100644
--- a/erpnext/public/js/hub/components/ItemCardsContainer.vue
+++ b/erpnext/public/js/hub/components/ItemCardsContainer.vue
@@ -5,8 +5,7 @@
:message="empty_state_message"
:bordered="true"
:height="80"
- >
- </empty-state>
+ />
<item-card
v-for="item in items"
:key="container_name + '_' +item[item_id_fieldname]"
diff --git a/erpnext/public/js/hub/components/ItemListCard.vue b/erpnext/public/js/hub/components/ItemListCard.vue
new file mode 100644
index 0000000..4c68100
--- /dev/null
+++ b/erpnext/public/js/hub/components/ItemListCard.vue
@@ -0,0 +1,23 @@
+<template>
+ <div class="hub-list-item" :data-route="item.route">
+ <div class="hub-list-left">
+ <img class="hub-list-image" v-img-src="item.image">
+ <div class="hub-list-body ellipsis">
+ <div class="hub-list-title">{{item.item_name}}</div>
+ <div class="hub-list-subtitle ellipsis">
+ <span>{{message.sender}}: </span>
+ <span>{{message.content}}</span>
+
+ </div>
+ </div>
+ </div>
+ <div class="hub-list-right">
+ <span class="text-muted" v-html="frappe.datetime.comment_when(message.creation, true)" />
+ </div>
+ </div>
+</template>
+<script>
+export default {
+ props: ['item', 'message']
+}
+</script>
diff --git a/erpnext/public/js/hub/components/SearchInput.vue b/erpnext/public/js/hub/components/SearchInput.vue
index 6647b15..4b1ce6e 100644
--- a/erpnext/public/js/hub/components/SearchInput.vue
+++ b/erpnext/public/js/hub/components/SearchInput.vue
@@ -5,7 +5,7 @@
class="form-control"
:placeholder="placeholder"
:value="value"
- @input="on_input">
+ @keydown.enter="on_input">
</div>
</template>
@@ -20,13 +20,7 @@
on_input(event) {
this.$emit('input', event.target.value);
this.on_search();
-
- // TODO: Debouncing doesn't fire search
- // frappe.utils.debounce(this.on_search, 500);
}
}
};
</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/erpnext/public/js/hub/components/SectionHeader.vue b/erpnext/public/js/hub/components/SectionHeader.vue
new file mode 100644
index 0000000..05a2f83
--- /dev/null
+++ b/erpnext/public/js/hub/components/SectionHeader.vue
@@ -0,0 +1,3 @@
+<template>
+ <div class="hub-items-header level"><slot></slot></div>
+</template>
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 00b6aee..10a55aa 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -1,4 +1,5 @@
import Vue from 'vue/dist/vue.js';
+import './vue-plugins';
// pages
import './pages/item';
@@ -148,12 +149,12 @@
make_body() {
this.$body = this.$parent.find('.layout-main-section');
- // this.$page_container = $('<div class="hub-page-container">').appendTo(this.$body);
+ this.$page_container = $('<div class="hub-page-container">').appendTo(this.$body);
- // new Vue({
- // el: '.hub-page-container',
- // render: h => h(PageContainer)
- // });
+ new Vue({
+ el: '.hub-page-container',
+ render: h => h(PageContainer)
+ });
erpnext.hub.on('seller-registered', () => {
this.registered = 1;
@@ -174,6 +175,10 @@
}
refresh() {
+
+ }
+
+ _refresh() {
const route = frappe.get_route();
this.subpages = this.subpages || {};
diff --git a/erpnext/public/js/hub/pages/Buying.vue b/erpnext/public/js/hub/pages/Buying.vue
new file mode 100644
index 0000000..7a783bc
--- /dev/null
+++ b/erpnext/public/js/hub/pages/Buying.vue
@@ -0,0 +1,46 @@
+<template>
+ <div>
+ <section-header>
+ <h4>{{ __('Buying') }}</h4>
+ </section-header>
+ <div class="row">
+ <div class="col-md-7"
+ v-for="item of items"
+ :key="item.name"
+ >
+ <item-list-card
+ :item="item"
+ :message="item.recent_message"
+ v-route="'marketplace/buying/' + item.name"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+import SectionHeader from '../components/SectionHeader.vue';
+import ItemListCard from '../components/ItemListCard.vue';
+
+export default {
+ components: {
+ SectionHeader,
+ ItemListCard
+ },
+ data() {
+ return {
+ items: []
+ }
+ },
+ created() {
+ this.get_items_for_messages()
+ .then(items => {
+ this.items = items;
+ });
+ },
+ methods: {
+ get_items_for_messages() {
+ return hub.call('get_buying_items_for_messages', {}, 'action:send_message');
+ }
+ }
+}
+</script>
diff --git a/erpnext/public/js/hub/pages/BuyingMessages.vue b/erpnext/public/js/hub/pages/BuyingMessages.vue
new file mode 100644
index 0000000..c718647
--- /dev/null
+++ b/erpnext/public/js/hub/pages/BuyingMessages.vue
@@ -0,0 +1,88 @@
+<template>
+ <div v-if="item_details">
+
+ <section-header>
+ <div class="flex flex-column margin-bottom">
+ <h4>{{ item_details.item_name }}</h4>
+ <span class="text-muted">{{ item_details.company }}</span>
+ </div>
+ </section-header>
+ <div class="row">
+ <div class="col-md-7">
+ <div class="message-container">
+ <div class="message-list">
+ <div class="level margin-bottom" v-for="message in messages" :key="message.name">
+ <div class="level-left ellipsis" style="width: 80%;">
+ <div v-html="frappe.avatar(message.sender)" />
+ <div style="white-space: normal;" v-html="message.content" />
+ </div>
+ <div class="level-right text-muted" v-html="frappe.datetime.comment_when(message.creation, true)" />
+ </div>
+ </div>
+ <div class="message-input">
+ <comment-input @change="send_message" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+import SectionHeader from '../components/SectionHeader.vue';
+import CommentInput from '../components/CommentInput.vue';
+import ItemListCard from '../components/ItemListCard.vue';
+
+export default {
+ components: {
+ SectionHeader,
+ CommentInput,
+ ItemListCard
+ },
+ data() {
+ return {
+ item_details: null,
+ messages: []
+ }
+ },
+ created() {
+ const hub_item_code = this.get_hub_item_code();
+ this.get_item_details(hub_item_code)
+ .then(item_details => {
+ this.item_details = item_details;
+ this.get_messages(item_details)
+ .then(messages => {
+ this.messages = messages;
+ });
+ });
+ },
+ methods: {
+ send_message(message) {
+ this.messages.push({
+ sender: hub.settings.company_email,
+ content: message,
+ creation: Date.now(),
+ name: frappe.utils.get_random(6)
+ });
+ hub.call('send_message', {
+ from_seller: hub.settings.company_email,
+ to_seller: this.item_details.hub_seller,
+ hub_item: this.item_details.hub_item_code,
+ message
+ });
+ },
+ get_item_details(hub_item_code) {
+ return hub.call('get_item_details', { hub_item_code })
+ },
+ get_messages() {
+ if (!this.item_details) return [];
+ return hub.call('get_messages', {
+ against_seller: this.item_details.hub_seller,
+ against_item: this.item_details.hub_item_code
+ });
+ },
+ get_hub_item_code() {
+ return frappe.get_route()[2];
+ }
+ }
+}
+</script>
diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue
index e91fb38..1d1973c 100644
--- a/erpnext/public/js/hub/pages/Home.vue
+++ b/erpnext/public/js/hub/pages/Home.vue
@@ -7,36 +7,34 @@
:placeholder="search_placeholder"
:on_search="set_search_route"
v-model="search_value"
- >
- </search-input>
+ />
- <div v-for="section in sections"
- :key="section.title"
- >
- <div class="hub-items-header margin-bottom level">
+ <div v-for="section in sections" :key="section.title">
+
+ <section-header>
<h4>{{ section.title }}</h4>
<p :data-route="'marketplace/category/' + section.title">{{ 'See All' }}</p>
- </div>
+ </section-header>
<item-cards-container
:container_name="section.title"
:items="section.items"
:item_id_fieldname="item_id_fieldname"
:on_click="go_to_item_details_page"
- >
- </item-cards-container>
+ />
</div>
-
</div>
</template>
<script>
import SearchInput from '../components/SearchInput.vue';
+import SectionHeader from '../components/SectionHeader.vue';
import ItemCardsContainer from '../components/ItemCardsContainer.vue';
export default {
name: 'home-page',
components: {
+ SectionHeader,
SearchInput,
ItemCardsContainer
},
diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js
new file mode 100644
index 0000000..5555945
--- /dev/null
+++ b/erpnext/public/js/hub/vue-plugins.js
@@ -0,0 +1,44 @@
+import Vue from 'vue/dist/vue.js';
+Vue.prototype.__ = window.__;
+Vue.prototype.frappe = window.frappe;
+
+Vue.directive('route', {
+ bind(el, binding) {
+ const route = binding.value;
+ el.classList.add('cursor-pointer');
+ el.addEventListener('click', () => frappe.set_route(route));
+ },
+ unbind(el) {
+ el.classList.remove('cursor-pointer');
+ }
+});
+
+const handleImage = (el, src) => {
+ let img = new Image();
+ // add loading class
+ el.src = '';
+ el.classList.add('img-loading');
+
+ img.onload = () => {
+ // image loaded, remove loading class
+ el.classList.remove('img-loading');
+ // set src
+ el.src = src;
+ }
+ img.onerror = () => {
+ el.classList.remove('img-loading');
+ el.classList.add('no-image');
+ el.src = null;
+ }
+ img.src = src;
+}
+
+Vue.directive('img-src', {
+ bind(el, binding) {
+ handleImage(el, binding.value);
+ },
+ update(el, binding) {
+ if (binding.value === binding.oldValue) return;
+ handleImage(el, binding.value);
+ }
+});
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index 779d45d..4cbe351 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -331,11 +331,10 @@
border-radius: 3px;
height: calc(100vh - 300px);
justify-content: space-between;
+ padding: 15px;
}
.message-list {
- padding-top: 15px;
- padding-bottom: 15px;
overflow: scroll;
}
}