diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js
index aef1b66a39..592066f06a 100644
--- a/client/app/pages/queries/view.js
+++ b/client/app/pages/queries/view.js
@@ -215,6 +215,10 @@ function QueryViewCtrl(
$scope.loadTags = () => getTags('api/queries/tags').then(tags => map(tags, t => t.name));
+ $scope.applyParametersChanges = () => {
+ $scope.$apply();
+ };
+
$scope.saveQuery = (customOptions, data) => {
let request = data;
diff --git a/client/app/services/query.js b/client/app/services/query.js
index 317da77c11..c194f2782a 100644
--- a/client/app/services/query.js
+++ b/client/app/services/query.js
@@ -93,6 +93,10 @@ function collectParams(parts) {
return parameters;
}
+function isEmptyValue(value) {
+ return isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
+}
+
function isDateParameter(paramType) {
return includes(['date', 'datetime-local', 'datetime-with-seconds'], paramType);
}
@@ -164,10 +168,6 @@ export class Parameter {
return isNull(this.getValue());
}
- getValue(extra = {}) {
- return this.constructor.getValue(this, extra);
- }
-
get hasDynamicValue() {
if (isDateParameter(this.type)) {
return isDynamicDate(this.value);
@@ -188,9 +188,12 @@ export class Parameter {
return false;
}
+ getValue(extra = {}) {
+ return this.constructor.getValue(this, extra);
+ }
+
static getValue(param, extra = {}) {
const { value, type, useCurrentDateTime, multiValuesOptions } = param;
- const isEmptyValue = isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
if (isDateRangeParameter(type) && param.hasDynamicValue) {
const { dynamicValue } = param;
if (dynamicValue) {
@@ -211,7 +214,7 @@ export class Parameter {
return null;
}
- if (isEmptyValue) {
+ if (isEmptyValue(value)) {
// keep support for existing useCurentDateTime (not available in UI)
if (
includes(['date', 'datetime-local', 'datetime-with-seconds'], type) &&
@@ -325,7 +328,11 @@ export class Parameter {
}
get hasPendingValue() {
- return this.pendingValue !== undefined && this.pendingValue !== this.value;
+ // normalize empty values
+ const pendingValue = isEmptyValue(this.pendingValue) ? null : this.pendingValue;
+ const value = isEmptyValue(this.value) ? null : this.value;
+
+ return this.pendingValue !== undefined && pendingValue !== value;
}
get normalizedValue() {
diff --git a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js
index b30041e155..85541d8ebe 100644
--- a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js
+++ b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js
@@ -1,7 +1,7 @@
/* global cy */
import { createDashboard, addTextbox } from '../../support/redash-api';
-import { getWidgetTestId, editDashboard, dragBy, resizeBy } from '../../support/dashboard';
+import { getWidgetTestId, editDashboard, resizeBy } from '../../support/dashboard';
describe('Grid compliant widgets', () => {
@@ -26,19 +26,22 @@ describe('Grid compliant widgets', () => {
});
it('stays put when dragged under snap threshold', () => {
- dragBy(cy.get('@textboxEl'), 90)
+ cy.get('@textboxEl')
+ .dragBy(90)
.invoke('offset')
.should('have.property', 'left', 15); // no change, 15 -> 15
});
it('moves one column when dragged over snap threshold', () => {
- dragBy(cy.get('@textboxEl'), 110)
+ cy.get('@textboxEl')
+ .dragBy(110)
.invoke('offset')
.should('have.property', 'left', 215); // moved by 200, 15 -> 215
});
it('moves two columns when dragged over snap threshold', () => {
- dragBy(cy.get('@textboxEl'), 330)
+ cy.get('@textboxEl')
+ .dragBy(330)
.invoke('offset')
.should('have.property', 'left', 415); // moved by 400, 15 -> 415
});
@@ -49,7 +52,8 @@ describe('Grid compliant widgets', () => {
cy.route('POST', 'api/widgets/*').as('WidgetSave');
editDashboard();
- dragBy(cy.get('@textboxEl'), 330);
+ cy.get('@textboxEl')
+ .dragBy(330);
cy.wait('@WidgetSave');
});
});
diff --git a/client/cypress/integration/query/parameter_spec.js b/client/cypress/integration/query/parameter_spec.js
index a1e2bb656f..78a649e5fe 100644
--- a/client/cypress/integration/query/parameter_spec.js
+++ b/client/cypress/integration/query/parameter_spec.js
@@ -528,4 +528,82 @@ describe('Parameter', () => {
cy.getByTestId('ExecuteButton').should('not.be.disabled');
});
});
+
+ describe('Draggable', () => {
+ beforeEach(() => {
+ const queryData = {
+ name: 'Draggable',
+ query: "SELECT '{{param1}}', '{{param2}}', '{{param3}}', '{{param4}}' AS parameter",
+ options: {
+ parameters: [
+ { name: 'param1', title: 'Parameter 1', type: 'text' },
+ { name: 'param2', title: 'Parameter 2', type: 'text' },
+ { name: 'param3', title: 'Parameter 3', type: 'text' },
+ { name: 'param4', title: 'Parameter 4', type: 'text' },
+ ],
+ },
+ };
+
+ createQuery(queryData, false)
+ .then(({ id }) => cy.visit(`/queries/${id}/source`));
+
+ cy.get('.parameter-block')
+ .first()
+ .invoke('width')
+ .as('paramWidth');
+ });
+
+ const dragParam = (paramName, offsetLeft, offsetTop) => {
+ cy.getByTestId(`DragHandle-${paramName}`)
+ .trigger('mouseover')
+ .trigger('mousedown');
+
+ cy.get('.parameter-dragged .drag-handle')
+ .trigger('mousemove', offsetLeft, offsetTop, { force: true })
+ .trigger('mouseup', { force: true });
+ };
+
+ it('is possible to rearrange parameters', function () {
+ dragParam('param1', this.paramWidth, 1);
+ dragParam('param4', -this.paramWidth, 1);
+
+ cy.reload();
+
+ const expectedOrder = ['Parameter 2', 'Parameter 1', 'Parameter 4', 'Parameter 3'];
+ cy.get('.parameter-container label')
+ .each(($label, index) => expect($label).to.have.text(expectedOrder[index]));
+ });
+ });
+
+ describe('Parameter Settings', () => {
+ beforeEach(() => {
+ const queryData = {
+ name: 'Draggable',
+ query: "SELECT '{{parameter}}' AS parameter",
+ options: {
+ parameters: [
+ { name: 'parameter', title: 'Parameter', type: 'text' },
+ ],
+ },
+ };
+
+ createQuery(queryData, false)
+ .then(({ id }) => cy.visit(`/queries/${id}/source`));
+
+ cy.getByTestId('ParameterSettings-parameter').click();
+ });
+
+ it('changes the parameter title', () => {
+ cy.getByTestId('ParameterTitleInput')
+ .type('{selectall}New Parameter Name');
+ cy.getByTestId('SaveParameterSettings')
+ .click();
+
+ cy.contains('Query saved');
+ cy.reload();
+
+ cy.getByTestId('ParameterName-parameter')
+ .contains('label', 'New Parameter Name');
+ });
+ });
});
diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js
index 37654fccdf..67fd12244d 100644
--- a/client/cypress/support/commands.js
+++ b/client/cypress/support/commands.js
@@ -50,3 +50,18 @@ Cypress.Commands.add('fillInputs', (elements) => {
cy.getByTestId(testId).clear().type(value);
});
});
+
+Cypress.Commands.add('dragBy', { prevSubject: true }, (subject, offsetLeft, offsetTop, force = false) => {
+ if (!offsetLeft) {
+ offsetLeft = 1;
+ }
+ if (!offsetTop) {
+ offsetTop = 1;
+ }
+ return cy.wrap(subject)
+ .trigger('mouseover', { force })
+ .trigger('mousedown', 'topLeft', { force })
+ .trigger('mousemove', 1, 1, { force }) // must have at least 2 mousemove events for react-grid-layout to trigger onLayoutChange
+ .trigger('mousemove', offsetLeft, offsetTop, { force })
+ .trigger('mouseup', { force });
+});
diff --git a/client/cypress/support/dashboard/index.js b/client/cypress/support/dashboard/index.js
index b2a2224a69..cd7b38a0c1 100644
--- a/client/cypress/support/dashboard/index.js
+++ b/client/cypress/support/dashboard/index.js
@@ -37,24 +37,9 @@ export function shareDashboard() {
return cy.getByTestId('SecretAddress').invoke('val');
}
-export function dragBy(wrapper, offsetLeft, offsetTop, force = false) {
- if (!offsetLeft) {
- offsetLeft = 1;
- }
- if (!offsetTop) {
- offsetTop = 1;
- }
- return wrapper
- .trigger('mouseover', { force })
- .trigger('mousedown', 'topLeft', { force })
- .trigger('mousemove', 1, 1, { force }) // must have at least 2 mousemove events for react-grid-layout to trigger onLayoutChange
- .trigger('mousemove', offsetLeft, offsetTop, { force })
- .trigger('mouseup', { force });
-}
-
export function resizeBy(wrapper, offsetLeft = 0, offsetTop = 0) {
return wrapper
.within(() => {
- dragBy(cy.get(RESIZE_HANDLE_SELECTOR), offsetLeft, offsetTop, true);
+ cy.get(RESIZE_HANDLE_SELECTOR).dragBy(offsetLeft, offsetTop, true);
});
}
diff --git a/package-lock.json b/package-lock.json
index 00020ef7fe..ec83da854f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1652,6 +1652,22 @@
"resize-observer-polyfill": "^1.5.1",
"shallowequal": "^1.1.0",
"warning": "~4.0.3"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ },
+ "rc-progress": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.3.0.tgz",
+ "integrity": "sha512-hYBKFSsNgD7jsF8j+ZC1J8y5UIC2X/ktCYI/OQhQNSX6mGV1IXnUCjAd9gbLmzmpChPvKyymRNfckScUNiTpFQ==",
+ "requires": {
+ "babel-runtime": "6.x",
+ "prop-types": "^15.5.8"
+ }
+ }
}
},
"any-observable": {
@@ -1930,12 +1946,9 @@
"dev": true
},
"async-validator": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.8.5.tgz",
- "integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==",
- "requires": {
- "babel-runtime": "6.x"
- }
+ "version": "1.11.5",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.11.5.tgz",
+ "integrity": "sha512-XNtCsMAeAH1pdLMEg1z8/Bb3a8cdCbui9QbJATRFHHHW5kT6+NPI3zSVQUXgikTFITzsg+kYY5NTWhM2Orwt9w=="
},
"asynckit": {
"version": "0.4.0",
@@ -9710,7 +9723,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "dev": true,
"requires": {
"loose-envify": "^1.0.0"
}
@@ -14602,15 +14614,6 @@
"react-lifecycles-compat": "^3.0.4"
}
},
- "rc-progress": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.3.0.tgz",
- "integrity": "sha512-hYBKFSsNgD7jsF8j+ZC1J8y5UIC2X/ktCYI/OQhQNSX6mGV1IXnUCjAd9gbLmzmpChPvKyymRNfckScUNiTpFQ==",
- "requires": {
- "babel-runtime": "6.x",
- "prop-types": "^15.5.8"
- }
- },
"rc-rate": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.5.0.tgz",
@@ -14866,13 +14869,14 @@
}
},
"rc-util": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.6.0.tgz",
- "integrity": "sha512-rbgrzm1/i8mgfwOI4t1CwWK7wGe+OwX+dNa7PVMgxZYPBADGh86eD4OcJO1UKGeajIMDUUKMluaZxvgraQIOmw==",
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.8.4.tgz",
+ "integrity": "sha512-1B2h0/pMXfSUBRAgPdoDIKK5XBuzLBuLI9rLwUEW163SPoDvfb9jmg3ymBPtzne2jWgwtdNw4j0vIq/8Yo849A==",
"requires": {
"add-dom-event-listener": "^1.1.0",
"babel-runtime": "6.x",
"prop-types": "^15.5.10",
+ "react-lifecycles-compat": "^3.0.4",
"shallowequal": "^0.2.2"
},
"dependencies": {
@@ -14982,6 +14986,31 @@
"resize-observer-polyfill": "^1.5.0"
}
},
+ "react-sortable-hoc": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-1.9.1.tgz",
+ "integrity": "sha512-2VeofjRav8+eZeE5Nm/+b8mrA94rQ+gBsqhXi8pRBSjOWNqslU3ZEm+0XhSlfoXJY2lkgHipfYAUuJbDtCixRg==",
+ "requires": {
+ "@babel/runtime": "^7.2.0",
+ "invariant": "^2.2.4",
+ "prop-types": "^15.5.7"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz",
+ "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==",
+ "requires": {
+ "regenerator-runtime": "^0.13.2"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.3",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
+ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
+ }
+ }
+ },
"react-test-renderer": {
"version": "16.8.3",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.3.tgz",
diff --git a/package.json b/package.json
index d011d2573d..c30a8f3b5c 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"react-ace": "^6.1.0",
"react-dom": "^16.8.3",
"react-grid-layout": "git+https://github.com/getredash/react-grid-layout.git",
+ "react-sortable-hoc": "^1.9.1",
"react2angular": "^3.2.1",
"ui-select": "^0.19.8"
},