Skip to content

Commit aa84cba

Browse files
committed
fix: add user popup is submittable again
react-form-hooks no longer played nice with custom input. created a third input type `FormInput` that is made to play well with the react-form-hooks. also fixes auto complete overriding bg + text color on inputs.
1 parent 8b1de30 commit aa84cba

File tree

6 files changed

+292
-50
lines changed

6 files changed

+292
-50
lines changed

frontend/src/Admin/index.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import {
1212
} from 'shared/generated/graphql';
1313
import styled from 'styled-components';
1414
import Button from 'shared/components/Button';
15-
import { useForm, Controller } from 'react-hook-form';
15+
import { useForm, Controller, UseFormSetError } from 'react-hook-form';
1616
import { usePopup, Popup } from 'shared/components/PopupMenu';
1717
import produce from 'immer';
1818
import updateApolloCache from 'shared/utils/cache';
1919
import { useCurrentUser } from 'App/context';
2020
import { Redirect } from 'react-router';
2121
import NOOP from 'shared/utils/noop';
2222
import ControlledInput from 'shared/components/ControlledInput';
23+
import FormInput from 'shared/components/FormInput';
2324

2425
const DeleteUserWrapper = styled.div`
2526
display: flex;
@@ -77,7 +78,7 @@ const CreateUserButton = styled(Button)`
7778
width: 100%;
7879
`;
7980

80-
const AddUserInput = styled(ControlledInput)`
81+
const AddUserInput = styled(FormInput)`
8182
margin-bottom: 8px;
8283
`;
8384

@@ -87,24 +88,24 @@ const InputError = styled.span`
8788
`;
8889

8990
type AddUserPopupProps = {
90-
onAddUser: (user: CreateUserData) => void;
91+
onAddUser: (user: CreateUserData, setError: UseFormSetError<CreateUserData>) => void;
9192
};
9293

9394
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
9495
const {
9596
register,
9697
handleSubmit,
9798
formState: { errors },
99+
setError,
98100
control,
99101
} = useForm<CreateUserData>();
100102

101103
const createUser = (data: CreateUserData) => {
102-
onAddUser(data);
104+
onAddUser(data, setError);
103105
};
104106
return (
105107
<CreateUserForm onSubmit={handleSubmit(createUser)}>
106108
<AddUserInput
107-
floatingLabel
108109
width="100%"
109110
label="Full Name"
110111
variant="alternate"
@@ -118,6 +119,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
118119
variant="alternate"
119120
{...register('email', { required: 'Email is required' })}
120121
/>
122+
{errors.email && <InputError>{errors.email.message}</InputError>}
121123
<Controller
122124
control={control}
123125
name="roleCode"
@@ -241,10 +243,15 @@ TODO: add permision check
241243
$target,
242244
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
243245
<AddUserPopup
244-
onAddUser={(u) => {
246+
onAddUser={(u, setError) => {
245247
const { roleCode, ...userData } = u;
246-
createUser({ variables: { ...userData, roleCode: roleCode.value } });
247-
hidePopup();
248+
createUser({
249+
variables: { ...userData, roleCode: roleCode.value },
250+
})
251+
.then(() => hidePopup())
252+
.catch((e) => {
253+
setError('email', { type: 'validate', message: e.message });
254+
});
248255
}}
249256
/>
250257
</Popup>,
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import styled, { css } from 'styled-components/macro';
3+
import theme from '../../../App/ThemeStyles';
4+
5+
const InputWrapper = styled.div<{ width: string }>`
6+
position: relative;
7+
width: ${(props) => props.width};
8+
display: flex;
9+
align-items: flex-start;
10+
flex-direction: column;
11+
position: relative;
12+
justify-content: center;
13+
14+
margin-bottom: 2.2rem;
15+
margin-top: 24px;
16+
`;
17+
18+
const InputLabel = styled.span<{ width: string }>`
19+
width: ${(props) => props.width};
20+
padding: 0.7rem !important;
21+
color: #c2c6dc;
22+
left: 0;
23+
top: 0;
24+
transition: all 0.2s ease;
25+
position: absolute;
26+
border-radius: 5px;
27+
overflow: hidden;
28+
font-size: 0.85rem;
29+
cursor: text;
30+
font-size: 12px;
31+
user-select: none;
32+
pointer-events: none;
33+
}
34+
`;
35+
36+
const InputInput = styled.input<{
37+
hasValue: boolean;
38+
hasIcon: boolean;
39+
width: string;
40+
focusBg: string;
41+
borderColor: string;
42+
}>`
43+
width: ${(props) => props.width};
44+
font-size: 14px;
45+
border: 1px solid rgba(0, 0, 0, 0.2);
46+
border-color: ${(props) => props.borderColor};
47+
background: #262c49;
48+
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
49+
${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
50+
51+
&:-webkit-autofill, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active {
52+
-webkit-box-shadow: 0 0 0 30px #262c49 inset !important;
53+
}
54+
&:-webkit-autofill {
55+
-webkit-text-fill-color: #c2c6dc !important;
56+
}
57+
line-height: 16px;
58+
color: #c2c6dc;
59+
position: relative;
60+
border-radius: 5px;
61+
transition: all 0.3s ease;
62+
&:focus {
63+
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
64+
border: 1px solid ${(props) => props.theme.colors.primary};
65+
background: ${(props) => props.focusBg};
66+
}
67+
&:focus ~ ${InputLabel} {
68+
color: ${(props) => props.theme.colors.primary};
69+
transform: translate(-3px, -90%);
70+
}
71+
${(props) =>
72+
props.hasValue &&
73+
css`
74+
& ~ ${InputLabel} {
75+
color: ${props.theme.colors.primary};
76+
transform: translate(-3px, -90%);
77+
}
78+
`}
79+
`;
80+
81+
const Icon = styled.div`
82+
display: flex;
83+
left: 16px;
84+
position: absolute;
85+
`;
86+
87+
type FormInputProps = {
88+
variant?: 'normal' | 'alternate';
89+
disabled?: boolean;
90+
label?: string;
91+
width?: string;
92+
floatingLabel?: boolean;
93+
placeholder?: string;
94+
icon?: JSX.Element;
95+
type?: string;
96+
autocomplete?: boolean;
97+
autoFocus?: boolean;
98+
autoSelect?: boolean;
99+
id?: string;
100+
name?: string;
101+
onChange: any;
102+
onBlur: any;
103+
className?: string;
104+
defaultValue?: string;
105+
value?: string;
106+
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
107+
};
108+
109+
function useCombinedRefs(...refs: any) {
110+
const targetRef = React.useRef();
111+
112+
React.useEffect(() => {
113+
refs.forEach((ref: any) => {
114+
if (!ref) return;
115+
116+
if (typeof ref === 'function') {
117+
ref(targetRef.current);
118+
} else {
119+
ref.current = targetRef.current;
120+
}
121+
});
122+
}, [refs]);
123+
124+
return targetRef;
125+
}
126+
127+
const FormInput = React.forwardRef(
128+
(
129+
{
130+
disabled = false,
131+
width = 'auto',
132+
variant = 'normal',
133+
type = 'text',
134+
autoFocus = false,
135+
autoSelect = false,
136+
autocomplete,
137+
label,
138+
placeholder,
139+
onBlur,
140+
onChange,
141+
icon,
142+
name,
143+
className,
144+
onClick,
145+
floatingLabel,
146+
defaultValue,
147+
value,
148+
id,
149+
}: FormInputProps,
150+
$ref: any,
151+
) => {
152+
const [hasValue, setHasValue] = useState(defaultValue !== '');
153+
const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
154+
const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
155+
156+
// Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
157+
// The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
158+
// TODO(jordanknott): This is super ugly, find a better approach?
159+
const $innerRef = React.useRef<HTMLInputElement>(null);
160+
const combinedRef: any = useCombinedRefs($ref, $innerRef);
161+
useEffect(() => {
162+
if (combinedRef && combinedRef.current) {
163+
if (autoFocus) {
164+
combinedRef.current.focus();
165+
}
166+
if (autoSelect) {
167+
combinedRef.current.select();
168+
}
169+
}
170+
}, []);
171+
return (
172+
<InputWrapper className={className} width={width}>
173+
<InputInput
174+
onChange={(e) => {
175+
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
176+
onChange(e);
177+
}}
178+
disabled={disabled}
179+
hasValue={hasValue}
180+
ref={combinedRef}
181+
id={id}
182+
type={type}
183+
name={name}
184+
onClick={onClick}
185+
autoComplete={autocomplete ? 'on' : 'off'}
186+
defaultValue={defaultValue}
187+
onBlur={onBlur}
188+
value={value}
189+
hasIcon={typeof icon !== 'undefined'}
190+
width={width}
191+
placeholder={placeholder}
192+
focusBg={focusBg}
193+
borderColor={borderColor}
194+
/>
195+
{label && <InputLabel width={width}>{label}</InputLabel>}
196+
<Icon>{icon && icon}</Icon>
197+
</InputWrapper>
198+
);
199+
},
200+
);
201+
202+
export default FormInput;

internal/db/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/db/query/user_accounts.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system');
6363
-- name: HasActiveUser :one
6464
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
6565

66+
-- name: DoesUserExist :one
67+
SELECT EXISTS(SELECT 1 FROM user_account WHERE email = $1 OR username = $2);
68+
6669
-- name: CreateConfirmToken :one
6770
INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;
6871

internal/db/user_accounts.sql.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)