diff --git a/client/app/components/EditParameterSettingsDialog.jsx b/client/app/components/EditParameterSettingsDialog.jsx index 7db1ee3003..f86d998bc8 100644 --- a/client/app/components/EditParameterSettingsDialog.jsx +++ b/client/app/components/EditParameterSettingsDialog.jsx @@ -153,6 +153,7 @@ function EditParameterSettingsDialog(props) { setParam({ ...param, title: e.target.value })} + data-test="ParameterTitleInput" /> diff --git a/client/app/components/ParameterApplyButton.jsx b/client/app/components/ParameterApplyButton.jsx index 1808ac0542..c41ec3898d 100644 --- a/client/app/components/ParameterApplyButton.jsx +++ b/client/app/components/ParameterApplyButton.jsx @@ -1,14 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { react2angular } from 'react2angular'; import Button from 'antd/lib/button'; import Badge from 'antd/lib/badge'; import Tooltip from 'antd/lib/tooltip'; import { KeyboardShortcuts } from '@/services/keyboard-shortcuts'; -function ParameterApplyButton({ paramCount, onClick, isApplying }) { - // show spinner when applying (also when count is empty so the fade out is consistent) - const icon = isApplying || !paramCount ? 'spinner fa-pulse' : 'check'; +function ParameterApplyButton({ paramCount, onClick }) { + // show spinner when count is empty so the fade out is consistent + const icon = !paramCount ? 'spinner fa-pulse' : 'check'; return (
@@ -28,11 +27,6 @@ function ParameterApplyButton({ paramCount, onClick, isApplying }) { ParameterApplyButton.propTypes = { onClick: PropTypes.func.isRequired, paramCount: PropTypes.number.isRequired, - isApplying: PropTypes.bool.isRequired, }; -export default function init(ngModule) { - ngModule.component('parameterApplyButton', react2angular(ParameterApplyButton)); -} - -init.init = true; +export default ParameterApplyButton; diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index fe8e82b901..6eb1d41ff2 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -14,7 +14,7 @@ import Input from 'antd/lib/input'; import Radio from 'antd/lib/radio'; import Form from 'antd/lib/form'; import Tooltip from 'antd/lib/tooltip'; -import { ParameterValueInput } from '@/components/ParameterValueInput'; +import ParameterValueInput from '@/components/ParameterValueInput'; import { ParameterMappingType } from '@/services/widget'; import { Parameter } from '@/services/query'; import { HelpTrigger } from '@/components/HelpTrigger'; diff --git a/client/app/components/ParameterValueInput.jsx b/client/app/components/ParameterValueInput.jsx index 1cc7ec3ce1..9d75fbb622 100644 --- a/client/app/components/ParameterValueInput.jsx +++ b/client/app/components/ParameterValueInput.jsx @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { react2angular } from 'react2angular'; import Select from 'antd/lib/select'; import Input from 'antd/lib/input'; import InputNumber from 'antd/lib/input-number'; @@ -19,7 +18,7 @@ const multipleValuesProps = { maxTagPlaceholder: num => `+${num.length} more`, }; -export class ParameterValueInput extends React.Component { +class ParameterValueInput extends React.Component { static propTypes = { type: PropTypes.string, value: PropTypes.any, // eslint-disable-line react/forbid-prop-types @@ -194,34 +193,4 @@ export class ParameterValueInput extends React.Component { } } -export default function init(ngModule) { - ngModule.component('parameterValueInput', { - template: ` - - `, - bindings: { - param: '<', - }, - controller($scope) { - this.setValue = (value, isDirty) => { - if (isDirty) { - this.param.setPendingValue(value); - } else { - this.param.clearPendingValue(); - } - $scope.$apply(); - }; - }, - }); - ngModule.component('parameterValueInputImpl', react2angular(ParameterValueInput)); -} - -init.init = true; +export default ParameterValueInput; diff --git a/client/app/components/ParameterValueInput.less b/client/app/components/ParameterValueInput.less index fb99a9542f..9921c74a94 100644 --- a/client/app/components/ParameterValueInput.less +++ b/client/app/components/ParameterValueInput.less @@ -5,9 +5,15 @@ .parameter-input { display: inline-block; position: relative; + width: 100%; - .@{ant-prefix}-input[type="text"] { - width: 195px; + .@{ant-prefix}-input, + .@{ant-prefix}-input-number { + min-width: 100% !important; + } + + .@{ant-prefix}-select { + width: 100%; } &[data-dirty] { @@ -18,65 +24,3 @@ } } } - -.parameter-container { - position: relative; - - .parameter-apply-button { - display: none; // default for mobile - - // "floating" on desktop - @media (min-width: 768px) { - position: absolute; - bottom: -42px; - left: -15px; - border-radius: 2px; - z-index: 1; - transition: opacity 150ms ease-out; - box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15); - background-color: #ffffff; - padding: 4px; - padding-left: 16px; - opacity: 0; - display: block; - pointer-events: none; // so tooltip doesn't remain after button hides - } - - &[data-show="true"] { - opacity: 1; - display: block; - pointer-events: auto; - } - - button { - padding: 0 8px 0 6px; - color: #2096f3; - border-color: #50acf6; - - // smaller on desktop - @media (min-width: 768px) { - font-size: 12px; - height: 27px; - } - - &:hover, &:focus, &:active { - background-color: #eef7fe; - } - - i { - margin-right: 3px; - } - } - - .ant-badge-count { - min-width: 15px; - height: 15px; - padding: 0 5px; - font-size: 10px; - line-height: 15px; - background: #f77b74; - border-radius: 7px; - box-shadow: 0px 0px 0 1px white, -1px 1px 0 1px #5d6f7d85; - } - } -} diff --git a/client/app/components/Parameters.jsx b/client/app/components/Parameters.jsx new file mode 100644 index 0000000000..afbd2b86fc --- /dev/null +++ b/client/app/components/Parameters.jsx @@ -0,0 +1,207 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { size, filter, forEach, extend } from 'lodash'; +import { react2angular } from 'react2angular'; +import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc'; +import { $location } from '@/services/ng'; +import { Parameter } from '@/services/query'; +import ParameterApplyButton from '@/components/ParameterApplyButton'; +import ParameterValueInput from '@/components/ParameterValueInput'; +import EditParameterSettingsDialog from './EditParameterSettingsDialog'; +import { toHuman } from '@/filters'; + +import './Parameters.less'; + +const DragHandle = sortableHandle(({ parameterName }) => ( +
+)); + +const SortableItem = sortableElement(({ className, parameterName, disabled, children }) => ( +
+ {!disabled && } + {children} +
+)); +const SortableContainer = sortableContainer(({ children }) => children); + +function updateUrl(parameters) { + const params = extend({}, $location.search()); + parameters.forEach((param) => { + extend(params, param.toUrlParams()); + }); + Object.keys(params).forEach(key => params[key] == null && delete params[key]); + $location.search(params); +} + +export class Parameters extends React.Component { + static propTypes = { + parameters: PropTypes.arrayOf(PropTypes.instanceOf(Parameter)), + editable: PropTypes.bool, + disableUrlUpdate: PropTypes.bool, + onValuesChange: PropTypes.func, + onPendingValuesChange: PropTypes.func, + onParametersEdit: PropTypes.func, + }; + + static defaultProps = { + parameters: [], + editable: false, + disableUrlUpdate: false, + onValuesChange: () => {}, + onPendingValuesChange: () => {}, + onParametersEdit: () => {}, + } + + constructor(props) { + super(props); + const { parameters } = props; + this.state = { parameters, dragging: false }; + if (!props.disableUrlUpdate) { + updateUrl(parameters); + } + } + + componentDidUpdate = (prevProps) => { + const { parameters, disableUrlUpdate } = this.props; + if (prevProps.parameters !== parameters) { + this.setState({ parameters }); + if (!disableUrlUpdate) { + updateUrl(parameters); + } + } + }; + + handleKeyDown = (e) => { + // Cmd/Ctrl/Alt + Enter + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) { + e.stopPropagation(); + this.applyChanges(); + } + }; + + setPendingValue = (param, value, isDirty) => { + const { onPendingValuesChange } = this.props; + this.setState(({ parameters }) => { + if (isDirty) { + param.setPendingValue(value); + } else { + param.clearPendingValue(); + } + onPendingValuesChange(); + return { parameters }; + }); + }; + + moveParameter = ({ oldIndex, newIndex }) => { + const { onParametersEdit } = this.props; + if (oldIndex !== newIndex) { + this.setState(({ parameters }) => { + parameters.splice(newIndex, 0, parameters.splice(oldIndex, 1)[0]); + onParametersEdit(); + return { parameters }; + }); + } + this.setState({ dragging: false }); + }; + + onBeforeSortStart = () => { + this.setState({ dragging: true }); + }; + + applyChanges = () => { + const { onValuesChange, disableUrlUpdate } = this.props; + this.setState(({ parameters }) => { + forEach(parameters, p => p.applyPendingValue()); + onValuesChange(); + if (!disableUrlUpdate) { + updateUrl(parameters); + } + return { parameters }; + }); + }; + + showParameterSettings = (parameter, index) => { + const { onParametersEdit } = this.props; + EditParameterSettingsDialog + .showModal({ parameter }) + .result.then((updated) => { + this.setState(({ parameters }) => { + const updatedParameter = extend(parameter, updated); + parameters[index] = new Parameter(updatedParameter, updatedParameter.parentQueryId); + onParametersEdit(); + return { parameters }; + }); + }); + }; + + renderParameter(param, index) { + const { editable } = this.props; + return ( +
+
+ + {editable && ( + + )} +
+ this.setPendingValue(param, value, isDirty)} + /> +
+ ); + } + + render() { + const { parameters, dragging } = this.state; + const { editable } = this.props; + const dirtyParamCount = size(filter(parameters, 'hasPendingValue')); + return ( + +
+ {parameters.map((param, index) => ( + + {this.renderParameter(param, index)} + + ))} + + +
+
+ ); + } +} + +export default function init(ngModule) { + ngModule.component('parameters', react2angular(Parameters)); +} + +init.init = true; diff --git a/client/app/components/Parameters.less b/client/app/components/Parameters.less new file mode 100644 index 0000000000..304c1a8f86 --- /dev/null +++ b/client/app/components/Parameters.less @@ -0,0 +1,124 @@ +@import '../assets/less/ant'; + +.drag-handle { + background: linear-gradient(90deg, transparent 0px, white 1px, white 2px) + center, + linear-gradient(transparent 0px, white 1px, white 2px) center, #111111; + background-size: 2px 2px; + display: inline-block; + width: 6px; + height: 36px; + vertical-align: bottom; + margin-right: 5px; + cursor: move; +} + +.parameter-block { + display: inline-block; + background: white; + padding: 0 12px 6px 0; + vertical-align: top; + + .parameter-container[data-draggable] & { + margin: 4px 0 0 4px; + padding: 3px 6px 6px; + } + + &.parameter-dragged { + box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15); + width: auto !important; + } +} + +.parameter-heading { + display: flex; + align-items: center; + padding-bottom: 4px; + + label { + margin-bottom: 1px; + overflow: hidden; + text-overflow: ellipsis; + min-width: 100%; + max-width: 195px; + white-space: nowrap; + + .parameter-block[data-editable] & { + min-width: calc(100% - 27px); // make room for settings button + max-width: 195px - 27px; + } + } +} + +.parameter-container { + position: relative; + + &[data-draggable] { + padding: 0 4px 4px 0; + transition: background-color 200ms ease-out; + transition-delay: 300ms; // short pause before returning to original bgcolor + } + + &[data-dragging] { + transition-delay: 0s; + background-color: #f6f8f9; + } + + .parameter-apply-button { + display: none; // default for mobile + + // "floating" on desktop + @media (min-width: 768px) { + position: absolute; + bottom: -36px; + left: -15px; + border-radius: 2px; + z-index: 1; + transition: opacity 150ms ease-out; + box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15); + background-color: #ffffff; + padding: 4px; + padding-left: 16px; + opacity: 0; + display: block; + pointer-events: none; // so tooltip doesn't remain after button hides + } + + &[data-show="true"] { + opacity: 1; + display: block; + pointer-events: auto; + } + + button { + padding: 0 8px 0 6px; + color: #2096f3; + border-color: #50acf6; + + // smaller on desktop + @media (min-width: 768px) { + font-size: 12px; + height: 27px; + } + + &:hover, &:focus, &:active { + background-color: #eef7fe; + } + + i { + margin-right: 3px; + } + } + + .ant-badge-count { + min-width: 15px; + height: 15px; + padding: 0 5px; + font-size: 10px; + line-height: 15px; + background: #f77b74; + border-radius: 7px; + box-shadow: 0px 0px 0 1px white, -1px 1px 0 1px #5d6f7d85; + } + } +} diff --git a/client/app/components/parameters.html b/client/app/components/parameters.html deleted file mode 100644 index 3e493ea188..0000000000 --- a/client/app/components/parameters.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
- - - -
- -
diff --git a/client/app/components/parameters.js b/client/app/components/parameters.js deleted file mode 100644 index 515abe5df5..0000000000 --- a/client/app/components/parameters.js +++ /dev/null @@ -1,98 +0,0 @@ -import { extend, filter, forEach, size } from 'lodash'; -import template from './parameters.html'; -import EditParameterSettingsDialog from './EditParameterSettingsDialog'; - -function ParametersDirective($location, KeyboardShortcuts) { - return { - restrict: 'E', - transclude: true, - scope: { - parameters: '=', - syncValues: '=?', - editable: '=?', - changed: '&onChange', - onUpdated: '=', - onValuesChange: '=', - applyOnKeyboardShortcut: ' scope.onApply(), - 'alt+enter': () => scope.onApply(), - }; - - const onFocus = () => { KeyboardShortcuts.bind(shortcuts); }; - const onBlur = () => { KeyboardShortcuts.unbind(shortcuts); }; - - el.addEventListener('focus', onFocus, true); - el.addEventListener('blur', onBlur, true); - - scope.$on('$destroy', () => { - KeyboardShortcuts.unbind(shortcuts); - el.removeEventListener('focus', onFocus); - el.removeEventListener('blur', onBlur); - }); - - // is this the correct location for this logic? - if (scope.syncValues !== false) { - scope.$watch( - 'parameters', - () => { - if (scope.changed) { - scope.changed({}); - } - const params = extend({}, $location.search()); - scope.parameters.forEach((param) => { - extend(params, param.toUrlParams()); - }); - Object.keys(params).forEach(key => params[key] == null && delete params[key]); - $location.search(params); - }, - true, - ); - } - - scope.showParameterSettings = (parameter, index) => { - EditParameterSettingsDialog - .showModal({ parameter }) - .result.then((updated) => { - scope.parameters[index] = extend(parameter, updated).setValue(updated.value); - scope.onUpdated(); - }); - }; - - scope.dirtyParamCount = 0; - scope.$watch( - 'parameters', - () => { - scope.dirtyParamCount = size(filter(scope.parameters, 'hasPendingValue')); - }, - true, - ); - - scope.isApplying = false; - scope.applyChanges = () => { - scope.isApplying = true; - forEach(scope.parameters, p => p.applyPendingValue()); - scope.isApplying = false; - }; - - scope.onApply = () => { - if (!scope.dirtyParamCount) { - return false; // so keyboard shortcut doesn't run needlessly - } - - scope.$apply(scope.applyChanges); - scope.onValuesChange(); - }; - }, - }; -} - -export default function init(ngModule) { - ngModule.directive('parameters', ParametersDirective); -} - -init.init = true; diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html index e2e50ae0ea..7837571f08 100644 --- a/client/app/pages/queries/query.html +++ b/client/app/pages/queries/query.html @@ -191,8 +191,8 @@

- +
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" },