From 930dd8af1fac500749c7a9efa2302b5f3f52b4d4 Mon Sep 17 00:00:00 2001 From: Murtuza Zabuawala Date: Tue, 21 Nov 2017 17:22:25 +0000 Subject: [PATCH] Some browsers don't properly support tri-state checkboxes, so create our own control to handle true/false/null. Fixes #2848 --- web/pgadmin/feature_tests/test_data.json | 15 ++- .../feature_tests/view_data_dml_queries.py | 16 ++- .../static/css/bootstrap.overrides.css | 28 ++++ web/pgadmin/static/js/slickgrid/editors.js | 127 +++++++++++++++--- 4 files changed, 157 insertions(+), 29 deletions(-) diff --git a/web/pgadmin/feature_tests/test_data.json b/web/pgadmin/feature_tests/test_data.json index ac00b0baa..6c53cc8fa 100644 --- a/web/pgadmin/feature_tests/test_data.json +++ b/web/pgadmin/feature_tests/test_data.json @@ -13,13 +13,14 @@ "10": ["[61,62]", "[61,62]", "json"], "11": ["", "true", "bool"], "12": ["", "[null]", "bool"], - "13": ["", "[null]", "text[]"], - "14": ["{}", "{}", "text[]"], - "15": ["{data,,'',\"\",\\'\\',\\\"\\\"}", "{data,[null],,,'',\"\"}", "text[]"], - "16": ["{}", "{}", "int[]"], - "17": ["{123,,456}", "{123,[null],456}", "int[]"], - "18": ["", "[null]", "boolean[]"], - "19": ["{false,,true}", "{false,[null],true}", "boolean[]"] + "13": ["", "false", "bool"], + "14": ["", "[null]", "text[]"], + "15": ["{}", "{}", "text[]"], + "16": ["{data,,'',\"\",\\'\\',\\\"\\\"}", "{data,[null],,,'',\"\"}", "text[]"], + "17": ["{}", "{}", "int[]"], + "18": ["{123,,456}", "{123,[null],456}", "int[]"], + "19": ["", "[null]", "boolean[]"], + "20": ["{false,,true}", "{false,[null],true}", "boolean[]"] } } } diff --git a/web/pgadmin/feature_tests/view_data_dml_queries.py b/web/pgadmin/feature_tests/view_data_dml_queries.py index 153d7962b..e5b0c0913 100644 --- a/web/pgadmin/feature_tests/view_data_dml_queries.py +++ b/web/pgadmin/feature_tests/view_data_dml_queries.py @@ -65,8 +65,9 @@ CREATE TABLE public.defaults text_null4 text COLLATE pg_catalog."default", json_defaults json DEFAULT '[51, 52]'::json, json_null json, - boolean_defaults boolean DEFAULT true, + boolean_true boolean DEFAULT true, boolean_null boolean, + boolean_false boolean, text_arr text[], text_arr_empty text[], text_arr_null text[], @@ -194,12 +195,17 @@ CREATE TABLE public.defaults self.page.find_by_xpath("//*[contains(@class, 'pg_text_editor')]" "//button[contains(@class, 'fa-save')]").click() else: + # Boolean editor test for to True click if data[1] == 'true': - checkbox_el = cell_el.find_element_by_xpath(".//input") + checkbox_el = cell_el.find_element_by_xpath(".//*[contains(@class, 'multi-checkbox')]") checkbox_el.click() - ActionChains(self.driver).move_to_element(checkbox_el).double_click( - checkbox_el - ).perform() + # Boolean editor test for to False click + elif data[1] == 'false': + checkbox_el = cell_el.find_element_by_xpath(".//*[contains(@class, 'multi-checkbox')]") + # Sets true + checkbox_el.click() + # Sets false + ActionChains(self.driver).click(checkbox_el).perform() def _tables_node_expandable(self): self.page.toggle_open_tree_item(self.server['name']) diff --git a/web/pgadmin/static/css/bootstrap.overrides.css b/web/pgadmin/static/css/bootstrap.overrides.css index 8790bba23..fcc030d61 100755 --- a/web/pgadmin/static/css/bootstrap.overrides.css +++ b/web/pgadmin/static/css/bootstrap.overrides.css @@ -1399,3 +1399,31 @@ body { font-size: 8px; color: #888888; } + +/* CSS for custom checkbox editor in SlickGrid */ +.multi-checkbox .check { + display: inline-block; + vertical-align: top; + width: 15px; + height: 15px; + border: 1px solid #333; + margin: 3px; + text-align: center; + line-height: 15px; +} + +.multi-checkbox .check.unchecked { + background: #fff; +} + +.multi-checkbox .check.partial { + background: #cccccc; +} + +.multi-checkbox .check.checked:after { + content: "\2713"; +} + +.multi-checkbox .check.partial:after { + content: "\fe56"; +} diff --git a/web/pgadmin/static/js/slickgrid/editors.js b/web/pgadmin/static/js/slickgrid/editors.js index 80f7cce85..ab0bff0d6 100644 --- a/web/pgadmin/static/js/slickgrid/editors.js +++ b/web/pgadmin/static/js/slickgrid/editors.js @@ -12,10 +12,10 @@ "pgText": pgTextEditor, "JsonText": JsonTextEditor, "CustomNumber": CustomNumberEditor, + "Checkbox": pgCheckboxEditor, // Below editor will read only editors, Just to display data "ReadOnlyText": ReadOnlyTextEditor, "ReadOnlyCheckbox": ReadOnlyCheckboxEditor, - "Checkbox": CheckboxEditor, // Override editor to implement checkbox with three states "ReadOnlypgText": ReadOnlypgTextEditor, "ReadOnlyJsonText": ReadOnlyJsonTextEditor } @@ -545,7 +545,7 @@ */ function CheckboxEditor(args) { var $select, el; - var defaultValue; + var defaultValue, previousState; var scope = this; this.init = function () { @@ -564,22 +564,31 @@ checkbox_status = 1; } switch(checkbox_status) { - // unchecked, going indeterminate + // State 0 will come when we had indeterminate state case 0: - el.prop('indeterminate', true); - el.data('checked', 2); // determines next checkbox status + // We will check now + el.prop('checked', true); + el.data('checked', 1); break; - // indeterminate, going checked + // State 1 will come when we had checked state case 1: - el.prop('checked', true); + // We will uncheck now + el.prop('checked', false); + el.data('checked', 2); + break; + + // State 2 will come when we had unchecked state + case 2: + // We will set to indeterminate state + el.prop('indeterminate', true); el.data('checked', 0); break; - // checked, going unchecked + // Default, Set to indeterminate state default: - el.prop('checked', false); - el.data('checked', 1); + el.prop('indeterminate', true); + el.data('checked', 0); } }); }; @@ -594,18 +603,21 @@ this.loadValue = function (item) { defaultValue = item[args.column.field]; + previousState = 0; if (_.isNull(defaultValue)||_.isUndefined(defaultValue)) { $select.prop('indeterminate', true); - $select.data('checked', 2); + $select.data('checked', 0); } else { defaultValue = !!item[args.column.field]; if (defaultValue) { $select.prop('checked', true); - $select.data('checked', 0); + $select.data('checked', 1); + previousState = 1; } else { $select.prop('checked', false); - $select.data('checked', 1); + $select.data('checked', 2); + previousState = 2; } } }; @@ -622,10 +634,8 @@ }; this.isValueChanged = function () { - // var select_value = this.serializeValue(); - var select_value = $select.data('checked'); - return (!(select_value === 2 && (defaultValue == null || defaultValue == undefined))) && - (select_value !== defaultValue); + var currentState = $select.data('checked'); + return currentState !== previousState; }; this.validate = function () { @@ -1023,4 +1033,87 @@ this.init(); } + // Custom checkbox editor, We need it for runtime as it does not render + // indeterminate checkbox state + function pgCheckboxEditor(args) { + var $select, el; + var defaultValue, previousState; + var scope = this; + + this.init = function () { + $select = $("
"); + $select.appendTo(args.container); + $select.focus(); + + // The following code is taken from https://css-tricks.com/indeterminate-checkboxes/ + $select.bind("click", function (e) { + el = $(this); + var states = ["unchecked", "partial", "checked"]; + var curState = el.find(".check").data("state"); + curState++; + el.find(".check") + .removeClass("unchecked partial checked") + .addClass(states[curState % states.length]) + .data("state", curState % states.length); + }); + }; + + this.destroy = function () { + $select.remove(); + }; + + this.focus = function () { + $select.focus(); + }; + + this.loadValue = function (item) { + defaultValue = item[args.column.field]; + previousState = 1; + if (_.isNull(defaultValue)||_.isUndefined(defaultValue)) { + $select.find(".check").data("state", 1).addClass("partial"); + } + else { + defaultValue = !!item[args.column.field]; + if (defaultValue) { + $select.find(".check").data("state", 2).addClass("checked"); + previousState = 2; + } else { + $select.find(".check").data("state", 0).addClass("unchecked"); + previousState = 0; + } + } + }; + + this.serializeValue = function () { + if ($select.find(".check").data("state") == 1) { + return null; + } + return $select.find(".check").data("state") == 2 ? true : false; + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + var currentState = $select.find(".check").data("state"); + return currentState !== previousState; + }; + + this.validate = function () { + if (args.column.validator) { + var validationResults = args.column.validator(this.serializeValue()); + if (!validationResults.valid) { + return validationResults; + } + } + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + })(jQuery);