Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React version of UserEdit #3354

Merged
merged 44 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8c2aeda
Update DynamicForm export
gabrieldutra Jan 26, 2019
16361a4
Move UserShow to users folder
gabrieldutra Jan 26, 2019
afdceff
Migrate User profile header and create DynamicForm for basic data
gabrieldutra Jan 26, 2019
8cfb157
Update UserShow to use UserProfile prop
gabrieldutra Jan 26, 2019
c456fb1
Add API Key input
gabrieldutra Jan 27, 2019
d4fc964
Add handler to regenerate API Key button
gabrieldutra Jan 28, 2019
2917e22
Handle user profile save
gabrieldutra Jan 28, 2019
e1624c9
Merge branch 'master' into user-edit
gabrieldutra Jan 28, 2019
fff4e7a
Add readOnly prop to DynamicForm and begin disabled user behavior
gabrieldutra Jan 28, 2019
d17266e
Add Change Password Modal
gabrieldutra Jan 29, 2019
576c3d0
Remove action buttons for disabled users
gabrieldutra Jan 29, 2019
d891df1
Add send password reset behavior
gabrieldutra Jan 31, 2019
0262e08
Merge remote-tracking branch 'fork/master' into user-edit
gabrieldutra Feb 2, 2019
fb1f571
Add minLength and password comparison to Password Modal
gabrieldutra Feb 2, 2019
7c6f11e
Resend Invitation button
gabrieldutra Feb 2, 2019
82237e7
Add Convert User Info
gabrieldutra Feb 3, 2019
f85668b
Fix UserShow test
gabrieldutra Feb 3, 2019
3a9d794
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 3, 2019
f5c4090
Some code updates
gabrieldutra Feb 3, 2019
6cf8b2d
Add enable/disable user button
gabrieldutra Feb 3, 2019
8b7336b
Add UserPolicy as an idea
gabrieldutra Feb 3, 2019
ddd9758
Remove UserPolicy
gabrieldutra Feb 3, 2019
f9984e0
Create Edit Profile spec
gabrieldutra Feb 4, 2019
f8bc318
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 4, 2019
e4f83ab
Move User profile screenshot to Edit Profile Spec
gabrieldutra Feb 4, 2019
80d5809
Add tests for saving user and changing password errors
gabrieldutra Feb 5, 2019
3522cdc
CC is back :) - Fix trailing spaces
gabrieldutra Feb 5, 2019
17ed37a
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 5, 2019
74a77c5
Add test for succesful password update
gabrieldutra Feb 5, 2019
ae5652e
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 5, 2019
a89ae71
A few improvements from code review
gabrieldutra Feb 8, 2019
99f317c
Remove Toggle User button when seeing your own profile
gabrieldutra Feb 8, 2019
20d4b7c
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 9, 2019
e2f5303
Create InputWithCopy
gabrieldutra Feb 9, 2019
e2ed0a4
Fix possible errors when network is off and improve Email not sent alert
gabrieldutra Feb 9, 2019
071c968
Add default response object for $http possible errors
gabrieldutra Feb 9, 2019
f046914
Changes in UserEdit
gabrieldutra Feb 10, 2019
389d9ee
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 10, 2019
0289923
Update UserEdit render behavior and styling
gabrieldutra Feb 10, 2019
2e8756f
Create ChangePasswordDialog and update UserEdit
gabrieldutra Feb 13, 2019
f6dfdab
Fix possible console error
gabrieldutra Feb 13, 2019
3851e61
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 13, 2019
d5a3c8f
Remove password match assertion from spec
gabrieldutra Feb 13, 2019
230f7e3
Fix typo
gabrieldutra Feb 14, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ jobs:
command: |
npm run cypress start
docker-compose run cypress node ./cypress/cypress.js db-seed
# Make sure the API key is the same so Percy snapshots are consistent
docker-compose -p cypress run postgres psql -U postgres -h postgres -c "update users set api_key = 'secret' where email ='admin@redash.io';"
- run:
name: Execute Cypress tests
command: npm run cypress run-ci
Expand Down
1 change: 1 addition & 0 deletions client/app/assets/less/ant.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import '~antd/lib/style/core/iconfont';
@import '~antd/lib/style/core/motion';
@import '~antd/lib/alert/style/index';
@import '~antd/lib/input/style/index';
@import '~antd/lib/input-number/style/index';
@import '~antd/lib/date-picker/style/index';
Expand Down
57 changes: 57 additions & 0 deletions client/app/components/InputWithCopy.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import Input from 'antd/lib/input';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';

export default class InputWithCopy extends React.Component {
constructor(props) {
super(props);
this.state = { copied: null };
this.ref = React.createRef();
this.copyFeatureSupported = document.queryCommandSupported('copy');
this.resetCopyState = null;
}

componentWillUnmount() {
if (this.resetCopyState) {
clearTimeout(this.resetCopyState);
}
}

copy = () => {
// select text
this.ref.current.select();

// copy
try {
const success = document.execCommand('copy');
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
} catch (err) {
this.setState({
copied: 'Copy failed',
});
}

// reset tooltip
this.resetCopyState = setTimeout(() => this.setState({ copied: null }), 2000);
};

render() {
const copyButton = (
<Tooltip title={this.state.copied || 'Copy'}>
<Icon
type="copy"
style={{ cursor: 'pointer' }}
onClick={this.copy}
/>
</Tooltip>
);

return (
<Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />
);
}
}
45 changes: 30 additions & 15 deletions client/app/components/dynamic-form/DynamicForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ import Checkbox from 'antd/lib/checkbox';
import Button from 'antd/lib/button';
import Upload from 'antd/lib/upload';
import Icon from 'antd/lib/icon';
import { includes } from 'lodash';
import { react2angular } from 'react2angular';
import { toastr } from '@/services/ng';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';

export class DynamicForm extends React.Component {
const fieldRules = ({ type, required, minLength }) => {
const requiredRule = required;
const minLengthRule = minLength && includes(['text', 'email', 'password'], type);

return [
requiredRule && { required, message: 'This field is required.' },
minLengthRule && { min: minLength, message: 'This field is too short.' },
].filter(rule => rule);
};

export const DynamicForm = Form.create()(class DynamicForm extends React.Component {
static propTypes = {
fields: PropTypes.arrayOf(Field),
actions: PropTypes.arrayOf(Action),
feedbackIcons: PropTypes.bool,
readOnly: PropTypes.bool,
saveText: PropTypes.string,
onSubmit: PropTypes.func,
form: AntdForm.isRequired,
};
Expand All @@ -25,6 +38,8 @@ export class DynamicForm extends React.Component {
fields: [],
actions: [],
feedbackIcons: false,
readOnly: false,
saveText: 'Save',
onSubmit: () => {},
};

Expand Down Expand Up @@ -96,11 +111,10 @@ export class DynamicForm extends React.Component {

renderUpload(field, props) {
const { getFieldDecorator, getFieldValue } = this.props.form;
const { name, initialValue, required } = field;
const fieldLabel = field.title || helper.toHuman(name);
const { name, initialValue } = field;

const fileOptions = {
rules: [{ required, message: `${fieldLabel} is required.` }],
rules: fieldRules(field),
initialValue,
getValueFromEvent: this.base64File.bind(this, name),
};
Expand All @@ -122,7 +136,7 @@ export class DynamicForm extends React.Component {
const fieldLabel = field.title || helper.toHuman(name);

const options = {
rules: [{ required: field.required, message: `${fieldLabel} is required.` }],
rules: fieldRules(field),
valuePropName: type === 'checkbox' ? 'checked' : 'value',
initialValue,
};
Expand All @@ -139,23 +153,25 @@ export class DynamicForm extends React.Component {

renderFields() {
return this.props.fields.map((field) => {
const [firstItem] = this.props.fields;
const [firstField] = this.props.fields;
const FormItem = Form.Item;
const { name, title, type } = field;
const fieldLabel = title || helper.toHuman(name);
const { feedbackIcons, readOnly } = this.props;

const formItemProps = {
key: name,
className: 'm-b-10',
hasFeedback: type !== 'checkbox' && type !== 'file' && this.props.feedbackIcons,
hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
label: type === 'checkbox' ? '' : fieldLabel,
};

const fieldProps = {
autoFocus: (firstItem === field),
autoFocus: (firstField === field),
className: 'w-100',
name,
type,
readOnly,
placeholder: field.placeholder,
'data-test': fieldLabel,
};
Expand All @@ -174,7 +190,7 @@ export class DynamicForm extends React.Component {
htmlType: 'button',
className: action.pullRight ? 'pull-right m-t-10' : 'm-t-10',
type: action.type,
disabled: inProgress || (isFieldsTouched() && action.disableWhenDirty),
disabled: (isFieldsTouched() && action.disableWhenDirty),
loading: inProgress,
onClick: this.handleAction,
};
Expand All @@ -191,22 +207,21 @@ export class DynamicForm extends React.Component {
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
const { readOnly, saveText } = this.props;
const saveButton = !readOnly;

return (
<Form layout="vertical" onSubmit={this.handleSubmit}>
{this.renderFields()}
<Button {...submitProps}>
Save
</Button>
{saveButton && <Button {...submitProps}>{saveText}</Button>}
{this.renderActions()}
</Form>
);
}
}
});

export default function init(ngModule) {
ngModule.component('dynamicForm', react2angular((props) => {
const UpdatedDynamicForm = Form.create()(DynamicForm);
const fields = helper.getFields(props.type.configuration_schema, props.target);

const onSubmit = (values, onSuccess, onError) => {
Expand All @@ -231,7 +246,7 @@ export default function init(ngModule) {
feedbackIcons: true,
onSubmit,
};
return (<UpdatedDynamicForm {...updatedProps} />);
return (<DynamicForm {...updatedProps} />);
}, ['target', 'type', 'actions']));
}

Expand Down
10 changes: 10 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const Field = PropTypes.shape({
type: PropTypes.oneOf(['text', 'email', 'password', 'number', 'checkbox', 'file']).isRequired,
initialValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
required: PropTypes.bool,
minLength: PropTypes.number,
placeholder: PropTypes.string,
});

Expand All @@ -52,6 +53,15 @@ export const AntdForm = PropTypes.shape({
validateFieldsAndScroll: PropTypes.func,
});

export const UserProfile = PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
profileImageUrl: PropTypes.string,
apiKey: PropTypes.string,
isDisabled: PropTypes.bool,
});

function checkMoment(isRequired, props, propName, componentName) {
const value = props[propName];
const isRequiredValid = isRequired && (value !== null);
Expand Down
Loading