fix: Workspace Sidebar, Edit, New Page Fixes

Bugs:
- Clicking on + button multiple time opens multiple block list
- Resizing Onboarding block breaks design
- In edit mode links should not be clickable
- While creating new page we cannot revisit it from sidebar
- While adding multiple links in card getting error

Refactor:
- All Sidebar actions happens while you do it, no need to click Save Customization button
- Updating Sidebar, Creating New Page, Duplicating every action is much faster
- Most of the actions are happening in background while rending page using cached data
This commit is contained in:
Shariq Ansari 2022-01-04 15:35:57 +05:30
parent 3a4b77c86c
commit db3870151a
14 changed files with 240 additions and 178 deletions

View file

@ -453,25 +453,24 @@ def get_custom_report_list(module):
return out
def save_new_widget(doc, page, blocks, new_widgets):
if loads(new_widgets):
widgets = _dict(loads(new_widgets))
widgets = _dict(loads(new_widgets))
if widgets.chart:
doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
if widgets.shortcut:
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
if widgets.card:
doc.build_links_table_from_card(widgets.card)
if widgets.chart:
doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
if widgets.shortcut:
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
if widgets.card:
doc.build_links_table_from_card(widgets.card)
# remove duplicate and unwanted widgets
if widgets:
clean_up(doc, blocks)
clean_up(doc, blocks)
try:
doc.save(ignore_permissions=True)
except (ValidationError, TypeError) as e:
# Create a json string to log
json_config = dumps(widgets, sort_keys=True, indent=4)
json_config = widgets and dumps(widgets, sort_keys=True, indent=4)
# Error log body
log = \

View file

@ -122,47 +122,68 @@ def get_report_type(report):
report_type = frappe.get_value("Report", report, "report_type")
return report_type in ["Query Report", "Script Report", "Custom Report"]
@frappe.whitelist()
def new_page(new_page):
if not loads(new_page):
return
page = loads(new_page)
if page.get("public") and not is_workspace_manager():
return
doc = frappe.new_doc('Workspace')
doc.title = page.get('title')
doc.icon = page.get('icon')
doc.content = page.get('content')
doc.parent_page = page.get('parent_page')
doc.label = page.get('label')
doc.for_user = page.get('for_user')
doc.public = page.get('public')
doc.sequence_id = last_sequence_id(doc) + 1
doc.save(ignore_permissions=True)
def last_sequence_id(doc):
doc_exists = frappe.db.exists({
'doctype': 'Workspace',
'public': doc.public,
'for_user': doc.for_user
})
if not doc_exists:
return 0
return frappe.db.get_list('Workspace',
fields=['sequence_id'],
filters={
'public': doc.public,
'for_user': doc.for_user
},
order_by="sequence_id desc"
)[0].sequence_id
@frappe.whitelist()
def save_page(title, icon, parent, public, sb_public_items, sb_private_items, new_widgets, blocks, save):
save = frappe.parse_json(save)
def save_page(title, public, new_widgets, blocks):
public = frappe.parse_json(public)
if save:
doc = frappe.new_doc('Workspace')
doc.title = title
doc.icon = icon
doc.content = blocks
doc.parent_page = parent
doc.label = title
doc.for_user = ''
doc.public = 1
if not public:
doc.label = title + "-" + frappe.session.user
doc.for_user = frappe.session.user
doc.public = 0
doc.save(ignore_permissions=True)
else:
filters = {
'public': public,
'label': title
}
if not public:
filters = {
'public': public,
'label': title
'for_user': frappe.session.user,
'label': title + "-" + frappe.session.user
}
if not public:
filters = {
'for_user': frappe.session.user,
'label': title + "-" + frappe.session.user
}
pages = frappe.get_list("Workspace", filters=filters)
if pages:
doc = frappe.get_doc("Workspace", pages[0])
pages = frappe.get_list("Workspace", filters=filters)
if pages:
doc = frappe.get_doc("Workspace", pages[0])
doc.content = blocks
doc.save(ignore_permissions=True)
doc.content = blocks
doc.save(ignore_permissions=True)
if loads(new_widgets):
save_new_widget(doc, title, blocks, new_widgets)
if loads(sb_public_items) or loads(sb_private_items):
sort_pages(loads(sb_public_items), loads(sb_private_items))
save_new_widget(doc, title, blocks, new_widgets)
return {"name": title, "public": public, "label": doc.label}
@ -242,7 +263,14 @@ def delete_page(page):
return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")}
@frappe.whitelist()
def sort_pages(sb_public_items, sb_private_items):
if not loads(sb_public_items) and not loads(sb_private_items):
return
sb_public_items = loads(sb_public_items)
sb_private_items = loads(sb_private_items)
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})

View file

@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
get_options() {
let options = '';
if (this.df.get_options) {
options = this.df.get_options();
options = this.df.get_options(this);
} else if (this.docname==null && cur_dialog) {
//for dialog box
options = cur_dialog.get_value(this.df.options);

View file

@ -142,6 +142,7 @@ export default class Block {
}
add_settings_button() {
let me = this;
this.dropdown_list = [
{
label: 'Delete',
@ -215,6 +216,10 @@ export default class Block {
$widget_control.prepend($button);
this.dropdown_list.forEach((item) => {
if ((item.label == 'Expand' || item.label == 'Shrink') &&
me.options && !me.options.allow_resize) {
return;
}
$button.find('.dropdown-list').append(dropdown_item(item.label, item.title, item.icon, item.action));
});
}
@ -259,6 +264,7 @@ export default class Block {
}
decrease_width() {
let min_width = this.options && this.options.min_width || 3;
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
if (currentBlockIndex < 0) {
@ -282,7 +288,7 @@ export default class Block {
});
let parts = className.split('-');
let width = parseInt(parts[1]);
if (width >= 4) {
if (width > min_width) {
currentBlockElement.classList.remove('col-'+width);
width = width - 1;
currentBlockElement.classList.add('col-'+width);

View file

@ -22,6 +22,7 @@ export default class Card extends Block {
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true,
allow_resize: true
};
}

View file

@ -21,7 +21,9 @@ export default class Chart extends Block {
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true,
max_widget_count: 2,
allow_resize: true,
min_width: 6,
max_widget_count: 2
};
}

View file

@ -21,7 +21,8 @@ export default class Onboarding extends Block {
allow_create: this.allow_customization,
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true
allow_edit: true,
allow_resize: false
};
}
@ -30,7 +31,6 @@ export default class Onboarding extends Block {
if (this.readOnly && !$(this.wrapper).find('.onboarding-widget-box').is(':visible')) {
$(e).hide();
}
!this.readOnly && this.resizer();
e.classList.add("col-" + this.get_col());
}

View file

@ -28,9 +28,7 @@ export default class Paragraph extends Block {
onKeyUp(e) {
if (!this.wrapper) return;
let $block_list_container = $(this.wrapper.parentElement).find('.block-list-container.dropdown-list');
$block_list_container.addClass('hidden');
this.show_hide_block_list(true);
if (e.code !== 'Backspace' && e.code !== 'Delete') {
return;
}
@ -38,11 +36,18 @@ export default class Paragraph extends Block {
const {textContent} = this._element;
if (textContent === '') {
$block_list_container .removeClass('hidden');
this.show_hide_block_list();
this._element.innerHTML = '';
}
}
show_hide_block_list(hide) {
let $wrapper = $(this.wrapper).hasClass('ce-paragraph') ? $(this.wrapper.parentElement) : $(this.wrapper);
let $block_list_container = $wrapper.find('.block-list-container.dropdown-list');
$block_list_container.removeClass('hidden');
hide && $block_list_container.addClass('hidden');
}
drawView() {
let div = document.createElement('DIV');
@ -54,10 +59,11 @@ export default class Paragraph extends Block {
div.addEventListener('focus', () => {
const {textContent} = this._element;
if (textContent !== '') return;
let $wrapper = $(this.wrapper).hasClass('ce-paragraph') ? $(this.wrapper.parentElement) : $(this.wrapper);
let $block_list_container = $wrapper.find('.block-list-container.dropdown-list');
$block_list_container.removeClass('hidden');
this.show_hide_block_list();
});
div.addEventListener('blur', () => {
setTimeout(() => { this.show_hide_block_list(true) }, 10);
})
div.dataset.placeholder = this.api.i18n.t(this._placeholder);
div.addEventListener('keyup', this.onKeyUp);
}

View file

@ -20,7 +20,8 @@ export default class Shortcut extends Block {
allow_create: this.allow_customization,
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true
allow_edit: true,
allow_resize: true
};
}

View file

@ -22,7 +22,6 @@ frappe.views.Workspace = class Workspace {
this.page = wrapper.page;
this.blocks = frappe.wspace_block.blocks;
this.is_read_only = true;
this.new_page = null;
this.pages = {};
this.sorted_public_items = [];
this.sorted_private_items = [];
@ -51,9 +50,10 @@ frappe.views.Workspace = class Workspace {
}
async setup_pages(reload) {
this.create_page_skeleton();
this.create_sidebar_skeleton();
!this.discard && this.create_page_skeleton();
!this.discard && this.create_sidebar_skeleton();
this.sidebar_pages = !this.discard ? await this.get_pages() : this.sidebar_pages;
this.cached_pages = $.extend(true, {}, this.sidebar_pages);
this.all_pages = this.sidebar_pages.pages;
this.has_access = this.sidebar_pages.has_access;
@ -69,20 +69,6 @@ frappe.views.Workspace = class Workspace {
for (let page of this.all_pages) {
frappe.workspaces[frappe.router.slug(page.name)] = {title: page.title};
}
if (this.new_page && this.new_page.name) {
if (!frappe.workspaces[frappe.router.slug(this.new_page.label)]) {
this.new_page = {
name: this.all_pages[0].title,
public: this.all_pages[0].public
};
}
let pre_url = this.new_page.public ? '' : 'private/';
let route = pre_url + frappe.router.slug(this.new_page.name);
frappe.set_route(route);
this.new_page = null;
}
this.make_sidebar();
reload && this.show();
}
@ -392,10 +378,12 @@ frappe.views.Workspace = class Workspace {
__("Save Customizations"),
() => {
this.clear_page_actions();
this.undo.readOnly = true;
this.save_page();
this.editor.readOnly.toggle();
this.is_read_only = true;
this.save_page(page).then((saved) => {
if (!saved) return;
this.undo.readOnly = true;
this.editor.readOnly.toggle();
this.is_read_only = true;
});
},
null,
__("Saving")
@ -408,6 +396,7 @@ frappe.views.Workspace = class Workspace {
this.clear_page_actions();
await this.editor.readOnly.toggle();
this.is_read_only = true;
this.sidebar_pages = this.cached_pages;
this.reload();
frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" });
}
@ -425,14 +414,21 @@ frappe.views.Workspace = class Workspace {
add_sidebar_actions(item, sidebar_control, is_new) {
if (!item.is_editable) {
$(`<span class="sidebar-info">${frappe.utils.icon("lock", "sm")}</span>`)
.appendTo(sidebar_control);
sidebar_control.parent().click(() => {
!this.is_read_only && frappe.show_alert({
message: __("Only Workspace Manager can sort or edit this page"),
indicator: 'info'
}, 5);
});
frappe.utils.add_custom_button(
frappe.utils.icon('duplicate', 'sm'),
() => this.duplicate_page(item),
"duplicate-page",
`${__('Duplicate Workspace')}`,
null,
sidebar_control
);
} else {
frappe.utils.add_custom_button(
frappe.utils.icon('drag', 'xs'),
@ -586,7 +582,7 @@ frappe.views.Workspace = class Workspace {
this.update_cached_values(old_child, child);
}
update_cached_values(old_item, new_item, duplicate) {
update_cached_values(old_item, new_item, duplicate, new_page) {
let [from_pages, to_pages] = old_item.public ?
[this.public_pages, this.private_pages] : [this.private_pages, this.public_pages];
@ -594,7 +590,7 @@ frappe.views.Workspace = class Workspace {
duplicate && old_item_index++;
// update frappe.workspaces
if (frappe.workspaces[frappe.router.slug(old_item.name)]) {
if (frappe.workspaces[frappe.router.slug(old_item.name)] || new_page) {
!duplicate && delete frappe.workspaces[frappe.router.slug(old_item.name)];
if (new_item) {
frappe.workspaces[frappe.router.slug(new_item.name)] = {'title': new_item.title};
@ -602,9 +598,9 @@ frappe.views.Workspace = class Workspace {
}
// update page block data
if (this.pages && this.pages[old_item.name]) {
if (this.pages && this.pages[old_item.name] || new_page) {
if (new_item) {
this.pages[new_item.name] = this.pages[old_item.name];
this.pages[new_item.name] = this.pages[old_item.name] || {};
}
!duplicate && delete this.pages[old_item.name];
}
@ -616,6 +612,8 @@ frappe.views.Workspace = class Workspace {
if (is_section_changed) {
!duplicate && from_pages.splice(old_item_index, 1);
to_pages.push(new_item);
} else if (new_page) {
from_pages.push(new_item);
} else {
from_pages.splice(old_item_index, duplicate ? 0 : 1, new_item);
}
@ -624,6 +622,7 @@ frappe.views.Workspace = class Workspace {
}
this.sidebar_pages.pages = [...this.public_pages, ...this.private_pages];
this.cached_pages = this.sidebar_pages;
}
add_settings_button(item, sidebar_control) {
@ -770,16 +769,14 @@ frappe.views.Workspace = class Workspace {
let new_page = {...page};
new_page.title = values.title;
new_page.name = values.title;
new_page.icon = values.icon;
new_page.public = values.is_public;
new_page.parent_page = values.parent || '';
new_page.for_user = '';
if (!values.is_public) {
new_page.name += '-' + frappe.session.user;
new_page.for_user = frappe.session.user;
}
new_page.public = values.is_public || 0;
new_page.name = values.title + new_page.public ? '' : '-' + frappe.session.user;
new_page.label = new_page.name;
new_page.icon = values.icon;
new_page.parent_page = values.parent || '';
new_page.for_user = new_page.public ? '' : frappe.session.user;
new_page.is_editable = !new_page.public;
new_page.selected = true;
this.update_cached_values(page, new_page, true);
@ -807,35 +804,69 @@ frappe.views.Workspace = class Workspace {
onEnd: function (evt) {
let is_public = $(evt.item).attr('item-public') == '1';
me.prepare_sorted_sidebar(is_public);
me.update_sorted_sidebar();
}
});
});
}
prepare_sorted_sidebar(is_public) {
let pages = is_public ? this.public_pages : this.private_pages;
if (is_public) {
this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last());
this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last(), pages);
} else {
this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first());
this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first(), pages);
}
this.sidebar_pages.pages = [...this.public_pages, ...this.private_pages];
this.cached_pages = this.sidebar_pages;
}
sort_sidebar($sidebar_section) {
sort_sidebar($sidebar_section, pages) {
let sorted_items = [];
for (let page of $sidebar_section.find('.sidebar-item-container')) {
Array.from($sidebar_section.find('.sidebar-item-container')).forEach((page, i) => {
let parent_page = "";
if (page.closest('.nested-container').classList.contains('sidebar-child-item')) {
parent_page = page.parentElement.parentElement.attributes["item-name"].value;
}
sorted_items.push({
title: page.attributes['item-name'].value,
parent_page: parent_page,
public: page.attributes['item-public'].value
});
}
let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first();
if ($(page).find('.sidebar-child-item > *').length != 0) {
$drop_icon.removeClass('hidden');
} else {
$drop_icon.addClass('hidden');
}
let from_index = pages.findIndex(p => p.title == page.attributes['item-name'].value);
let element = pages[from_index];
element.parent_page = parent_page;
if (from_index != i) {
pages.splice(from_index, 1);
pages.splice(i, 0, element);
}
});
return sorted_items;
}
update_sorted_sidebar() {
if (this.sorted_public_items || this.sorted_private_items) {
frappe.call({
method: "frappe.desk.doctype.workspace.workspace.sort_pages",
args: {
sb_public_items: this.sorted_public_items,
sb_private_items: this.sorted_private_items,
}
});
}
}
make_blocks_sortable() {
let me = this;
this.page_sortable = Sortable.create(this.page.main.find(".codex-editor__redactor").get(0), {
@ -894,26 +925,49 @@ frappe.views.Workspace = class Workspace {
d.hide();
this.initialize_editorjs_undo();
this.setup_customization_buttons({is_editable: true});
this.title = values.title;
this.icon = values.icon;
this.parent = values.parent;
this.public = values.is_public;
let name = values.title + (values.is_public ? '' : '-' + frappe.session.user);
let blocks = [{
type: "header",
data: { text: values.title }
}]
let new_page = {
content: JSON.stringify(blocks),
name: name,
label: name,
title: values.title,
public: values.is_public || 0,
for_user: values.is_public ? '' : frappe.session.user,
icon: values.icon,
parent_page: values.parent || '',
is_editable: true,
selected: true
}
this.editor.render({
blocks: [{
type: "header",
data: { text: this.title }
}]
blocks: blocks
}).then(async () => {
if (this.editor.configuration.readOnly) {
this.is_read_only = false;
await this.editor.readOnly.toggle();
}
this.add_page_to_sidebar(values);
this.show_sidebar_actions();
this.make_blocks_sortable();
this.prepare_sorted_sidebar(values.is_public);
this.update_selected_sidebar(this.current_page, false); //remove selected from old page
frappe.call({
method: "frappe.desk.doctype.workspace.workspace.new_page",
args: {
new_page: new_page
}
});
this.update_cached_values(new_page, new_page, true, true);
let pre_url = new_page.public ? '' : 'private/';
let route = pre_url + frappe.router.slug(new_page.title);
frappe.set_route(route);
this.make_sidebar();
this.show_sidebar_actions();
});
}
});
@ -1047,21 +1101,11 @@ frappe.views.Workspace = class Workspace {
});
}
save_page() {
frappe.dom.freeze();
this.create_page_skeleton();
save_page(page) {
let me = this;
let save = true;
this.current_page = { name: page.title, public: page.public };
if (!this.title && this.current_page) {
this.title = this.current_page.name;
this.public = this.current_page.public;
save = false;
} else {
this.current_page = { name: this.title, public: this.public };
}
this.editor.save().then((outputData) => {
return this.editor.save().then((outputData) => {
let new_widgets = {};
outputData.blocks.forEach(item => {
@ -1080,29 +1124,32 @@ frappe.views.Workspace = class Workspace {
item.data.card_name !== 'Custom Reports')
);
if (page.content == JSON.stringify(blocks)) {
this.setup_customization_buttons(page);
frappe.show_alert({ message: __("No changes made on the page"), indicator: "warning" });
return false;
}
this.create_page_skeleton();
page.content = JSON.stringify(blocks);
frappe.call({
method: "frappe.desk.doctype.workspace.workspace.save_page",
args: {
title: me.title,
icon: me.icon || '',
parent: me.parent || '',
public: me.public || 0,
sb_public_items: me.sorted_public_items,
sb_private_items: me.sorted_private_items,
title: page.title,
public: page.public || 0,
new_widgets: new_widgets,
blocks: JSON.stringify(blocks),
save: save
blocks: JSON.stringify(blocks)
},
callback: function(res) {
frappe.dom.unfreeze();
if (res.message) {
me.new_page = res.message;
me.pages[res.message.label] && delete me.pages[res.message.label];
me.discard = true;
me.update_cached_values(page, page);
me.reload();
frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" });
}
}
});
return true;
}).catch((error) => {
error;
// console.log('Saving failed: ', error);
@ -1110,10 +1157,6 @@ frappe.views.Workspace = class Workspace {
}
reload() {
this.title = '';
this.icon = '';
this.parent = '';
this.public = false;
this.sorted_public_items = [];
this.sorted_private_items = [];
this.setup_pages(true);

View file

@ -66,22 +66,6 @@ export default class Widget {
null,
this.action_area
);
if (options.allow_resize) {
const title = this.width == 'Full'? `${__('Collapse')}` : `${__('Expand')}`;
frappe.utils.add_custom_button(
'<i class="fa fa-expand" aria-hidden="true"></i>',
() => this.toggle_width(),
"resize-button",
title,
null,
this.action_area
);
this.resize_button = this.action_area.find(
".resize-button"
);
}
}
make() {

View file

@ -28,7 +28,7 @@ export default class ChartWidget extends Widget {
}
set_chart_title() {
const max_chars = this.widget.width() < 600 ? 20 : 60;
const max_chars = this.widget.width() < 600 ? 40 : 60;
this.set_title(max_chars);
}

View file

@ -182,19 +182,16 @@ class CardDialog extends WidgetDialog {
fieldtype: "Select",
in_list_view: 1,
label: "Link Type",
options: ["DocType", "Page", "Report"],
onchange: (e) => {
me.link_to = e.currentTarget.value;
}
options: ["DocType", "Page", "Report"]
},
{
fieldname: "link_to",
fieldtype: "Dynamic Link",
in_list_view: 1,
label: "Link To",
options: "link_type",
get_options: () => {
return me.link_to;
get_options: (df) => {
return df.doc.link_type;
}
},
{
@ -507,7 +504,7 @@ class NumberCardDialog extends WidgetDialog {
setup_dialog_events() {
if (!this.document_type) {
if (this.default_values['doctype']) {
if (this.default_values && this.default_values['doctype']) {
this.document_type = this.default_values['doctype'];
this.setup_filter(this.default_values['doctype']);
this.set_aggregate_function_fields();
@ -519,7 +516,7 @@ class NumberCardDialog extends WidgetDialog {
set_aggregate_function_fields() {
let aggregate_function_fields = [];
if (this.document_type) {
if (this.document_type && frappe.get_meta(this.document_type)) {
frappe.get_meta(this.document_type).fields.map(df => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
if (df.fieldtype == 'Currency') {
@ -538,7 +535,7 @@ class NumberCardDialog extends WidgetDialog {
if (data.new_or_existing == 'Existing Card') {
data.name = data.card;
}
data.stats_filter = JSON.stringify(this.filter_group.get_filters());
data.stats_filter = this.filter_group && JSON.stringify(this.filter_group.get_filters());
data.document_type = this.document_type;
return data;

View file

@ -875,7 +875,7 @@ body {
display: none;
}
.setting-btn {
.setting-btn, .duplicate-page {
display: none;
}
@ -883,10 +883,6 @@ body {
padding: 10px 12px 10px 2px;
}
.sidebar-info {
display: none;
}
svg {
margin-right: 0;
}
@ -927,12 +923,7 @@ body {
display: inline-block;
}
.setting-btn {
display: inline-block;
margin-right: 8px;
}
.sidebar-info {
.setting-btn, .duplicate-page {
display: inline-block;
margin-right: 8px;
}
@ -989,6 +980,10 @@ body {
opacity: 0;
transition: visibility 0s, opacity 0.5s ease-in-out;
}
.link-item {
pointer-events: none;
}
}
&:hover {