feat(doctype-urls): pretty doctype urls and removed social JS code

This commit is contained in:
Rushabh Mehta 2020-11-12 14:55:21 +05:30
parent c826d489e4
commit 248e9cd7d1
30 changed files with 222 additions and 1931 deletions

View file

@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes', 'db_tables') + doctype_map_keys
'sitemap_routes', 'db_tables', 'doctype_name_map') + doctype_map_keys
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
@ -67,19 +67,17 @@ def clear_defaults_cache(user=None):
elif frappe.flags.in_install!="frappe":
frappe.cache().delete_key("defaults")
def clear_document_cache():
frappe.local.document_cache = {}
frappe.cache().delete_key("document_cache")
def clear_doctype_cache(doctype=None):
cache = frappe.cache()
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
del frappe.local.meta_cache[doctype]
for key in ('is_table', 'doctype_modules'):
for key in ('is_table', 'doctype_modules', 'doctype_name_map', 'document_cache'):
cache.delete_value(key)
frappe.local.document_cache = {}
def clear_single(dt):
for name in doctype_cache_keys:
cache.hdel(name, dt)
@ -101,9 +99,6 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()
def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'

View file

@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe

16
frappe/desk/utils.py Normal file
View file

@ -0,0 +1,16 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
@frappe.whitelist(allow_guest=True)
def get_doctype_name(name):
# translates the doctype name from url to name `sales-order` to `Sales Order`
def get_name_map():
name_map = {}
for d in frappe.get_all('DocType'):
name_map[d.name.lower().replace(' ', '-')] = d.name
return name_map
return frappe.cache().get_value('doctype_name_map', get_name_map).get(name, name)

View file

@ -85,7 +85,6 @@
"public/less/mobile.less",
"public/less/controls.less",
"public/less/chat.less",
"public/less/social.less",
"public/css/fonts/inter/inter.css",
"public/scss/desk.scss",
"node_modules/frappe-charts/dist/frappe-charts.min.css",
@ -217,7 +216,6 @@
"public/js/frappe/utils/rating_icons.html",
"public/js/frappe/chat.js",
"public/js/frappe/social/social_factory.js",
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
@ -303,7 +301,6 @@
"node_modules/frappe-datatable/dist/frappe-datatable.css"
],
"css/email.css": "public/less/email.less",
"js/social.min.js": "public/js/frappe/social/social_home.js",
"js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js",
"js/data_import_tools.min.js": "public/js/frappe/data_import/index.js",
"css/login.css": "public/scss/login.scss"

View file

@ -173,9 +173,6 @@ frappe.Application = Class.extend({
if (frappe.boot && localStorage.getItem("session_last_route")) {
frappe.set_route(localStorage.getItem("session_last_route"));
localStorage.removeItem("session_last_route");
} else if (frappe._cur_route) {
// go to the appropriate sub-path
frappe.set_route(frappe._cur_route);
} else {
// route to home page
frappe.route();

View file

@ -1007,7 +1007,7 @@ frappe.ui.form.Form = class FrappeForm {
return;
}
frappe.re_route[frappe.get_sub_path()] = 'Form/' + encodeURIComponent(this.doctype) + '/' + encodeURIComponent(name);
frappe.re_route[frappe.router.get_sub_path()] = 'Form/' + encodeURIComponent(this.doctype) + '/' + encodeURIComponent(name);
frappe.set_route('Form', this.doctype, name);
}

View file

@ -13,14 +13,13 @@ frappe.view_factory = {};
frappe.view_factories = [];
frappe.route_options = null;
frappe.route_hooks = {};
frappe._cur_route = null;
$(window).on('hashchange', function() {
// v1 style routing, route is in hash
if (window.location.hash) {
let sub_path = frappe.get_sub_path(window.location.hash);
let sub_path = frappe.router.get_sub_path(window.location.hash);
window.location.hash = '';
frappe.push_state(sub_path);
frappe.router.push_state(sub_path);
}
});
@ -33,7 +32,7 @@ window.addEventListener('popstate', (event) => {
$('body').on('click', 'a', function(e) {
let override = (e, route) => {
e.preventDefault();
frappe.push_state(frappe.get_sub_path(route));
frappe.router.push_state(frappe.router.get_sub_path(route));
return false;
};
@ -58,51 +57,83 @@ $('body').on('click', 'a', function(e) {
}
});
frappe.route = function() {
frappe.router = {
current_route: null,
doctype_names: {},
route() {
// resolve the route from the URL or hash
// translate it so the objects are well defined
// and render the page as required
// Application is not yet initiated
if (!frappe.app) return;
if (!frappe.app) return;
let sub_path = frappe.get_sub_path();
let sub_path = frappe.router.get_sub_path();
if (frappe.router.re_route(sub_path)) return;
if (frappe.re_route[sub_path] !== undefined) {
// after saving a doc, for example,
// "New DocType 1" and the renamed "TestDocType", both exist in history
// now if we try to go back,
// it doesn't allow us to go back to the one prior to "New DocType 1"
// Hence if this check is true, instead of changing location hash,
// we just do a back to go to the doc previous to the "New DocType 1"
var re_route_val = frappe.get_sub_path(frappe.re_route[sub_path]);
if (decodeURIComponent(re_route_val) === decodeURIComponent(sub_path)) {
window.history.back();
return;
} else {
frappe.set_route(re_route_val);
return;
}
}
frappe.router.translate_doctype_name().then(() => {
let route = frappe.router.current_route;
frappe._cur_route = sub_path;
frappe.router.set_history(route, sub_path);
let route = frappe.get_route();
if (route[0]) {
frappe.router.render_page(route);
} else {
// Show home
frappe.views.pageview.show('');
}
frappe.route_history.push(route);
frappe.router.set_title();
frappe.route.trigger('change');
});
},
// set title
frappe.route_titles[sub_path] = frappe._original_title || document.title;
translate_doctype_name() {
return new Promise((resolve) => {
let route = frappe.router.parse();
let factory = route[0].toLowerCase();
let done = () => {
route[1] = frappe.router.doctype_names[route[1]];
frappe.router.current_route = route;
resolve();
};
// hide open dialog
frappe.ui.hide_open_dialog();
if (['form', 'list', 'report', 'tree'].includes(factory)) {
// translate the doctype to its original name
if (frappe.router.doctype_names[route[1]]) {
done();
} else {
frappe.xcall('frappe.desk.utils.get_doctype_name', {name: route[1]}).then((data) => {
frappe.router.doctype_names[route[1]] = data;
done();
});
}
} else {
done();
}
});
},
set_history(route, sub_path) {
frappe.route_history.push(route);
frappe.route_titles[sub_path] = frappe._original_title || document.title;
frappe.ui.hide_open_dialog();
},
render_page(route) {
// create the page generator (factory) object and call `show`
// if there is no generator, render the `Page` object
// first the router needs to know if its a "page", "doctype", "workspace"
if (route[0]) {
const title_cased_route = frappe.utils.to_title_case(route[0]);
if (title_cased_route === 'Workspace') {
frappe.views.pageview.show('');
return;
}
if (route[1] && frappe.views[title_cased_route + "Factory"]) {
// has a view generator, generate!
if(!frappe.view_factory[title_cased_route]) {
if (!frappe.view_factory[title_cased_route]) {
frappe.view_factory[title_cased_route] = new frappe.views[title_cased_route + "Factory"]();
}
@ -114,44 +145,127 @@ frappe.route = function() {
frappe.views.pageview.show(route_name);
}
}
} else {
// Show home
frappe.views.pageview.show('');
},
re_route(sub_path) {
if (frappe.re_route[sub_path] !== undefined) {
// after saving a doc, for example,
// "New DocType 1" and the renamed "TestDocType", both exist in history
// now if we try to go back,
// it doesn't allow us to go back to the one prior to "New DocType 1"
// Hence if this check is true, instead of changing location hash,
// we just do a back to go to the doc previous to the "New DocType 1"
var re_route_val = frappe.router.get_sub_path(frappe.re_route[sub_path]);
if (decodeURIComponent(re_route_val) === decodeURIComponent(sub_path)) {
window.history.back();
return true;
} else {
frappe.set_route(re_route_val);
return true;
}
}
},
set_title(sub_path) {
if (frappe.route_titles[sub_path]) {
frappe.utils.set_title(frappe.route_titles[sub_path]);
} else {
setTimeout(function() {
frappe.route_titles[frappe.get_route_str()] = frappe._original_title || document.title;
}, 1000);
}
},
push_state(route) {
// change the URL and call the router
let url = route;
if (!route.startsWith('/app/')) {
url = `/app/${route}`;
}
if (window.location.pathname !== url) {
// cleanup any remenants of v1 routing
window.location.hash = '';
// push state so the browser looks fine
history.pushState(null, null, url);
// now process the route
frappe.router.route();
}
},
parse(route) {
route = frappe.router.get_sub_path_string(route).split('/');
route = $.map(route, frappe.router.decode_component);
frappe.router.set_route_options(route);
return route;
},
get_sub_path_string(route) {
// return clean sub_path from hash or url
// supports both v1 and v2 routing
if (!route) {
route = window.location.hash || window.location.pathname;
}
return frappe.router.strip_prefix(route);
},
strip_prefix(route) {
if (route.substr(0, 1)=='/') route = route.substr(1); // for /app/sub
if (route.startsWith('app')) route = route.substr(4); // for desk/sub
if (route.substr(0, 1)=='/') route = route.substr(1);
if (route.substr(0, 1)=='#') route = route.substr(1);
if (route.substr(0, 1)=='!') route = route.substr(1);
return route;
},
get_sub_path(route) {
var sub_path = frappe.router.get_sub_path_string(route);
route = $.map(sub_path.split('/'), frappe.router.decode_component).join('/');
return route;
},
set_route_options(route) {
// set query parameters as frappe.route_options
var last_part = route[route.length - 1];
if (last_part.indexOf("?") < last_part.indexOf("=")) {
// has ? followed by =
let parts = last_part.split("?");
// route should not contain string after ?
route[route.length - 1] = parts[0];
let query_params = frappe.utils.get_query_params(parts[1]);
frappe.route_options = $.extend(frappe.route_options || {}, query_params);
}
},
decode_component(r) {
try {
return decodeURIComponent(r);
} catch (e) {
if (e instanceof URIError) {
// legacy: not sure why URIError is ignored.
return r;
} else {
throw e;
}
}
},
slug(name) {
return name.toLowerCase().replace(/ /g, '-');
}
if (frappe.route_titles[sub_path]) {
frappe.utils.set_title(frappe.route_titles[sub_path]);
} else {
setTimeout(function() {
frappe.route_titles[frappe.get_route_str()] = frappe._original_title || document.title;
}, 1000);
}
frappe.route.trigger('change');
};
frappe.get_route = function(route) {
// for app
route = frappe.get_sub_path_string(route).split('/');
route = $.map(route, frappe._decode_str);
var parts = null;
var doc_name = route[route.length - 1];
// if the last part contains ? then check if it is valid query string
if (doc_name.indexOf("?") < doc_name.indexOf("=")) {
parts = doc_name.split("?");
route[route.length - 1] = parts[0];
} else {
parts = doc_name;
}
if (parts.length > 1) {
var query_params = frappe.utils.get_query_params(parts[1]);
frappe.route_options = $.extend(frappe.route_options || {}, query_params);
}
return route;
}
frappe.route = frappe.router.route;
frappe.get_route = () => frappe.router.current_route;
frappe.get_route_str = () => frappe.router.current_route.join('/');
frappe.get_prev_route = function() {
if (frappe.route_history && frappe.route_history.length > 1) {
@ -161,44 +275,6 @@ frappe.get_prev_route = function() {
}
}
frappe._decode_str = function(r) {
try {
return decodeURIComponent(r);
} catch (e) {
if (e instanceof URIError) {
return r;
} else {
throw e;
}
}
}
frappe.get_sub_path_string = function(route) {
// return clean sub_path from hash or url
// supports both v1 and v2 routing
if (!route) {
route = window.location.hash;
}
if (!route) {
route = window.location.pathname;
}
if (route.substr(0, 1)=='/') route = route.substr(1); // for /app/sub
if (route.startsWith('app')) route = route.substr(4); // for desk/sub
if (route.substr(0, 1)=='/') route = route.substr(1);
if (route.substr(0, 1)=='#') route = route.substr(1);
if (route.substr(0, 1)=='!') route = route.substr(1);
return route;
};
frappe.get_sub_path = frappe.get_route_str = function(route) {
var sub_path = frappe.get_sub_path_string(route);
route = $.map(sub_path.split('/'), frappe._decode_str).join('/');
return route;
};
frappe.set_route = function() {
return new Promise(resolve => {
@ -230,7 +306,7 @@ frappe.set_route = function() {
// window.location.hash = route;
// routing v2
frappe.push_state(route);
frappe.router.push_state(route);
}
// Set favicon (app.js)
@ -245,24 +321,10 @@ frappe.set_route = function() {
});
};
frappe.push_state = function (route) {
let url = `/app/${route}`;
if (window.location.pathname !== url) {
// cleanup any remenants of v1 routing
window.location.hash = '';
// push state so the browser looks fine
history.pushState(null, null, url);
// now process the route
frappe.route();
}
};
frappe.set_re_route = function() {
var tmp = frappe.get_sub_path();
var tmp = frappe.router.get_sub_path();
frappe.set_route.apply(null, arguments);
frappe.re_route[tmp] = frappe.get_sub_path();
frappe.re_route[tmp] = frappe.router.get_sub_path();
};
frappe.has_route_options = function() {

View file

@ -1,111 +0,0 @@
<template>
<div ref="social" class="social">
<keep-alive>
<component :is="current_page.component" v-bind="current_page.props"></component>
</keep-alive>
<image-viewer :src="preview_image_src" v-if="show_preview"></image-viewer>
</div>
</template>
<script>
import Wall from './pages/Wall.vue';
import Profile from './pages/Profile.vue';
import UserList from './pages/UserList.vue';
import NotFound from './components/NotFound.vue';
import ImageViewer from './components/ImageViewer.vue';
function get_route_map() {
return {
'social/home': {
'component': Wall,
'props': {}
},
'social/profile/*': {
'component': Profile,
'props': {
'user_id': frappe.get_route()[2],
'key': frappe.get_route()[2]
}
},
'social/users': {
'component': UserList,
'props': {}
},
'not_found': {
'component': NotFound,
}
}
}
export default {
components: {
ImageViewer
},
data() {
return {
current_page: this.get_current_page(),
show_preview: false,
preview_image_src: ''
}
},
created() {
this.$root.$on('show_preview', (src) => {
this.preview_image_src = src;
this.show_preview = true;
})
this.$root.$on('hide_preview', () => {
this.preview_image_src = '';
this.show_preview = false;
})
this.update_primary_action(frappe.get_route()[1])
},
mounted() {
frappe.route.on('change', () => {
if (frappe.get_route()[0] === 'social') {
this.set_current_page();
this.update_primary_action(frappe.get_route()[1])
frappe.utils.scroll_to(0);
$("body").attr("data-route", frappe.get_route_str());
}
});
frappe.ui.setup_like_popover($(this.$refs.social), '.likes', false);
},
methods: {
set_current_page() {
this.current_page = this.get_current_page();
},
update_primary_action(current_route) {
if (current_route === 'home') {
this.$root.page.set_title(__('Social'));
frappe.breadcrumbs.update();
this.$root.page.set_primary_action(__('Post'), () => {
frappe.social.post_dialog.show();
});
} else {
frappe.breadcrumbs.add({
type: 'Custom',
label: __('Social Home'),
route: '#social/home'
});
this.$root.page.clear_primary_action();
}
if (current_route === 'users') {
this.$root.page.set_title(__('Leaderboard'));
}
},
get_current_page() {
const route_map = get_route_map();
const route = frappe.get_route_str();
if (route_map[route]) {
return route_map[route];
} else {
return route_map[route.substring(0, route.lastIndexOf('/')) + '/*'] || route_map['not_found']
}
},
}
}
</script>

View file

@ -1,48 +0,0 @@
<template>
<div>
<div class="muted-title">{{ __('Upcoming Events') }}</div>
<div class="event" v-for="event in events" :key="event.name">
<span class="bold">{{ get_time(event.starts_on) }}</span>
<a @click="open_event(event)"> {{ event.subject }}</a>
</div>
<div class="event" v-if="!events.length">
{{ __('No Upcoming Events') }}
</div>
<div class="muted-title">{{ __('Chat') }}</div>
<a @click="open_chat">
{{ __('Open Chat') }}
</a>
</div>
</template>
<script>
export default {
data() {
return {
'events': []
}
},
created() {
this.get_events().then((events) => {
this.events = events
})
},
methods: {
get_events() {
const today = frappe.datetime.now_date();
return frappe.xcall('frappe.desk.doctype.event.event.get_events', {
start: today,
end: today
})
},
open_chat() {
setTimeout(frappe.chat.widget.toggle);
},
get_time(timestamp) {
return frappe.datetime.get_time(timestamp)
},
open_event(event) {
frappe.set_route('Form', 'Event', event.name);
}
}
}
</script>

View file

@ -1,66 +0,0 @@
<template>
<div class="backdrop">
<img
:src="src"
:class="{'psuedo-zoom': full_size}"
@dblclick="full_size = !full_size"
/>
<i class="fa fa-close close" @click="$root.$emit('hide_preview')"></i>
</div>
</template>
<script>
export default {
props: ['src'],
data() {
return {
full_size: false
}
},
created() {
document.addEventListener('keyup', this.close_preview_on_escape);
},
destroyed() {
document.removeEventListener('keyup', this.close_preview_on_escape);
},
methods: {
close_preview_on_escape(e) {
if (e.keyCode === 27) {
this.$root.$emit('hide_preview')
}
}
}
}
</script>
<style lang="less" scoped>
.backdrop {
position: fixed;
height: 100%;
width: 100%;
background: #0000006e;
z-index: 1030;
top: 0;
right: 0;
img {
margin: auto;
display: block;
width: 80%;
max-width: 700px;
padding-top: 100px;
}
.psuedo-zoom {
padding: 10px 0px;
width: auto;
height: 100vh;
max-width: initial;
}
.close {
position: absolute;
top: 15px;
right: 35px;
color: black;
font-size: 30px;
}
}
</style>

View file

@ -1,5 +0,0 @@
<template>
<div>
Not Found
</div>
</template>

View file

@ -1,227 +0,0 @@
<template>
<div class="post-card">
<div class="post-body" :class="{'pinned ': is_pinned}">
<div class="pull-right flex">
<div
class="pin-option"
v-if="is_pinned">
Globally Pinned
</div>
<post-dropdown-menu
class="post-dropdown-menu"
v-if="options.length"
:options="options"
/>
<transition name="fade">
<span
class="indicator blue"
v-if="!this.post.seen">
</span>
</transition>
</div>
<div class="user-avatar" v-html="user_avatar" @click="goto_profile(post.owner)"></div>
<a class="user-name" @click="goto_profile(post.owner)">{{ user_name }}</a>
<div class="text-muted" v-html="post_time"></div>
<div ref="content" class="content" v-html="post.content"></div>
</div>
<post-action
:liked_by="post.liked_by"
:comment_count="comments.length"
@toggle_comment="toggle_comment"
@toggle_like="toggle_like"
/>
<post-comment
v-if="show_comments"
class="post-comments"
:comments="comments"
@create_comment="create_comment"
/>
</div>
</template>
<script>
import PostAction from './PostAction.vue';
import PostComment from './PostComment.vue';
import PostDropdownMenu from './PostDropdownMenu.vue';
export default {
props: ['post'],
components: {
PostAction,
PostComment,
PostDropdownMenu
},
data() {
return {
user_avatar: frappe.avatar(this.post.owner, 'avatar-medium'),
post_time: comment_when(this.post.creation),
user_name: frappe.user_info(this.post.owner).fullname,
comment_count: 0,
comments: [],
show_comments: false,
is_globally_pinnable: frappe.user_roles.includes('System Manager') && frappe.social.is_home_page(),
is_pinnable: false,
is_user_post_owner: this.post.owner === frappe.session.user
}
},
computed: {
can_pin() {
return this.is_globally_pinnable || this.is_pinnable
},
is_pinned() {
return false && frappe.social.is_profile_page(this.post.owner)
|| this.post.is_globally_pinned && frappe.social.is_home_page()
},
options() {
const options = []
if (this.can_pin) {
if (this.is_pinned) {
options.push({
'label': __('Unpin'),
'action': this.toggle_pin
})
} else {
options.push({
'label': __('Pin Globally'),
'action': this.toggle_pin
})
}
}
if (this.is_user_post_owner) {
options.push({
'label': __('Delete'),
'action': this.delete_post
})
}
return options;
}
},
created() {
frappe.db.get_list('Post Comment', {
fields: ['name', 'content', 'owner', 'creation'],
order_by: 'creation desc',
filters: {
parent: this.post.name
}
}).then(comments => {
this.comments = comments;
})
if (!this.post.liked_by) {
this.$set(this.post, 'liked_by', '')
}
frappe.realtime.on('new_post_comment' + this.post.name, (comment) => {
this.comments = [comment].concat(this.comments);
})
frappe.realtime.on('update_liked_by' + this.post.name, this.update_liked_by)
frappe.realtime.on('delete_post' + this.post.name, () => {
this.$emit('delete-post')
})
this.$root.$on('user_image_updated', () => {
this.user_avatar = frappe.avatar(this.post.owner, 'avatar-medium')
})
},
mounted() {
this.$refs['content'].querySelectorAll('img').forEach((img) => {
img.addEventListener('click', () => {
this.$root.$emit('show_preview', img.src);
})
});
this.$refs['content'].querySelectorAll('a').forEach(link_element => {
// to open link in new tab
link_element.target = 'blank';
this.generate_preview(link_element);
})
},
methods: {
goto_profile(user) {
frappe.set_route('social', 'profile/' + user)
},
toggle_comment() {
this.show_comments = !this.show_comments
},
update_liked_by(liked_by) {
this.post.liked_by = liked_by;
},
toggle_like() {
frappe.xcall('frappe.social.doctype.post.post.toggle_like', {
post_name: this.post.name,
})
},
toggle_pin() {
if (this.is_globally_pinnable) {
frappe.db.set_value('Post', this.post.name, 'is_globally_pinned', cint(!this.is_pinned))
.then(res => this.post.is_globally_pinned = cint(res.message.is_globally_pinned))
}
if (this.is_pinnable) {
frappe.db.set_value('Post', this.post.name, 'is_pinned', cint(!this.is_pinned))
.then(res => this.post.is_pinned = cint(res.message.is_pinned))
}
},
create_comment(content) {
const comment = frappe.model.get_new_doc('Post Comment');
comment.content = content
comment.parent = this.post.name;
frappe.db.insert(comment);
},
delete_post() {
frappe.confirm(__("Are you sure you want to delete this post?"), () => {
frappe.dom.freeze();
frappe.xcall('frappe.social.doctype.post.post.delete_post', {
'post_name': this.post.name
}).then(frappe.dom.unfreeze)
})
},
update_seen() {
frappe.xcall('frappe.social.doctype.post.post.set_seen', {
post_name: this.post.name
}).then(() => this.post.seen = true)
},
generate_preview(link_element) {
// TODO: move the code to separate component
frappe.xcall('frappe.social.doctype.post.post.get_link_info', {
'url': link_element.href
}).then(info => {
const title = frappe.ellipsis(info['og:title'] || info['title'], 60)
const description = frappe.ellipsis(info['og:description'] || info['description'], 280)
const image = info['og:image'];
const url = info['og:url'];
if (title) {
link_element.insertAdjacentHTML('afterend', `
<a href="${url}" target="blank" class="preview-card" class="flex">
<img src="${image}"/>
<div class="flex-column">
<h5>${title}</h5>
<p class="text-muted">${description}</p>
</div>
</a>
` );
}
})
.catch(console.error)
}
}
}
</script>
<style lang="less" scoped>
.post-comments {
padding: 15px 46px;
padding-top: 0px;
background: #F6F6F6;
}
.indicator {
margin-left: 15px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .8s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,66 +0,0 @@
<template>
<div class="post-action-container">
<div class="like" @click="$emit('toggle_like')">
<i class="octicon octicon-heart" :class="{'liked': post_liked}"></i>
<span class="likes" :data-liked-by="liked_by_data">{{ like_count }}</span>
</div>
<div class="comment" @click="$emit('toggle_comment')">
<i class="octicon octicon-comment"></i>
<span class="comment_count">{{ comment_count }}</span>
</div>
</div>
</template>
<script>
export default {
props: [
'liked_by',
'comment_count',
],
computed: {
like_count() {
return this.split_string(this.liked_by).length;
},
post_liked() {
return this.split_string(this.liked_by).includes(frappe.session.user);
},
liked_by_data() {
return JSON.stringify(this.split_string(this.liked_by));
}
},
methods: {
split_string(str) {
return str && str !== '' ? str.split('\n') : []
}
}
}
</script>
<style lang='less' scoped>
.post-action-container {
display: flex;
background-color: #F6F6F6;
padding: 10px;
.comment, .like {
padding-right: 20px;
cursor: pointer;
color: #8d99a6;
span {
padding: 5px;
}
&:hover {
color: darken(#8d99a6, 10%);
}
}
.likes {
cursor: pointer;
}
.liked {
color: #fc4f51;
&:hover {
color: lighten(#fc4f51, 10%) !important;
}
}
.pinned {
color: black;
}
}
</style>

View file

@ -1,123 +0,0 @@
<template>
<div>
<div class="comment-box flex-column">
<div class="text-muted comment-label">{{ __('Add a comment') }}</div>
<div ref="comment-section"></div>
<div class="flex justify-between">
<div class="text-muted small">
{{ __("Ctrl+Enter to add comment") }}
</div>
<button
class="btn btn-primary btn-sm"
@click="create_comment">
{{ __('Comment') }}
</button>
</div>
</div>
<div ref="comments" v-if="comments.length" class="comment-list">
<div class="comment" v-for="comment in comments" :key="comment.name">
<span
class="cursor-pointer"
@click="go_to_profile_page(comment.owner)"
v-html="get_avatar(comment.owner)">
</span>
<span class="content" v-html="comment.content"/>
<span
class="text-muted"
v-html="get_time(comment.creation)">
</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['comments'],
mounted() {
this.make_comment_section();
this.make_mentions_clickable(this.$refs['comments']);
},
methods: {
get_avatar(user) {
return frappe.avatar(user)
},
get_time(timestamp) {
return comment_when(timestamp, true)
},
go_to_profile_page(user) {
frappe.set_route('social', 'profile', user)
},
make_comment_section() {
this.comment_section = frappe.ui.form.make_control({
parent: this.$refs['comment-section'],
only_input: true,
render_input: true,
no_wrapper: true,
mentions: this.get_names_for_mentions(),
df: {
fieldtype: 'Comment',
fieldname: 'comment'
},
on_submit: this.create_comment.bind(this)
});
},
create_comment() {
const message = this.comment_section.get_value().replace('<div><br></div>', '');
if (!strip_html(message)) return
frappe.utils.play_sound("click");
this.$emit('create_comment', message);
this.comment_section.clear();
},
get_names_for_mentions() {
var valid_users = Object.keys(frappe.boot.user_info)
.filter(user => !["Administrator", "Guest"].includes(user));
valid_users = valid_users
.filter(user => frappe.boot.user_info[user].allowed_in_mentions==1);
return valid_users.map(user => frappe.boot.user_info[user].name);
},
make_mentions_clickable(parent_element) {
Array.from(parent_element.getElementsByClassName('mention'))
.forEach((mention) => {
mention.classList.add('cursor-pointer');
mention.addEventListener('click', () => {
this.go_to_profile_page(mention.dataset.value)
})
});
}
}
}
</script>
<style lang="less" scoped>
.comment-box {
.comment-label {
margin-bottom: 5px;
}
::v-deep .ql-editor {
background: white;
border-radius: 4px;
min-height: 60px !important;
border: 1px solid #d1d8dd;
}
button {
padding: 2px 5px;
font-size: 10px;
align-self: flex-end;
}
}
.comment-list {
margin-top: 10px;
.comment {
.comment-input-wrapper {
margin-top: -6px;
font-size: 11px;
}
display: flex;
padding: 5px 0;
.content {
align-self: center;
font-size: 12px;
flex: 1
}
}
}
</style>

View file

@ -1,27 +0,0 @@
<template>
<div class="dropdown">
<span class="caret cursor-pointer dropdown-toggle" data-toggle="dropdown"></span>
<ul class="dropdown-menu">
<li v-for="option in options" :key="option.label">
<a @click="option.action">{{option.label}}</a>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['options'],
}
</script>
<style lang="less" scoped>
.dropdown-menu {
min-width: 100px;
left: auto;
right: 0;
a {
padding: 12px;
font-size: 10px;
}
}
</style>

View file

@ -1,128 +0,0 @@
<template>
<div>
<div v-if="loading_posts && !posts.length">
<post-skeleton v-for="index in 5" :key="index"/>
</div>
<transition-group name="flip-list">
<post ref="posts"
:post="post"
v-for="(post, index) in posts"
:key="post.name"
@delete-post="delete_post(index)"
/>
</transition-group>
<div v-if="!loading_posts && !posts.length" class="no-post-message text-muted">
{{ __('No posts yet') }}
</div>
<div
v-show="loading_posts && posts.length"
class="text-center text-muted padding">
{{ __('Fetching posts...') }}
</div>
<div
v-show="posts.length && !loading_posts && !more_posts_available"
class="text-center text-muted padding">
{{ __("No more posts") }}
</div>
</div>
</template>
<script>
import Post from './Post.vue';
import PostSkeleton from './PostSkeleton.vue';
export default {
props: ['post_list_filter'],
components: {
Post,
PostSkeleton
},
data() {
return {
posts: [],
more_posts_available: true,
loading_posts: false,
load_new: false
}
},
created() {
window.addEventListener('scroll', this.handle_scroll);
this.update_posts();
this.$parent.$on('load_new_posts', () => {
this.update_posts()
})
frappe.realtime.on('global_pin', () => {
this.update_posts()
})
},
watch: {
post_list_filter(old_val, new_val) {
if (JSON.stringify(old_val) !== JSON.stringify(new_val)){
this.update_posts()
}
}
},
methods: {
get_posts(filters, load_old) {
return frappe.xcall('frappe.social.doctype.post.post.get_posts', {
filters,
limit_start: load_old ? this.posts.length : 0
})
},
update_posts(load_old = false) {
if (!this.post_list_filter) return
this.loading_posts = true;
const filters = Object.assign({}, this.post_list_filter);
this.get_posts(filters, load_old).then((res) => {
if (load_old) {
if (!res.length) {
this.more_posts_available = false;
}
this.posts = this.posts.concat(res);
} else {
this.posts = res;
}
}).finally(() => {
this.loading_posts = false;
this.track_seen()
});
},
handle_scroll: frappe.utils.debounce(function() {
this.track_seen()
const screen_bottom = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (screen_bottom && this.more_posts_available) {
if (!this.loading_posts) {
this.update_posts(true);
}
}
}, 500),
track_seen() {
const posts = this.$refs.posts || []
posts.forEach((post_component) => {
if(!post_component.post.seen
&& frappe.dom.is_element_in_viewport(post_component.$el, 50)) {
post_component.update_seen()
}
})
},
delete_post(index) {
this.posts.splice(index, 1);
},
},
destroyed() {
window.removeEventListener('scroll', this.handle_scroll);
}
}
</script>
<style lang="less" scoped>
.no-post-message {
height: 200px;
text-align: center;
vertical-align: middle;
line-height: 200px;
}
.flip-list-move, .flip-list-to {
transition: transform 0.3s;
}
</style>

View file

@ -1,81 +0,0 @@
<template>
<div class="flex flex-column">
<a class="leaderboard-link"
@click.prevent="go_to_user_list()">
{{ __('Leaderboard') }}
</a>
<div class="links" v-if="frequently_visited_list.length">
<div class="muted-title">
{{ __('Frequently Visited Links') }}
</div>
<div class="flex flex-column">
<a class="route-link"
@click.prevent="goto_list(route_obj.route)"
v-for="route_obj in frequently_visited_list"
:key="route_obj.route">
{{ get_label(route_obj.route) }}
</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
frequently_visited_list: [],
}
},
created() {
this.set_frequently_visited_list()
},
methods: {
goto_list(route) {
frappe.set_route(route);
},
set_frequently_visited_list() {
frappe.xcall('frappe.social.doctype.post.post.frequently_visited_links')
.then(data => {
this.frequently_visited_list = data;
})
},
get_label(route) {
return frappe.utils.get_route_label(route);
},
go_to_profile_page() {
frappe.set_route('social', 'profile', frappe.session.user)
},
go_to_user_list() {
frappe.set_route('social', 'users')
}
}
}
</script>
<style lang="less" scoped>
.route-link {
margin: 0px 10px 10px 0;
text-transform: capitalize;
}
.leaderboard-link {
.route-link;
margin-bottom: 15px;
}
.stats {
min-height: 150px
}
.user-details {
.user-avatar {
/deep/.avatar-xl {
height: 150px;
width: 150px;
}
}
.user_name {
display: block;
margin-top: 10px;
font-size: 2rem;
font-weight: 600
}
}
</style>

View file

@ -1,61 +0,0 @@
<template>
<div class="post-skeleton flex-column justify-between">
<div class="post-skeleton-body">
<div class="flex">
<div class="avatar avatar-medium"></div>
<div class="user-name"></div>
</div>
<div class="content"></div>
</div>
<div class="post-skeleton-foot"></div>
</div>
</template>
<style lang="less" scoped>
@base-color: #f8f8f8;
@shine-color: #fcfcfc;
@animation-duration: 3s;
@keyframes load {
0% {
background-position: -100px
}
40%, 100% {
background-position: 500px
}
}
.background-img() {
background-image: linear-gradient(90deg, @base-color 0px, @shine-color 40px, @base-color 80px);
background-size: 600px
}
.post-skeleton {
height: 250px;
border-radius: 4px;
max-width: 600px;
border: 1px solid #ededed;
margin-bottom: 15px;
.post-skeleton-body {
padding: 15px;
.user-name {
height: 10px;
width: 100px;
margin-left: 5px;
}
.content {
height: 100px;
margin: 15px 0 0 46px;
}
.user-name, .avatar, .content {
.background-img();
border-radius: 4px;
animation: load @animation-duration infinite linear;
}
}
.post-skeleton-foot {
width: 100%;
height: 40px;
background: @base-color;
}
}
</style>

View file

@ -1,69 +0,0 @@
<template>
<div ref="banner" class="banner" :style="background_style">
<div
class="user-avatar container"
v-html="user_avatar">
</div>
</div>
</template>
<script>
export default {
props: ['user_id'],
data() {
return {
user_avatar: frappe.avatar(this.user_id, 'avatar-xl'),
user_banner: frappe.user_info(this.user_id).banner_image
}
},
created() {
this.$root.$on('user_image_updated', () => {
this.user_avatar = frappe.avatar(this.user_id, 'avatar-xl')
this.user_banner = frappe.user_info(this.user_id).banner_image
})
},
computed: {
background_style() {
const style = {}
if (this.user_banner) {
style['background-image'] = `url('${this.user_banner}')`
}
return style;
}
},
}
</script>
<style lang="less" scoped>
.banner {
top: 0;
left: 0;
width: 100%;
height: 300px;
z-index: 101;
position: absolute;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-color: #262626;
.user-avatar {
position: relative;
/deep/ .avatar {
top: 220px;
left: 10px;
width: 150px;
height: 150px;
border-radius: 4px;
background: white;
position: absolute !important;
}
}
.editable-image {
/deep/ .avatar {
cursor: pointer;
:hover {
opacity: 0.9;
}
}
}
}
</style>

View file

@ -1,145 +0,0 @@
<template>
<div class="profile-sidebar flex flex-column">
<div class="user-details">
<h3>{{ user.fullname }}</h3>
<p><a @click="view_energy_point_list(user)" class="text-muted">
{{ __("Energy Points") }}: {{ energy_points }}</a></p>
<p>{{ user.bio }}</p>
<div class="location" v-if="user.location">
<span class="text-muted">
<i class="fa fa-map-marker">&nbsp;</i>
{{ user.location }}
</span>
</div>
<div class="interest" v-if="user.interest">
<span class="text-muted">
<i class="fa fa-puzzle-piece">&nbsp;</i>
{{ user.interest }}
</span>
</div>
</div>
<a
class="edit-profile-link"
v-if="can_edit_profile"
@click="edit_profile()"
>{{ __('Edit Profile') }}</a>
<a
class="edit-profile-link"
v-if="can_edit_user"
@click="edit_user()"
>{{ __('User Settings') }}</a>
</div>
</template>
<script>
export default {
props: {
user_id: String
},
data() {
return {
user: frappe.user_info(this.user_id),
can_edit_profile: frappe.social.is_session_user_page(),
can_edit_user: frappe.session.user === this.user_id,
energy_points: 0
};
},
mounted() {
frappe.xcall('frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points', {user: this.user_id}).then(r => {
this.energy_points = r[this.user_id].energy_points;
});
},
methods: {
edit_user() {
frappe.set_route('Form', 'User', this.user_id);
},
edit_profile() {
const edit_profile_dialog = new frappe.ui.Dialog({
title: __('Edit Profile'),
fields: [
{
fieldtype: 'Attach Image',
fieldname: 'user_image',
label: 'Profile Image',
},
{
fieldtype: 'Data',
fieldname: 'interest',
label: 'Interests',
},
{
fieldtype: 'Column Break'
},
{
fieldtype: 'Attach Image',
fieldname: 'banner_image',
label: 'Banner Image',
},
{
fieldtype: 'Data',
fieldname: 'location',
label: 'Location',
},
{
fieldtype: 'Section Break',
fieldname: 'Interest'
},
{
fieldtype: 'Small Text',
fieldname: 'bio',
label: 'Bio',
}
],
primary_action: values => {
edit_profile_dialog.disable_primary_action();
frappe
.xcall('frappe.core.doctype.user.user.update_profile_info', {
profile_info: values
})
.then(user => {
user.image = user.user_image;
let user_info = frappe.user_info(this.user_id);
this.user = Object.assign(user_info, user);
this.$root.$emit('user_image_updated');
edit_profile_dialog.hide();
})
.finally(() => {
edit_profile_dialog.enable_primary_action();
});
},
primary_action_label: __('Save')
});
edit_profile_dialog.set_values({
user_image: this.user.image,
banner_image: this.user.banner_image,
location: this.user.location,
interest: this.user.interest,
bio: this.user.bio
});
edit_profile_dialog.show();
},
view_energy_point_list(user) {
frappe.set_route('List', 'Energy Point Log', {user:user.name});
}
}
};
</script>
<style lang="less" scoped>
.profile-sidebar {
padding: 10px 10px 0 0;
}
.user-details {
min-height: 150px;
.location,
.interest {
margin-bottom: 10px;
i {
width: 15px;
}
}
}
.edit-profile-link {
margin-top: 15px;
}
</style>

View file

@ -1,107 +0,0 @@
<template>
<div>
<div class="profile-head">
<profile-banner :user_id="user_id"></profile-banner>
</div>
<div class="profile-container">
<profile-sidebar
:user_id="user_id"
class="profile-sidebar"
/>
<div class="post-container">
<div class="list-options">
<div
class="option"
:class="{'bold': show_list === 'user_posts'}"
@click="show_list = 'user_posts'">
<span>{{__('Posts')}}</span>
<span>({{ user_posts_count }})</span>
</div>
<div
class="option"
:class="{'bold': show_list === 'liked_posts'}"
@click="show_list = 'liked_posts'">
<span>{{__('Likes')}}</span>
<span>({{ liked_posts_count }})</span>
</div>
</div>
<post-loader :post_list_filter="post_list_filter"></post-loader>
</div>
<activity-sidebar class="activity-sidebar hidden-xs"/>
</div>
</div>
</template>
<script>
import PostLoader from '../components/PostLoader.vue';
import ProfileSidebar from '../components/ProfileSidebar.vue';
import ActivitySidebar from '../components/ActivitySidebar.vue';
import ProfileBanner from '../components/ProfileBanner.vue';
export default {
props: ['user_id'],
components: {
PostLoader,
ProfileSidebar,
ProfileBanner,
ActivitySidebar
},
data() {
return {
show_list: 'user_posts',
post_list_filter : null,
user_posts_count: 0,
liked_posts_count: 0,
}
},
watch: {
show_list() {
if (this.show_list == 'user_posts') {
this.post_list_filter = this.get_user_posts_filter();
} else if (this.show_list == 'liked_posts') {
this.post_list_filter = this.get_liked_posts_filter();
}
}
},
created() {
this.post_list_filter = this.get_user_posts_filter();
this.set_post_count()
},
methods: {
get_user_posts_filter() {
return {
'owner': this.user_id
}
},
get_liked_posts_filter() {
return {
'liked_by': ['like', '%' + this.user_id + '%']
}
},
set_post_count() {
frappe.db.count('Post', { filters: this.get_user_posts_filter() })
.then(count => this.user_posts_count = count)
frappe.db.count('Post', { filters: this.get_liked_posts_filter() })
.then(count => this.liked_posts_count = count)
}
}
}
</script>
<style lang="less" scoped>
.profile-head {
height: 190px;
}
.profile-sidebar {
margin-top: 60px;
flex: 20%;
}
.right-sidebar {
margin-top: 5px;
}
.list-options {
display: flex;
.option {
cursor: pointer;
padding: 0px 10px 10px 0;
}
}
</style>

View file

@ -1,219 +0,0 @@
<template>
<div class="user-list-container">
<ul class="list-unstyled user-list">
<li class="user-card user-list-header text-medium">
<span class="rank-column"></span>
<span class="user-details text-muted">
<input
class="form-control"
type="search"
placeholder="Search User"
v-model="filter_users_by"
>
</span>
<span class="flex-40"></span>
<span class="flex-20 text-muted">
<select class="form-control" data-toggle="tooltip" title="Period" v-model="period">
<option v-for="value in period_options" :key="value" :value="value">{{ value }}</option>
</select>
</span>
</li>
<li class="user-card user-list-header text-medium">
<span class="rank-column">#</span>
<span class="user-details text-muted">{{ __('User') }}</span>
<span
class="flex-20 text-muted"
v-for="title in ['Energy Points', 'Review Points', 'Points Given']"
:key="title"
>{{ __(title) }}</span>
</li>
<li v-for="(user, index) in filtered_users" :key="user.name">
<div class="user-card">
<span class="user-details flex" @click="go_to_profile_page(user.name)">
<span class="rank-column">{{ index + 1 }}</span>
<span v-html="get_avatar(user.name)"></span>
<span>
{{ user.fullname }}
<div
class="text-muted text-medium"
:class="{'italic': !user.bio}"
>{{ frappe.ellipsis(user.bio, 100) || 'No Bio'}}</div>
</span>
</span>
<span
class="text-muted text-nowrap flex-20"
v-for="key in ['energy_points', 'review_points', 'given_points']"
:key="key"
@click="toggle_log(user.name)"
>{{ user[key] }}</span>
</div>
</li>
<li class="user-card text-muted" v-if="!filtered_users.length">{{__('No user found')}}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
users: [],
filter_users_by: null,
sort_users_by: 'energy_points',
sort_order: 'desc',
show_log_for: null,
period_options: ['Lifetime', 'This Month', 'This Week', 'Today'],
period: 'This Month'
};
},
computed: {
from_date() {
if (this.period === 'This Month') {
return frappe.datetime.month_start();
}
if (this.period === 'This Week') {
return frappe.datetime.week_start();
}
if (this.period === 'Today') {
return frappe.datetime.get_today();
}
return null;
},
filtered_users() {
let filtered = this.users.slice();
if (this.filter_users_by) {
filtered = filtered.filter(user =>
user.fullname
.toLowerCase()
.includes(this.filter_users_by.toLowerCase())
);
}
if (this.sort_users_by) {
filtered.sort((a, b) => {
const value_a = a[this.sort_users_by];
const value_b = b[this.sort_users_by];
let return_value = 0;
if (value_a > value_b) {
return_value = 1;
}
if (value_a < value_b) {
return_value = -1;
}
if (this.sort_order === 'desc') {
return_value = -return_value;
}
return return_value;
});
}
return filtered;
}
},
watch: {
period() {
this.fetch_users_energy_points_and_update_users();
}
},
mounted() {
$('[data-toggle="tooltip"]').tooltip();
},
created() {
const standard_users = ['Administrator', 'Guest', 'guest@example.com'];
this.users = frappe.boot.user_info;
// delete standard users from the list
standard_users.forEach(user => delete this.users[user]);
this.users = Object.values(this.users);
this.fetch_users_energy_points_and_update_users();
frappe.realtime.on('update_points', () => {
this.fetch_users_energy_points_and_update_users();
});
},
methods: {
get_avatar(user) {
return frappe.avatar(user, 'avatar-medium');
},
go_to_profile_page(user) {
frappe.set_route('social', 'profile', user);
},
fetch_users_energy_points_and_update_users() {
frappe
.xcall(
'frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points',
{
from_date: this.from_date
}
)
.then(data => {
let users = this.users.slice();
this.users = users.map(user => {
const points = data[user.name] || {};
user.energy_points = points.energy_points || 0;
user.review_points = points.review_points || 0;
user.given_points = points.given_points || 0;
return user;
});
});
},
toggle_log(user) {
frappe.set_route('List', 'Energy Point Log', {user:user});
}
}
};
</script>
<style lang="less" scoped>
@import 'frappe/public/less/common';
.user-list {
border-left: 1px solid @border-color;
border-right: 1px solid @border-color;
.user-card {
display: flex;
cursor: pointer;
padding: 12px 15px;
border-bottom: 1px solid @border-color;
.user-details {
flex: 1;
.italic {
font-style: italic;
}
}
}
}
.rank-column {
flex: 0 0 30px;
align-self: center;
.text-muted
}
.flex-20 {
flex: 0 0 20%;
text-align: right;
align-self: center;
}
.flex-40 {
flex: 0 0 40%;
}
.user-list-header {
background-color: @light-bg;
}
.search-bar {
position: sticky;
top: 0;
background: white;
height: 75px;
text-align: center;
div {
margin: auto;
}
width: 100%;
left: 0;
}
.energy-point-history {
border-bottom: 1px solid @border-color;
background-color: @light-bg;
}
</style>

View file

@ -1,50 +0,0 @@
<template>
<div class="wall-container">
<post-sidebar class="post-sidebar hidden-xs"></post-sidebar>
<div class="post-container">
<div
class="new_posts_count"
@click="load_new_posts"
v-show='new_posts_count'>
{{ new_posts_count + ' new post'}}
</div>
<post-loader :post_list_filter="{}"></post-loader>
</div>
<activity-sidebar class="activity-sidebar hidden-xs"/>
</div>
</template>
<script>
import PostLoader from '../components/PostLoader.vue';
import PostSidebar from '../components/PostSidebar.vue';
import ActivitySidebar from '../components/ActivitySidebar.vue';
export default {
components: {
PostLoader,
PostSidebar,
ActivitySidebar
},
data() {
return {
'posts': [],
'events': [],
'new_posts_count': 0,
}
},
created() {
frappe.realtime.on('new_post', (post_owner) => {
if (post_owner === frappe.session.user) {
this.load_new_posts()
} else {
this.new_posts_count += 1;
}
})
},
methods: {
load_new_posts() {
this.$emit('load_new_posts');
this.new_posts_count = 0;
}
}
};
</script>

View file

@ -1,21 +0,0 @@
frappe.views.SocialFactory = class SocialFactory extends frappe.views.Factory {
show() {
if (frappe.pages.social) {
frappe.container.change_to('social');
} else {
this.make('social');
}
}
make(page_name) {
const assets = [
'/assets/js/social.min.js'
];
frappe.require(assets, () => {
frappe.social.home = new frappe.social.Home({
parent: this.make_page(true, page_name)
});
});
}
};

View file

@ -1,67 +0,0 @@
import Home from './Home.vue';
frappe.provide('frappe.social');
frappe.social.Home = class SocialHome {
constructor({ parent }) {
this.$parent = $(parent);
this.page = parent.page;
this.setup_header();
this.make_body();
}
make_body() {
this.$social_container = this.$parent.find('.layout-main');
new Vue({
el: this.$social_container[0],
render: h => h(Home),
data: {
'page': this.page
}
});
}
setup_header() {
this.page.set_title(__('Social'));
}
};
frappe.social.post_dialog = new frappe.ui.Dialog({
title: __('Create Post'),
fields: [
{
fieldtype: "Text Editor",
fieldname: "content",
label: __("Content"),
reqd: 1
}
],
primary_action_label: __('Post'),
primary_action: (values) => {
frappe.social.post_dialog.disable_primary_action();
const post = frappe.model.get_new_doc('Post');
post.content = values.content;
frappe.db.insert(post).then(() => {
frappe.social.post_dialog.clear();
frappe.social.post_dialog.hide();
}).finally(() => {
frappe.social.post_dialog.enable_primary_action();
});
}
});
frappe.social.is_home_page = () => {
return frappe.get_route()[0] === 'social' && frappe.get_route()[1] === 'home';
};
frappe.social.is_profile_page = (user) => {
return frappe.get_route()[0] === 'social'
&& frappe.get_route()[1] === 'profile'
&& (user ? frappe.get_route()[2] === user : true);
};
frappe.social.is_session_user_page = () => {
return frappe.social.is_profile_page() && frappe.get_route()[2] === frappe.session.user;
};
frappe.provide('frappe.app_updates');
frappe.utils.make_event_emitter(frappe.app_updates);

View file

@ -82,7 +82,7 @@ frappe.views.Container = Class.extend({
$(document).trigger("page-change");
this.page._route = frappe.get_sub_path();
this.page._route = frappe.router.get_sub_path();
$(this.page).trigger('show');
!this.page.disable_scroll_to_top && frappe.utils.scroll_to(0);
frappe.breadcrumbs.update();

View file

@ -93,7 +93,7 @@ frappe.views.Workspace = class Workspace {
const get_sidebar_item = function (item) {
return $(`<a
href="/desk/workspace/${item.name}"
href="/app/workspace/${item.name}"
class="desk-sidebar-item standard-sidebar-item ${item.selected ? "selected" : ""}"
>
<div> ${frappe.utils.icon(item.icon || "folder-normal", "md")} </div>

View file

@ -90,11 +90,8 @@ export default class LinksWidget extends Widget {
if (this.in_customize_mode) return;
if (link_label.hasClass("help-video-link")) {
let yt_id = event.target.dataset.youtubeid;
let yt_id = event.currentTarget.dataset.youtubeid;
frappe.help.show_video(yt_id);
} else {
let route = event.target.dataset.route;
frappe.set_route(route);
}
});
}

View file

@ -8,8 +8,10 @@ function generate_route(item) {
if (item.link) {
route = strip(item.link, "#");
} else if (type === "doctype") {
let doctype_slug = frappe.router.slug(item.doctype);
if (frappe.model.is_single(item.doctype)) {
route = "Form/" + item.doctype;
route = "Form/" + doctype_slug;
} else {
if (!item.doc_view) {
if (frappe.model.is_tree(item.doctype)) {
@ -18,27 +20,28 @@ function generate_route(item) {
item.doc_view = "List";
}
}
switch (item.doc_view) {
case "List":
if (item.filters) {
frappe.route_options = item.filters;
}
route = "List/" + item.doctype;
route = "List/" + doctype_slug;
break;
case "Tree":
route = "Tree/" + item.doctype;
route = "Tree/" + doctype_slug;
break;
case "Report Builder":
route = "List/" + item.doctype + "/Report";
route = "List/" + doctype_slug + "/Report";
break;
case "Dashboard":
route = "List/" + item.doctype + "/Dashboard";
route = "List/" + doctype_slug + "/Dashboard";
break;
case "New":
route = "Form/" + item.doctype + "/New " + item.doctype;
route = "Form/" + doctype_slug + "/New " + item.doctype;
break;
case "Calendar":
route = "List/" + item.doctype + "/Calendar/Default";
route = "List/" + doctype_slug + "/Calendar/Default";
break;
default:
frappe.throw({ message: __("Not a valid DocType view:") + item.doc_view, title: __("Unknown View") });
@ -48,7 +51,7 @@ function generate_route(item) {
} else if (type === "report" && item.is_query_report) {
route = "query-report/" + item.name;
} else if (type === "report") {
route = "List/" + item.doctype + "/Report/" + item.name;
route = "List/" + frappe.router.slug(item.doctype) + "/Report/" + item.name;
} else if (type === "page") {
route = item.name;
} else if (type === "dashboard") {
@ -73,7 +76,7 @@ function generate_route(item) {
// (item.doctype && frappe.model.can_read(item.doctype))) {
// item.shown = true;
// }
return route;
return `/app/${route}`;
}
function generate_grid(data) {

View file

@ -1,155 +0,0 @@
@import "variables.less";
@import (reference) 'common.less';
body[data-route*="social"] {
.layout-main-section {
border: none;
}
a {
transition: none;
}
.liked-by-popover {
font-size: @text-small;
}
.wall-container {
.post-container {
.new_posts_count {
cursor: pointer;
text-align: center;
padding: 5px;
margin-bottom: 15px;
}
}
}
.wall-container, .profile-container {
display: flex;
font-size: 12px;
.post-sidebar {
padding-top: 20px;
flex: 20%
}
.post-container {
flex: 55%;
display: flex;
padding: 15px;
flex-direction: column;
}
.activity-sidebar {
padding: 15px;
flex: 25%;
.event {
margin-bottom: 15px;
}
}
}
.generic-card() {
background: white;
font-size: 12px;
margin-bottom: 15px;
min-height: 70px;
border: 1px solid @border-color;
border-radius: 4px;
overflow: hidden;
.content {
font-size: 14px;
img, iframe {
border-radius: 5px;
border: 1px solid @light-border-color;
margin: 5px 0;
}
img {
&:hover {
opacity: 0.7;
cursor: pointer;
}
}
}
}
.post-card {
.generic-card();
max-width: 600px;
.post-body {
padding: 15px;
.user-name {
font-weight: 500;
}
.user-avatar {
float: left;
margin-right: 5px;
}
.user-avatar, .user-name {
cursor: pointer;
}
.content {
margin: 15px 0 0 46px;
}
.post-dropdown-menu {
display: none;
padding-left: 10px;
line-height: 1;
}
}
.pin-option {
.text-muted;
text-transform: uppercase;
font-size: @text-small;
display: none;
}
.pinned {
background: #fefdf2;
.pin-option {
display: block;
}
}
&:hover {
.post-dropdown-menu {
display: block;
}
}
.post-action-container {
padding-left: 56px;
background-color: #F6F6F6;
border-top: 1px solid @border-color;
}
.preview-card {
text-decoration: none;
max-height: 150px;
border: 1px solid @light-border-color;
border-radius: 5px;
margin-top: 15px;
padding-right: 10px;
background: white;
display: flex;
img {
border: none;
margin: 10px;
max-width: 160px;
max-height: 140px;
align-self: center;
}
p {
overflow: hidden;
.text-medium
}
}
}
.muted-title {
&:extend(.text-muted);
margin-bottom: 10px;
text-transform: uppercase;
font-size: 10px;
font-weight: 600;
}
}
body[data-route*="social/profile"] {
.page-head {
display: none;
}
}