瀏覽代碼

Merge pull request #17491 from ankush/undo_stack

feat: Undo/Redo changes on FormView
version-14
Ankush Menat 2 年之前
committed by GitHub
父節點
當前提交
b42e3b7a6b
沒有發現已知的金鑰在資料庫的簽署中 GPG 金鑰 ID: 4AEE18F83AFDEB23
共有 4 個檔案被更改,包括 178 行新增2 行删除
  1. +60
    -0
      cypress/integration/form.js
  2. +9
    -0
      frappe/public/js/frappe/form/controls/base_control.js
  3. +28
    -2
      frappe/public/js/frappe/form/form.js
  4. +81
    -0
      frappe/public/js/frappe/form/undo_manager.js

+ 60
- 0
cypress/integration/form.js 查看文件

@@ -6,6 +6,7 @@ context('Form', () => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});

it('create a new form', () => {
cy.visit('/app/todo/new');
cy.get_field('description', 'Text Editor').type('this is a test todo', {force: true}).wait(200);
@@ -95,4 +96,63 @@ context('Form', () => {
})
})
});

it('let user undo/redo field value changes', { scrollBehavior: false }, () => {
const jump_to_field = (field_label) => {
cy.get("body")
.type("{esc}") // lose focus if any
.type("{ctrl+j}") // jump to field
.type(field_label)
.wait(500)
.type("{enter}")
.wait(200)
.type("{enter}")
.wait(500);
};

const type_value = (value) => {
cy.focused()
.clear()
.type(value)
.type("{esc}");
};

const undo = () => cy.get("body").type("{esc}").type("{ctrl+z}").wait(500);
const redo = () => cy.get("body").type("{esc}").type("{ctrl+y}").wait(500);

cy.new_form('User');

jump_to_field("Email");
type_value("admin@example.com");

jump_to_field("Username");
type_value("admin42");

jump_to_field("Birth Date");
type_value("12-31-01");

jump_to_field("Send Welcome Email");
cy.focused().uncheck()

// make a mistake
jump_to_field("Username");
type_value("admin24");

// undo behaviour
undo();
cy.get_field("username").should('have.value', 'admin42');

// redo behaviour
redo();
cy.get_field("username").should('have.value', 'admin24');

// undo everything & redo everything, ensure same values at the end
undo(); undo(); undo(); undo(); undo();
redo(); redo(); redo(); redo(); redo();

cy.get_field("username").should('have.value', 'admin24');
cy.get_field("email").should('have.value', 'admin@example.com');
cy.get_field("birth_date").should('have.value', '12-31-2001'); // parsed value
cy.get_field("send_welcome_email").should('not.be.checked');
});
});

+ 9
- 0
frappe/public/js/frappe/form/controls/base_control.js 查看文件

@@ -187,6 +187,15 @@ frappe.ui.form.Control = class BaseControl {
return Promise.resolve();
}

const old_value = this.get_model_value();
this.frm?.undo_manager?.record_change({
fieldname: me.df.fieldname,
old_value,
new_value: value,
doctype: this.doctype,
docname: this.docname,
is_child: Boolean(this.doc?.parenttype)
});
this.inside_change_event = true;
function set(value) {
me.inside_change_event = false;


+ 28
- 2
frappe/public/js/frappe/form/form.js 查看文件

@@ -13,6 +13,7 @@ import './script_helpers';
import './sidebar/form_sidebar';
import './footer/footer';
import './form_tour';
import { UndoManager } from './undo_manager';

frappe.ui.form.Controller = class FormController {
constructor(opts) {
@@ -38,6 +39,7 @@ frappe.ui.form.Form = class FrappeForm {
this.fetch_dict = {};
this.parent = parent;
this.doctype_layout = frappe.get_doc('DocType Layout', doctype_layout_name);
this.undo_manager = new UndoManager({frm: this});
this.setup_meta(doctype);

this.beforeUnloadListener = (event) => {
@@ -143,6 +145,26 @@ frappe.ui.form.Form = class FrappeForm {
condition: () => !this.is_new()
});

// Undo and redo
frappe.ui.keys.add_shortcut({
shortcut: 'ctrl+z',
action: () => this.undo_manager.undo(),
page: this.page,
description: __('Undo last action'),
});
frappe.ui.keys.add_shortcut({
shortcut: 'shift+ctrl+z',
action: () => this.undo_manager.redo(),
page: this.page,
description: __('Redo last action'),
});
frappe.ui.keys.add_shortcut({
shortcut: 'ctrl+y',
action: () => this.undo_manager.redo(),
page: this.page,
description: __('Redo last action'),
});

let grid_shortcut_keys = [
{
'shortcut': 'Up Arrow',
@@ -357,6 +379,8 @@ frappe.ui.form.Form = class FrappeForm {

cur_frm = this;

this.undo_manager.erase_history();

if(this.docname) { // document to show
this.save_disabled = false;
// set the doc
@@ -1761,7 +1785,7 @@ frappe.ui.form.Form = class FrappeForm {
return sum;
}

scroll_to_field(fieldname) {
scroll_to_field(fieldname, focus=true) {
let field = this.get_field(fieldname);
if (!field) return;

@@ -1781,7 +1805,9 @@ frappe.ui.form.Form = class FrappeForm {
frappe.utils.scroll_to($el, true, 15);

// focus if text field
$el.find('input, select, textarea').focus();
if (focus) {
$el.find('input, select, textarea').focus();
}

// highlight control inside field
let control_element = $el.find('.form-control')


+ 81
- 0
frappe/public/js/frappe/form/undo_manager.js 查看文件

@@ -0,0 +1,81 @@
export class UndoManager {
constructor({ frm }) {
this.frm = frm;
this.undo_stack = [];
this.redo_stack = [];
}
record_change({
fieldname,
old_value,
new_value,
doctype,
docname,
is_child,
}) {
if (old_value == new_value) {
return;
}

this.undo_stack.push({
fieldname,
old_value,
new_value,
doctype,
docname,
is_child,
});
}

erase_history() {
this.undo_stack = [];
this.redo_stack = [];
}

undo() {
const change = this.undo_stack.pop();
if (change) {
this._apply_change(change);
this._push_reverse_entry(change, this.redo_stack);
} else {
this._show_alert(__("Nothing left to undo"));
}
}

redo() {
const change = this.redo_stack.pop();
if (change) {
this._apply_change(change);
this._push_reverse_entry(change, this.undo_stack);
} else {
this._show_alert(__("Nothing left to redo"));
}
}

_push_reverse_entry(change, stack) {
stack.push({
...change,
new_value: change.old_value,
old_value: change.new_value,
});
}

_apply_change(change) {
if (change.is_child) {
frappe.model.set_value(
change.doctype,
change.docname,
change.fieldname,
change.old_value
);
} else {
this.frm.set_value(change.fieldname, change.old_value);
this.frm.scroll_to_field(change.fieldname, false);
}
}

_show_alert(msg) {
// reduce duration
// keyboard interactions shouldn't have long running annoying toasts
frappe.show_alert(msg, 3);
}
}

Loading…
取消
儲存