diff --git a/superset-frontend/src/components/Form/FormLabel.tsx b/superset-frontend/src/components/Form/FormLabel.tsx index cb4cba0793..3e0faea0e9 100644 --- a/superset-frontend/src/components/Form/FormLabel.tsx +++ b/superset-frontend/src/components/Form/FormLabel.tsx @@ -40,6 +40,7 @@ const RequiredLabel = styled.label` margin-bottom: ${({ theme }) => theme.gridUnit}px; &::after { display: inline-block; + margin-left: ${({ theme }) => theme.gridUnit}px; color: ${({ theme }) => theme.colors.error.base}; font-size: ${({ theme }) => theme.typography.sizes.m}px; content: '*'; diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx new file mode 100644 index 0000000000..6061848d85 --- /dev/null +++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState } from 'react'; +import LabeledErrorBoundInput, { + LabeledErrorBoundInputProps, +} from './LabeledErrorBoundInput'; + +export default { + title: 'LabeledErrorBoundInput', + component: LabeledErrorBoundInput, +}; + +export const InteractiveLabeledErrorBoundInput = ({ + name, + value, + placeholder, + type, + id, +}: LabeledErrorBoundInputProps) => { + const [currentValue, setCurrentValue] = useState(value); + + const validateFunctionality: (value: any) => string = value => { + setCurrentValue(value.target.value); + if (value.target.value.includes('success')) { + return 'success'; + } + return 'error'; + }; + + return ( + + ); +}; + +InteractiveLabeledErrorBoundInput.args = { + name: 'Username', + placeholder: 'Example placeholder text...', + id: 1, +}; + +InteractiveLabeledErrorBoundInput.argTypes = { + type: { + defaultValue: 'textbox', + control: { + type: 'select', + options: ['textbox', 'checkbox', 'radio'], + }, + }, +}; diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx new file mode 100644 index 0000000000..15f956fd57 --- /dev/null +++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput'; + +const defaultProps = { + id: 1, + label: 'Username', + name: 'Username', + validationMethods: () => {}, + errorMessage: '', + helpText: 'This is a line of example help text', + value: '', + placeholder: 'Example placeholder text...', + type: 'textbox', +}; + +describe('LabeledErrorBoundInput', () => { + it('renders a LabeledErrorBoundInput normally, without an error', () => { + render(); + + const label = screen.getByText(/username/i); + const textboxInput = screen.getByRole('textbox'); + const helperText = screen.getByText('This is a line of example help text'); + + expect(label).toBeVisible(); + expect(textboxInput).toBeVisible(); + expect(helperText).toBeVisible(); + }); + + it('renders a LabeledErrorBoundInput with an error', () => { + // Pass an error into props, causing errorText to replace helperText + defaultProps.errorMessage = 'Example error message'; + render(); + + const label = screen.getByText(/username/i); + const textboxInput = screen.getByRole('textbox'); + const errorText = screen.getByText(/example error message/i); + + expect(label).toBeVisible(); + expect(textboxInput).toBeVisible(); + expect(errorText).toBeVisible(); + }); +}); diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx new file mode 100644 index 0000000000..8569b554a0 --- /dev/null +++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { Input } from 'antd'; +import { styled, css, SupersetTheme } from '@superset-ui/core'; +import FormItem from './FormItem'; +import FormLabel from './FormLabel'; + +export interface LabeledErrorBoundInputProps { + label?: string; + validationMethods: + | { onBlur: (value: any) => string } + | { onChange: (value: any) => string }; + errorMessage: string | null; + helpText?: string; + required?: boolean; + id?: string; + [x: string]: any; +} + +const StyledInput = styled(Input)` + margin: 8px 0; +`; + +const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css` + .ant-form-item-children-icon { + display: none; + } + ${hasError && + `.ant-form-item-control-input-content { + position: relative; + + &:after { + content: ' '; + display: inline-block; + background: ${theme.colors.error.base}; + mask: url('/images/icons/error.svg'); + mask-size: cover; + width: ${theme.gridUnit * 4}px; + height: ${theme.gridUnit * 4}px; + position: absolute; + right: 7px; + top: 15px; + } + }`} +`; + +const LabeledErrorBoundInput = ({ + label, + validationMethods, + errorMessage, + helpText, + required = false, + id, + ...props +}: LabeledErrorBoundInputProps) => ( + <> + + {label} + + alertIconStyles(theme, !!errorMessage)} + validateTrigger={Object.keys(validationMethods)} + validateStatus={errorMessage ? 'error' : 'success'} + help={errorMessage || helpText} + hasFeedback={!!errorMessage} + > + + + +); + +export default LabeledErrorBoundInput; diff --git a/superset-frontend/src/components/Form/index.tsx b/superset-frontend/src/components/Form/index.tsx index f0734a254c..7d7a60745d 100644 --- a/superset-frontend/src/components/Form/index.tsx +++ b/superset-frontend/src/components/Form/index.tsx @@ -19,5 +19,6 @@ import Form from './Form'; import FormItem from './FormItem'; import FormLabel from './FormLabel'; +import LabeledErrorBoundInput from './LabeledErrorBoundInput'; -export { Form, FormItem, FormLabel }; +export { Form, FormItem, FormLabel, LabeledErrorBoundInput };