fix: Desktop

- Customize Shortcuts dialog
- Show / Hide cards dialog
- MultiSelect UI Pills
This commit is contained in:
Faris Ansari 2019-03-29 13:16:11 +05:30
parent 23d4dfcedf
commit 65484367bd
10 changed files with 293 additions and 384 deletions

View file

@ -98,7 +98,6 @@ def load_conf_settings(bootinfo):
def load_desktop_icons(bootinfo):
from frappe.config import get_modules_from_all_apps_for_user
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
bootinfo.home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings','')
def get_allowed_pages():
return get_user_pages_or_reports('Page')

View file

@ -302,30 +302,30 @@ def get_links(app, module):
link_names.append(item.get("label"))
return link_names
@frappe.whitelist()
def hide_modules_from_desktop(modules):
modules = frappe.parse_json(modules)
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
home_settings = frappe.parse_json(home_settings or '{}')
home_settings['hidden_modules'] = modules
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
return home_settings
@frappe.whitelist()
def update_desk_section_settings(desk_section, new_settings):
def update_links_for_module(module_name, links):
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
if home_settings:
home_settings = json.loads(home_settings)
else:
return {}
home_settings = frappe.parse_json(home_settings or '{}')
new_settings = json.loads(new_settings)
home_settings.setdefault('links', {})
home_settings['links'].setdefault(module_name, None)
home_settings['links'][module_name] = links
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
for module, data in new_settings.items():
if data.get("links"):
data["links"] = get_module_link_items_from_list(data["app"], module, data.get("links"))
data.pop("app", None)
home_settings[desk_section] = new_settings
settings_json_str = json.dumps(home_settings)
# # This didn't work
# frappe.db.set_value("User", frappe.session.user, 'home_settings', json.dumps(home_settings))
frappe.db.sql("""update tabUser set home_settings = %s""", (settings_json_str), debug=True)
frappe.db.commit()
return new_settings
return home_settings
def get_module_link_items_from_list(app, module, list_of_link_names):

View file

@ -238,3 +238,4 @@ frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
frappe.patches.v12_0.init_desk_settings #11-03-2019
frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings

View file

@ -0,0 +1,8 @@
import frappe
def execute():
frappe.db.sql('''
UPDATE `tabUser`
SET `home_settings` = ''
WHERE `user_type` = 'System User'
''')

View file

@ -1,6 +1,93 @@
import Awesomplete from 'awesomplete';
frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({
make_input() {
this._super();
this.$input_area = $(this.input_area);
this.$input_area.addClass('form-control table-multiselect');
this.$input.removeClass('form-control');
this.$input.on("awesomplete-selectcomplete", () => {
this.$input.val('').focus();
});
// used as an internal model to store values
this.rows = [];
this.$input_area.on('click', '.btn-remove', (e) => {
const $target = $(e.currentTarget);
const $value = $target.closest('.tb-selected-value');
const value = decodeURIComponent($value.data().value);
this.rows = this.rows.filter(val => val !== value);
this.parse_validate_and_set_in_model('');
});
this.$input.on('keydown', e => {
// if backspace key pressed on empty input, delete last value
if (e.keyCode == frappe.ui.keyCode.BACKSPACE && e.target.value === '') {
this.rows = this.rows.slice(0, this.rows.length - 1);
this.parse_validate_and_set_in_model('');
}
});
},
parse(value) {
if (value) {
this.rows.push(value);
}
return this.rows;
},
validate(value) {
const rows = (value || []).slice();
if (rows.length === 0) {
return rows;
}
const all_rows_except_last = rows.slice(0, rows.length - 1);
const last_value = rows[rows.length - 1];
// falsy value
if (!last_value) {
return all_rows_except_last;
}
// duplicate value
if (all_rows_except_last.includes(last_value)) {
return all_rows_except_last;
}
return rows;
},
set_formatted_input(value) {
this.rows = value || [];
this.set_pill_html(this.rows);
},
set_pill_html(values) {
const html = values
.map(value => this.get_pill_html(value))
.join('');
this.$input_area.find('.tb-selected-value').remove();
this.$input_area.prepend(html);
},
get_pill_html(value) {
const encoded_value = encodeURIComponent(value);
return `<div class="btn-group tb-selected-value" data-value="${encoded_value}">
<button class="btn btn-default btn-xs btn-link-to-form">${__(value)}</button>
<button class="btn btn-default btn-xs btn-remove">
<i class="fa fa-remove text-muted"></i>
</button>
</div>`;
},
get_awesomplete_settings() {
const settings = this._super();
@ -23,59 +110,37 @@ frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({
}
return v;
},
replace: function(text) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value = before + text + ", ";
}
});
},
get_value() {
let data = this._super();
// find value of label from option list and return actual value string
if (this.df.options && this.df.options.length && this.df.options[0].label) {
data = data.split(',').map(op => op.trim());
data = data.map(val => {
let option = this.df.options.find(op => op.label === val);
return option ? option.value : null;
}).filter(n => n != null).join(', ');
}
return data;
},
set_formatted_input(value) {
if (!value) return;
// find label of value from option list and set from it as input
if (this.df.options && this.df.options.length && this.df.options[0].label) {
value = value.split(',').map(d => d.trim()).map(val => {
let option = this.df.options.find(op => op.value === val);
return option ? option.label : val;
}).filter(n => n != null).join(', ');
}
this._super(value);
return this.rows;
},
get_values() {
const value = this.get_value() || '';
const values = value.split(/\s*,\s*/).filter(d => d);
return values;
return this.rows;
},
get_data() {
let data;
if(this.df.get_data) {
data = this.df.get_data();
this.set_data(data);
if (data && data.then) {
data.then((r) => {
this.set_data(r);
});
data = this.get_value();
} else {
this.set_data(data);
}
} else {
data = this._super();
}
const values = this.get_values() || [];
// return values which are not already selected
if(data) data.filter(d => !values.includes(d));
if (data) data.filter(d => !values.includes(d));
return data;
}
});

View file

@ -3,11 +3,6 @@
v-if="!hidden"
class="border module-box"
:class="{ 'hovered-box': hovered }"
:draggable="true"
@dragstart="on_dragstart"
@dragend="on_dragend"
@dragenter="on_enter"
@drop="on_drop"
>
<div class="flush-top">
<div class="module-box-content">
@ -20,23 +15,12 @@
</div>
</h4>
</a>
<dropdown v-if="links && links.length" :items="links">
<dropdown v-if="dropdown_links && dropdown_links.length" :items="dropdown_links">
<span class="pull-right">
<i class="octicon octicon-chevron-down"></i>
<i class="octicon octicon-chevron-down text-muted"></i>
</span>
</dropdown>
<!-- <span class="drag-handle octicon octicon-three-bars text-extra-muted"></span> -->
</div>
<!-- <p v-if="links && links.length" class="small text-muted">
<a
v-for="shortcut in links"
:key="(shortcut.name || shortcut.label) + shortcut.type"
:href="shortcut.route"
class="btn btn-default btn-xs shortcut-tag"
title="toggle Tag"
>{{ shortcut.label }}</a
>
</p>-->
</div>
</div>
</div>
@ -50,6 +34,7 @@ export default {
"index",
"name",
"label",
"category",
"type",
"module_name",
"link",
@ -75,32 +60,21 @@ export default {
} else {
return "octicon octicon-file-text";
}
}
},
dropdown_links() {
return this.links
.filter(link => !link.hidden)
.concat([
{ label: __('Customize'), action: () => this.$emit('customize'), class: 'border-top' }
]);
}
},
methods: {
on_dragstart() {
this.$emit("box-dragstart", this.index);
return 0;
},
on_dragend() {
this.$emit("box-dragend", this.index);
return 0;
},
on_enter() {
this.$emit("box-enter", this.index);
// this.hovered = 1;
},
on_drop() {
this.$emit("box-drop", this.index);
},
on_exit() {
// this.hovered = 0;
}
}
};
</script>
<style lang="less" scoped>
@import "frappe/public/less/variables";
.module-box {
border-radius: 4px;
padding: 5px 15px;
@ -109,16 +83,21 @@ export default {
}
.module-box:hover {
box-shadow: 0 3px 4px 0 rgba(18, 18, 19, 0.08);
border-color: @text-muted;
}
.hovered-box {
background-color: #fafbfc;
background-color: @light-bg;
}
.octicon-chevron-down {
font-size: 24px;
padding: 5px;
font-size: 14px;
padding: 4px 6px 2px 6px;
border-radius: 4px;
&:hover {
background: @btn-bg;
}
}
.octicon-chevron-down:hover {

View file

@ -2,22 +2,15 @@
<div>
<div class="section-header level text-muted">
<div class="module-category h6 uppercase">{{ category }}</div>
<div>
<a class="small text-muted" @click="show_customize_dialog">{{ __("Customize") }}</a>
</div>
</div>
<div class="modules-container">
<desk-module-box
v-for="(module, index) in modules.filter(m => !m.hidden)"
v-for="(module, index) in modules"
:key="module.name"
:index="index"
v-bind="module"
@box-dragstart="box_dragstart($event)"
@box-dragend="box_dragend($event)"
@box-enter="box_enter($event)"
@box-drop="box_drop($event)"
@customize="show_module_card_customize_dialog(module)"
></desk-module-box>
</div>
</div>
@ -25,228 +18,49 @@
<script>
import DeskModuleBox from "./DeskModuleBox.vue";
import { generate_route } from "./utils.js";
export default {
props: ['category', 'all_modules', 'customization_settings'],
props: ['category', 'modules'],
components: {
DeskModuleBox
},
data() {
let default_modules = this.all_modules;
let modules = this.get_customized_modules(default_modules, this.customization_settings);
return {
default_modules: default_modules,
modules: modules,
new_settings: {},
dragged_index: -1,
hovered_index: -1,
}
},
methods: {
show_customize_dialog() {
if(!this.dialog) {
const fields = this.make_fields();
this.make_and_show_dialog(fields);
} else {
this.dialog.show();
}
},
make_fields() {
let fields = [];
this.modules.forEach(module => {
fields.push(this.get_module_select_field(module));
if(module.links) {
fields.push(this.get_links_multiselect_field(module));
}
});
return fields;
},
make_and_show_dialog(fields) {
this.dialog = new frappe.ui.Dialog({
title: __("Customize " + this.category),
fields: fields,
primary_action_label: __("Update"),
primary_action: (values) => {
this.update_settings(values);
}
});
this.dialog.modal_body.find('.clearfix').css({'display': 'none'});
this.dialog.modal_body.find('.frappe-control*[data-fieldtype="MultiSelect"]').css({'margin-bottom': '30px'});
this.dialog.show();
},
update_settings(values) {
// Figure out the diff from the default settings known from modules
let new_settings = {};
const checkbox_fields = Object.keys(values).filter(f => !f.includes('links'));
checkbox_fields.forEach(module_name => {
const default_module = this.default_modules.filter(f => f.module_name === module_name)[0];
// Check if hidden changed
const default_hidden = default_module.hidden ? 1 : 0;
const new_hidden = !values[module_name] ? 1 : 0;
const hidden_changed = new_hidden != default_hidden;
// Check if links changed
let links_changed = 0;
let new_links = [];
if(!new_hidden) {
const default_links = default_module.links.map(l => (l.name || l.label));
const new_links_str = values[module_name + '_links'] || '';
new_links = new_links_str ? new_links_str.split(",") : [];
links_changed = !this.are_arrays_equal(new_links, default_links);
}
// Make new settings
let new_module_settings;
if(hidden_changed || links_changed) {
new_module_settings = {};
if(hidden_changed) {
new_module_settings.hidden = new_hidden;
}
if(links_changed) {
new_module_settings.links = new_links;
}
}
if(new_module_settings) {
new_module_settings.app = this.default_modules.filter(m => m.module_name === module_name)[0].app;
new_settings[module_name] = new_module_settings;
}
});
if(Object.keys(new_settings)) {
frappe.call({
type: "GET",
method:'frappe.desk.moduleview.update_desk_section_settings',
freeze: true,
args: {
desk_section: this.category,
new_settings: new_settings
},
callback: (r) => {
let new_settings_with_link_objects = r.message;
let home_settings = JSON.parse(frappe.boot.home_settings);
home_settings[this.category] = new_settings_with_link_objects;
frappe.boot.home_settings = JSON.stringify(home_settings);
this.modules = this.get_customized_modules(this.default_modules, new_settings_with_link_objects);
this.dialog.hide();
}
});
} else {
this.dialog.hide();
};
},
get_customized_modules(default_modules, customization_settings={}) {
return default_modules.map(module => {
let customized_module = JSON.parse(JSON.stringify(module));
const module_settings = customization_settings[module.module_name];
if(module_settings) {
if(module_settings.links) {
customized_module.links = module_settings.links;
}
customized_module.hidden = module_settings ? module_settings.hidden : 0;
}
if(customized_module.links) {
customized_module.links.forEach(link => {
link.route = generate_route(link);
});
}
return customized_module;
});
},
get_module_select_field(module) {
return {
label: __(module.module_name),
fieldname: module.module_name,
fieldtype: "Check",
default: module.hidden ? 0 : 1
}
},
get_links_multiselect_field(module) {
return {
label: __(""),
fieldname: module.module_name + "_links",
fieldtype: "MultiSelect",
get_data: function() {
let data = [];
frappe.call({
type: "GET",
method:'frappe.desk.moduleview.get_links',
async: false,
no_spinner: true,
args: {
app: module.app,
module: module.module_name,
show_module_card_customize_dialog(module) {
const d = new frappe.ui.Dialog({
title: __('Customize Shortcuts'),
fields: [
{
label: __('Shortcuts'),
fieldname: 'links',
fieldtype: 'MultiSelect',
get_data() {
return frappe.call('frappe.desk.moduleview.get_links', {
app: module.app,
module: module.module_name,
}).then(r => r.message);
},
callback: function(r) {
data = r.message;
}
default: module.links.filter(l => !l.hidden).map(l => l.name)
}
],
primary_action_label: __('Save'),
primary_action: ({ links }) => {
frappe.call('frappe.desk.moduleview.update_links_for_module', {
module_name: module.module_name,
links
}).then(r => {
this.$emit('update_home_settings', r.message);
});
return data;
},
default: module.links.map(l => (l.name || l.label)),
depends_on: module.module_name
};
},
are_arrays_equal(arr1, arr2) {
if(arr1.length !== arr2.length) return false;
let areEqual = true;
arr1.map((d, i) => {
if(arr2[i] !== d) areEqual = false;
d.hide();
}
});
return areEqual;
},
box_dragstart(index) {
this.dragged_index = index;
},
box_dragend(index) {
this.dragged_index = -1;
this.hovered_index = -1;
},
box_enter(index) {
this.hovered_index = index;
},
box_drop(index) {
let d = this.dragged_index;
let h = this.hovered_index;
if (d < h) {
this.modules.splice(h, 0, this.modules[d]);
this.modules.splice(d, 1);
}
d.show();
},
}
}
</script>
<style lang="less" scoped>
.section-header {
margin-top: 30px;
margin-bottom: 15px;
border-bottom: 1px solid #d0d8dd;
}
.modules-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@ -256,4 +70,3 @@ export default {
align-items: center;
}
</style>

View file

@ -1,24 +1,30 @@
<template>
<div class="modules-page-container">
<div class="toolbar-underlay"></div>
<div v-for="category in module_categories"
:key="category">
<div class="modules-page-container" v-if="home_settings_fetched">
<div
class="modules-section"
v-for="(category, i) in module_categories" :key="category"
>
<a
v-if="i === 0"
class="btn-show-hide text-muted text-medium"
@click="show_hide_cards_dialog"
>
{{ __('Show / Hide Cards') }}
</a>
<desk-section
v-if="modules.filter(m => m.category === category).length"
v-if="get_modules_for_category(category)"
:category="category"
:all_modules="modules.filter(m => m.category === category)"
:customization_settings="home_settings ? home_settings[category] : {}"
:modules="get_modules_for_category(category)"
@update_home_settings="hs => update_modules_with_home_settings(hs)"
>
</desk-section>
</div>
</div>
</template>
<script>
import DeskSection from './DeskSection.vue';
import { generate_route } from './utils';
export default {
components: {
@ -26,77 +32,98 @@ export default {
},
data() {
let modules_list = frappe.boot.allowed_modules
.filter(d => (d.type==='module' || d.category==='Places') && !d.blocked);
modules_list.forEach(module => {
module.count = this.get_module_count(module.module_name);
});
const home_settings = frappe.boot.home_settings || '{}';
.filter(d => (d.type==='module' || d.category==='Places') && !d.blocked)
.map(d => {
d.links = (d.links || []).map(link => {
link.route = generate_route(link);
return link;
});
return d;
});
return {
route_str: frappe.get_route()[1],
module_label: '',
module_categories: ["Modules", "Domains", "Places", "Administration"],
module_categories: ['Modules', 'Domains', 'Places', 'Administration'],
modules: modules_list,
// // Desk customizations. Format of user settings:
// home_settings = { // <--- Settings
// "Domains": { // <--- Category (Desk Section)
// "Manufacturing": { // <--- Module
// "index": 3,
// "links": [],
// "hidden": 1,
// },
// },
// }
home_settings: JSON.parse(home_settings)
home_settings_fetched: false
};
},
created() {
this.fetch_home_settings();
},
methods: {
get_settings() {
fetch_home_settings() {
return frappe.db.get_value('User', user, 'home_settings')
.then(resp => {
this.all_settings = JSON.parse(resp.message['home_settings']);
this.settings = this.all_settings[this.category];
.then(r => {
let home_settings = JSON.parse(r.message.home_settings || '{}');
this.update_modules_with_home_settings(home_settings);
this.home_settings_fetched = true;
});
},
get_module_count(module_name) {
var module_doctypes = frappe.boot.notification_info.module_doctypes[module_name];
var sum = 0;
update_modules_with_home_settings(home_settings) {
this.modules = this.modules.map(m => {
let hidden_modules = home_settings.hidden_modules || [];
m.hidden = hidden_modules.includes(m.module_name);
if(module_doctypes && frappe.boot.notification_info.open_count_doctype) {
// sum all doctypes for a module
for (var j=0, k=module_doctypes.length; j < k; j++) {
var doctype = module_doctypes[j];
let count = (frappe.boot.notification_info.open_count_doctype[doctype] || 0);
count = typeof count == "string" ? parseInt(count) : count;
sum += count;
let links = home_settings.links && home_settings.links[m.module_name];
if (links) {
links = JSON.parse(links);
let default_links = m.links.map(link => link.name);
m.links = m.links.map(link => {
link.hidden = !links.includes(link.name);
return link;
});
let new_links = links
.filter(link => !default_links.includes(link))
.filter(Boolean)
.map(link => {
let new_link = { name: link, label: link, type: 'doctype' };
new_link.route = generate_route(new_link);
return new_link;
});
m.links = m.links.concat(new_links);
}
}
if(frappe.boot.notification_info.open_count_doctype
&& frappe.boot.notification_info.open_count_doctype[module_name]!=null) {
// notification count explicitly for doctype
let count = frappe.boot.notification_info.open_count_doctype[module_name] || 0;
count = typeof count == "string" ? parseInt(count) : count;
sum += count;
}
return m;
});
},
get_modules_for_category(category) {
return this.modules.filter(m => m.category === category && !m.hidden);
},
show_hide_cards_dialog() {
let fields = this.module_categories.map(category => {
let modules = this.modules.filter(m => m.category === category);
let options = modules.map(
m => ({ label: m.label, value: m.module_name, checked: !m.hidden })
);
return {
label: category,
fieldname: category,
fieldtype: 'MultiCheck',
options,
columns: 2
}
});
const d = new frappe.ui.Dialog({
title: __('Show / Hide Cards'),
fields,
primary_action_label: __('Save'),
primary_action: (values) => {
let all_modules = this.modules.map(m => m.module_name);
let modules_to_show = Object.keys(values).map(k => values[k]).flatMap(m => m);
let modules_to_hide = all_modules.filter(m => !modules_to_show.includes(m));
d.hide();
if(frappe.boot.notification_info.open_count_module
&& frappe.boot.notification_info.open_count_module[module_name]!=null) {
// notification count explicitly for module
let count = frappe.boot.notification_info.open_count_module[module_name] || 0;
count = typeof count == "string" ? parseInt(count) : count;
sum += count;
}
frappe.call('frappe.desk.moduleview.hide_modules_from_desktop', {
modules: modules_to_hide
})
.then(r => r.message)
.then(hs => this.update_modules_with_home_settings(hs));
}
});
sum = sum > 99 ? "99+" : sum;
return sum;
d.show();
}
}
}
@ -104,12 +131,23 @@ export default {
<style lang="less" scoped>
.modules-page-container {
margin-top: 40px;
margin-bottom: 30px;
}
.toolbar-underlay{
margin: 70px;
.modules-section {
position: relative;
padding-top: 30px;
}
.btn-show-hide {
position: absolute;
right: 0;
top: 36px;
}
.toolbar-underlay {
margin: 70px;
}
</style>

View file

@ -1,9 +1,10 @@
<template>
<Popover :align="align">
<slot></slot>
<ul slot="popover-content" class="list-reset">
<li v-for="item of dropdownItems" :key="item.label">
<a class="list-item" :href="item.route">{{ item.label }}</a>
<ul slot="popover-content" class="list-reset border">
<li v-for="item of dropdownItems" :key="item.label" :class="item.class || null">
<a v-if="item.route" class="list-item" :href="item.route">{{ item.label }}</a>
<div v-else class="list-item" @click="item.action">{{ item.label }}</div>
</li>
</ul>
</Popover>

View file

@ -66,12 +66,17 @@
}
.table-multiselect.form-control input {
display: inline-block;
outline: none;
border: none;
padding: 0;
font-size: @text-medium;
}
.table-multiselect .awesomplete {
display: inline;
}
.tb-selected-value {
display: inline-block;
margin-right: 5px;