Migrated query edit/add param dialog to React/AntD/Hooks (#3488)

This commit is contained in:
Ran Byron
2019-02-28 16:31:34 +02:00
committed by GitHub
parent dab35acd2c
commit 34e03b01bb
7 changed files with 223 additions and 135 deletions

View File

@@ -953,3 +953,7 @@ text.slicetext {
pointer-events: none;
cursor: not-allowed;
}
.select-option-divider {
margin: 10px 0 !important;
}

View File

@@ -0,0 +1,189 @@
import { includes, words, capitalize, clone, isNull } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import Form from 'antd/lib/form';
import Checkbox from 'antd/lib/checkbox';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import Divider from 'antd/lib/divider';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { QuerySelector } from '@/components/QuerySelector';
import { Query } from '@/services/query';
const { Option } = Select;
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
function getDefaultTitle(text) {
return capitalize(words(text).join(' ')); // humanize
}
function NameInput({ name, onChange, existingNames, setValidation }) {
let helpText = `This is what will be added to your query editor {{ ${name} }}`;
let validateStatus = '';
if (!name) {
helpText = 'Choose a keyword for this parameter';
setValidation(false);
} else if (includes(existingNames, name)) {
helpText = 'Parameter with this name already exists';
setValidation(false);
validateStatus = 'error';
} else {
setValidation(true);
}
return (
<Form.Item
required
label="Keyword"
help={helpText}
validateStatus={validateStatus}
{...formItemProps}
>
<Input onChange={e => onChange(e.target.value)} />
</Form.Item>
);
}
NameInput.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
existingNames: PropTypes.arrayOf(PropTypes.string).isRequired,
setValidation: PropTypes.func.isRequired,
};
function EditParameterSettingsDialog(props) {
const [param, setParam] = useState(clone(props.parameter));
const [isNameValid, setIsNameValid] = useState(true);
const [initialQuery, setInitialQuery] = useState();
const isNew = !props.parameter.name;
// fetch query by id
useEffect(() => {
const { queryId } = props.parameter;
if (queryId) {
Query.get({ id: queryId }, (query) => {
setInitialQuery(query);
});
}
}, []);
function isFulfilled() {
// name
if (!isNameValid) {
return false;
}
// title
if (param.title === '') {
return false;
}
// query
if (param.type === 'query' && !param.queryId) {
return false;
}
return true;
}
function onConfirm() {
// update title to default
if (!param.title) {
// forced to do this cause param won't update in time for save
param.title = getDefaultTitle(param.name);
setParam(param);
}
props.dialog.close(param);
}
return (
<Modal
{...props.dialog.props}
title={isNew ? 'Add Parameter' : param.name}
onOk={onConfirm}
okText={isNew ? 'Add Parameter' : null}
okButtonProps={{ disabled: !isFulfilled() }}
>
<Form layout="horizontal">
{isNew && (
<NameInput
name={param.name}
onChange={name => setParam({ ...param, name })}
setValidation={setIsNameValid}
existingNames={props.existingParams}
/>
)}
<Form.Item label="Title" {...formItemProps}>
<Input
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
onChange={e => setParam({ ...param, title: e.target.value })}
/>
</Form.Item>
<Form.Item label="Type" {...formItemProps}>
<Select value={param.type} onChange={type => setParam({ ...param, type })}>
<Option value="text">Text</Option>
<Option value="number">Number</Option>
<Option value="enum">Dropdown List</Option>
<Option value="query">Query Based Dropdown List</Option>
<Option disabled key="dv1">
<Divider className="select-option-divider" />
</Option>
<Option value="date">Date</Option>
<Option value="datetime-local">Date and Time</Option>
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
<Option disabled key="dv2">
<Divider className="select-option-divider" />
</Option>
<Option value="date-range">Date Range</Option>
<Option value="datetime-range">Date and Time Range</Option>
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
</Select>
</Form.Item>
{includes(['date', 'datetime-local', 'datetime-with-seconds'], param.type) && (
<Form.Item label=" " colon={false} {...formItemProps}>
<Checkbox
defaultChecked={param.useCurrentDateTime}
onChange={e => setParam({ ...param, useCurrentDateTime: e.target.checked })}
>
Default to Today/Now if no other value is set
</Checkbox>
</Form.Item>
)}
{param.type === 'enum' && (
<Form.Item label="Values" help="Dropdown list values (newline delimeted)" {...formItemProps}>
<Input.TextArea
rows={3}
value={param.enumOptions}
onChange={e => setParam({ ...param, enumOptions: e.target.value })}
/>
</Form.Item>
)}
{param.type === 'query' && (
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
<QuerySelector
selectedQuery={initialQuery}
onChange={q => setParam({ ...param, queryId: q && q.id })}
type="select"
/>
</Form.Item>
)}
</Form>
</Modal>
);
}
EditParameterSettingsDialog.propTypes = {
parameter: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dialog: DialogPropType.isRequired,
existingParams: PropTypes.arrayOf(PropTypes.string),
};
EditParameterSettingsDialog.defaultProps = {
existingParams: [],
};
export default wrapDialog(EditParameterSettingsDialog);

View File

@@ -1,65 +0,0 @@
<form novalidate ng-submit="$ctrl.close({ $value: $ctrl.parameter })">
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.dismiss()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{ $ctrl.isNewParameter ? 'Add Parameter' : $ctrl.parameter.name }}</h4>
</div>
<div class="modal-body">
<div class="form">
<div class="form-group" ng-if="$ctrl.isNewParameter" ng-class="{'has-error': $ctrl.parameterAlreadyExists($ctrl.parameter.name)}">
<label>Keyword</label>
<input type="text" class="form-control" ng-model="$ctrl.parameter.name" autofocus ng-change="$ctrl.updateTitle()">
<div class="help-block" ng-if="!$ctrl.parameterAlreadyExists($ctrl.parameter.name)">
This is what will be added to your query editor<span ng-if="$ctrl.parameter.name != ''">: \{\{&nbsp;{{ $ctrl.parameter.name }}&nbsp;\}\}</span>
</div>
<div class="help-block" ng-if="$ctrl.parameterAlreadyExists($ctrl.parameter.name)">
Parameter with this name already exists.
</div>
</div>
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" ng-model="$ctrl.parameter.title" ng-change="$ctrl.shouldGenerateTitle = false">
</div>
<div class="form-group">
<label>Type</label>
<select ng-model="$ctrl.parameter.type" class="form-control">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="enum">Dropdown List</option>
<option value="query">Query Based Dropdown List</option>
<option disabled>--------------------------------------------------</option>
<option value="date">Date</option>
<option value="datetime-local">Date and Time</option>
<option value="datetime-with-seconds">Date and Time (with seconds)</option>
<option disabled>--------------------------------------------------</option>
<option value="date-range">Date Range</option>
<option value="datetime-range">Date and Time Range</option>
<option value="datetime-range-with-seconds">Date and Time Range (with seconds)</option>
</select>
</div>
<div class="form-group" ng-if="['date', 'datetime-local', 'datetime-with-seconds'].indexOf($ctrl.parameter.type) >= 0">
<label>
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.useCurrentDateTime"> Use Today/Now as default value if no other value is set
</label>
</div>
<div class="form-group" ng-if="$ctrl.parameter.type === 'enum'">
<label>Dropdown List Values (newline delimited)</label>
<textarea class="form-control" rows="3" ng-model="$ctrl.parameter.enumOptions"></textarea>
</div>
<div class="form-group" ng-if="$ctrl.parameter.type === 'query'">
<label>Query to load dropdown values from:</label>
<ui-select ng-model="$ctrl.parameter.queryId" reset-search-input="false">
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q.id as q in $ctrl.queries" refresh="$ctrl.searchQueries($select.search)" refresh-delay="0">
<div class="form-group" ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="modal-footer p-t-0" ng-if="$ctrl.isNewParameter">
<button type="button" class="btn btn-default pull-left" ng-click="$ctrl.dismiss()">Close</button>
<button type="submit" class="btn btn-primary pull-right" ng-disabled="($ctrl.parameter.name == '') || $ctrl.parameterAlreadyExists($ctrl.parameter.name)">+ Add Parameter</button>
</div>
</form>

View File

@@ -5,7 +5,7 @@
>
<div class="form-group m-r-10" ng-repeat="param in parameters">
<label class="parameter-label">{{param.title}}</label>
<button class="btn btn-default btn-xs" ng-if="editable" ng-click="showParameterSettings(param)">
<button class="btn btn-default btn-xs" ng-if="editable" ng-click="showParameterSettings(param, $index)">
<i class="zmdi zmdi-settings"></i>
</button>
<parameter-value-input param="param"></parameter-value-input>

View File

@@ -1,53 +1,8 @@
import { includes, words, capitalize, extend } from 'lodash';
import { extend } from 'lodash';
import template from './parameters.html';
import parameterSettingsTemplate from './parameter-settings.html';
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
function humanize(str) {
return capitalize(words(str).join(' '));
}
const ParameterSettingsComponent = {
template: parameterSettingsTemplate,
bindings: {
resolve: '<',
close: '&',
dismiss: '&',
},
controller($sce, Query) {
'ngInject';
this.trustAsHtml = html => $sce.trustAsHtml(html);
this.parameter = this.resolve.parameter;
this.isNewParameter = this.parameter.name === '';
this.shouldGenerateTitle = this.isNewParameter && this.parameter.title === '';
this.parameterAlreadyExists = name => includes(this.resolve.existingParameters, name);
if (this.parameter.queryId) {
Query.get({ id: this.parameter.queryId }, (query) => {
this.queries = [query];
});
}
this.searchQueries = (term) => {
if (!term || term.length < 3) {
return;
}
Query.query({ q: term }, (results) => {
this.queries = results.results;
});
};
this.updateTitle = () => {
if (this.shouldGenerateTitle) {
this.parameter.title = humanize(this.parameter.name);
}
};
},
};
function ParametersDirective($location, $uibModal) {
function ParametersDirective($location) {
return {
restrict: 'E',
transclude: true,
@@ -56,6 +11,7 @@ function ParametersDirective($location, $uibModal) {
syncValues: '=?',
editable: '=?',
changed: '&onChange',
onUpdated: '=',
},
template,
link(scope) {
@@ -77,13 +33,13 @@ function ParametersDirective($location, $uibModal) {
);
}
scope.showParameterSettings = (param) => {
$uibModal.open({
component: 'parameterSettings',
resolve: {
parameter: param,
},
});
scope.showParameterSettings = (parameter, index) => {
EditParameterSettingsDialog
.showModal({ parameter })
.result.then((updated) => {
scope.parameters[index] = extend(parameter, updated);
scope.onUpdated();
});
};
},
};
@@ -91,7 +47,6 @@ function ParametersDirective($location, $uibModal) {
export default function init(ngModule) {
ngModule.directive('parameters', ParametersDirective);
ngModule.component('parameterSettings', ParameterSettingsComponent);
}
init.init = true;

View File

@@ -191,7 +191,7 @@
<section class="flex-fill p-relative t-body query-visualizations-wrapper">
<div class="d-flex flex-column p-b-15 p-absolute static-position__mobile" style="left: 0; top: 0; right: 0; bottom: 0;">
<div class="p-t-15 p-b-5" ng-if="query.getParametersDefs().length > 0">
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit"></parameters>
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit" on-updated="onParametersUpdated"></parameters>
</div>
<!-- Query Execution Status -->

View File

@@ -1,5 +1,6 @@
import { map, defer } from 'lodash';
import template from './query.html';
import EditParameterSettingsDialog from '@/components/EditParameterSettingsDialog';
function QuerySourceCtrl(
Events,
@@ -73,19 +74,15 @@ function QuerySourceCtrl(
};
$scope.addNewParameter = () => {
$uibModal
.open({
component: 'parameterSettings',
resolve: {
parameter: {
title: '',
name: '',
type: 'text',
value: null,
global: false,
},
existingParameters: () => map($scope.query.getParameters().get(), p => p.name),
EditParameterSettingsDialog
.showModal({
parameter: {
title: null,
name: '',
type: 'text',
value: null,
},
existingParams: map($scope.query.getParameters().get(), p => p.name),
})
.result.then((param) => {
param = $scope.query.getParameters().add(param);
@@ -94,6 +91,14 @@ function QuerySourceCtrl(
});
};
$scope.onParametersUpdated = () => {
// save if query clean
// https://discuss.redash.io/t/query-unsaved-changes-indication/3302/5
if (!$scope.isDirty) {
$scope.saveQuery();
}
};
$scope.listenForEditorCommand = f => $scope.$on('query-editor.command', f);
$scope.listenForResize = f => $scope.$parent.$on('angular-resizable.resizing', f);