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.
This commit is contained in:
		@@ -12,7 +12,7 @@ import {
 | 
				
			|||||||
} from 'shared/generated/graphql';
 | 
					} from 'shared/generated/graphql';
 | 
				
			||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
import { useForm, Controller } from 'react-hook-form';
 | 
					import { useForm, Controller, UseFormSetError } from 'react-hook-form';
 | 
				
			||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
 | 
					import { usePopup, Popup } from 'shared/components/PopupMenu';
 | 
				
			||||||
import produce from 'immer';
 | 
					import produce from 'immer';
 | 
				
			||||||
import updateApolloCache from 'shared/utils/cache';
 | 
					import updateApolloCache from 'shared/utils/cache';
 | 
				
			||||||
@@ -20,6 +20,7 @@ import { useCurrentUser } from 'App/context';
 | 
				
			|||||||
import { Redirect } from 'react-router';
 | 
					import { Redirect } from 'react-router';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
import ControlledInput from 'shared/components/ControlledInput';
 | 
					import ControlledInput from 'shared/components/ControlledInput';
 | 
				
			||||||
 | 
					import FormInput from 'shared/components/FormInput';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DeleteUserWrapper = styled.div`
 | 
					const DeleteUserWrapper = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -77,7 +78,7 @@ const CreateUserButton = styled(Button)`
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AddUserInput = styled(ControlledInput)`
 | 
					const AddUserInput = styled(FormInput)`
 | 
				
			||||||
  margin-bottom: 8px;
 | 
					  margin-bottom: 8px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,7 +88,7 @@ const InputError = styled.span`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AddUserPopupProps = {
 | 
					type AddUserPopupProps = {
 | 
				
			||||||
  onAddUser: (user: CreateUserData) => void;
 | 
					  onAddUser: (user: CreateUserData, setError: UseFormSetError<CreateUserData>) => void;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
 | 
					const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
 | 
				
			||||||
@@ -95,16 +96,16 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
 | 
				
			|||||||
    register,
 | 
					    register,
 | 
				
			||||||
    handleSubmit,
 | 
					    handleSubmit,
 | 
				
			||||||
    formState: { errors },
 | 
					    formState: { errors },
 | 
				
			||||||
 | 
					    setError,
 | 
				
			||||||
    control,
 | 
					    control,
 | 
				
			||||||
  } = useForm<CreateUserData>();
 | 
					  } = useForm<CreateUserData>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createUser = (data: CreateUserData) => {
 | 
					  const createUser = (data: CreateUserData) => {
 | 
				
			||||||
    onAddUser(data);
 | 
					    onAddUser(data, setError);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <CreateUserForm onSubmit={handleSubmit(createUser)}>
 | 
					    <CreateUserForm onSubmit={handleSubmit(createUser)}>
 | 
				
			||||||
      <AddUserInput
 | 
					      <AddUserInput
 | 
				
			||||||
        floatingLabel
 | 
					 | 
				
			||||||
        width="100%"
 | 
					        width="100%"
 | 
				
			||||||
        label="Full Name"
 | 
					        label="Full Name"
 | 
				
			||||||
        variant="alternate"
 | 
					        variant="alternate"
 | 
				
			||||||
@@ -118,6 +119,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
 | 
				
			|||||||
        variant="alternate"
 | 
					        variant="alternate"
 | 
				
			||||||
        {...register('email', { required: 'Email is required' })}
 | 
					        {...register('email', { required: 'Email is required' })}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      {errors.email && <InputError>{errors.email.message}</InputError>}
 | 
				
			||||||
      <Controller
 | 
					      <Controller
 | 
				
			||||||
        control={control}
 | 
					        control={control}
 | 
				
			||||||
        name="roleCode"
 | 
					        name="roleCode"
 | 
				
			||||||
@@ -241,10 +243,15 @@ TODO: add permision check
 | 
				
			|||||||
              $target,
 | 
					              $target,
 | 
				
			||||||
              <Popup tab={0} title="Add member" onClose={() => hidePopup()}>
 | 
					              <Popup tab={0} title="Add member" onClose={() => hidePopup()}>
 | 
				
			||||||
                <AddUserPopup
 | 
					                <AddUserPopup
 | 
				
			||||||
                  onAddUser={(u) => {
 | 
					                  onAddUser={(u, setError) => {
 | 
				
			||||||
                    const { roleCode, ...userData } = u;
 | 
					                    const { roleCode, ...userData } = u;
 | 
				
			||||||
                    createUser({ variables: { ...userData, roleCode: roleCode.value } });
 | 
					                    createUser({
 | 
				
			||||||
                    hidePopup();
 | 
					                      variables: { ...userData, roleCode: roleCode.value },
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                      .then(() => hidePopup())
 | 
				
			||||||
 | 
					                      .catch((e) => {
 | 
				
			||||||
 | 
					                        setError('email', { type: 'validate', message: e.message });
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              </Popup>,
 | 
					              </Popup>,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										202
									
								
								frontend/src/shared/components/FormInput/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								frontend/src/shared/components/FormInput/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,202 @@
 | 
				
			|||||||
 | 
					import React, { useState, useEffect, useRef } from 'react';
 | 
				
			||||||
 | 
					import styled, { css } from 'styled-components/macro';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const InputWrapper = styled.div<{ width: string }>`
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  width: ${(props) => props.width};
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  margin-bottom: 2.2rem;
 | 
				
			||||||
 | 
					  margin-top: 24px;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const InputLabel = styled.span<{ width: string }>`
 | 
				
			||||||
 | 
					  width: ${(props) => props.width};
 | 
				
			||||||
 | 
					  padding: 0.7rem !important;
 | 
				
			||||||
 | 
					  color: #c2c6dc;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  font-size: 0.85rem;
 | 
				
			||||||
 | 
					  cursor: text;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					      user-select: none;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const InputInput = styled.input<{
 | 
				
			||||||
 | 
					  hasValue: boolean;
 | 
				
			||||||
 | 
					  hasIcon: boolean;
 | 
				
			||||||
 | 
					  width: string;
 | 
				
			||||||
 | 
					  focusBg: string;
 | 
				
			||||||
 | 
					  borderColor: string;
 | 
				
			||||||
 | 
					}>`
 | 
				
			||||||
 | 
					  width: ${(props) => props.width};
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  border: 1px solid rgba(0, 0, 0, 0.2);
 | 
				
			||||||
 | 
					  border-color: ${(props) => props.borderColor};
 | 
				
			||||||
 | 
					  background: #262c49;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					  ${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:-webkit-autofill, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active {
 | 
				
			||||||
 | 
					    -webkit-box-shadow: 0 0 0 30px #262c49 inset !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &:-webkit-autofill {
 | 
				
			||||||
 | 
					    -webkit-text-fill-color: #c2c6dc !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  line-height: 16px;
 | 
				
			||||||
 | 
					  color: #c2c6dc;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					    border: 1px solid ${(props) => props.theme.colors.primary};
 | 
				
			||||||
 | 
					    background: ${(props) => props.focusBg};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &:focus ~ ${InputLabel} {
 | 
				
			||||||
 | 
					    color: ${(props) => props.theme.colors.primary};
 | 
				
			||||||
 | 
					    transform: translate(-3px, -90%);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ${(props) =>
 | 
				
			||||||
 | 
					    props.hasValue &&
 | 
				
			||||||
 | 
					    css`
 | 
				
			||||||
 | 
					      & ~ ${InputLabel} {
 | 
				
			||||||
 | 
					        color: ${props.theme.colors.primary};
 | 
				
			||||||
 | 
					        transform: translate(-3px, -90%);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Icon = styled.div`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  left: 16px;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FormInputProps = {
 | 
				
			||||||
 | 
					  variant?: 'normal' | 'alternate';
 | 
				
			||||||
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  label?: string;
 | 
				
			||||||
 | 
					  width?: string;
 | 
				
			||||||
 | 
					  floatingLabel?: boolean;
 | 
				
			||||||
 | 
					  placeholder?: string;
 | 
				
			||||||
 | 
					  icon?: JSX.Element;
 | 
				
			||||||
 | 
					  type?: string;
 | 
				
			||||||
 | 
					  autocomplete?: boolean;
 | 
				
			||||||
 | 
					  autoFocus?: boolean;
 | 
				
			||||||
 | 
					  autoSelect?: boolean;
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
 | 
					  onChange: any;
 | 
				
			||||||
 | 
					  onBlur: any;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  defaultValue?: string;
 | 
				
			||||||
 | 
					  value?: string;
 | 
				
			||||||
 | 
					  onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useCombinedRefs(...refs: any) {
 | 
				
			||||||
 | 
					  const targetRef = React.useRef();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    refs.forEach((ref: any) => {
 | 
				
			||||||
 | 
					      if (!ref) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (typeof ref === 'function') {
 | 
				
			||||||
 | 
					        ref(targetRef.current);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        ref.current = targetRef.current;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, [refs]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return targetRef;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormInput = React.forwardRef(
 | 
				
			||||||
 | 
					  (
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      disabled = false,
 | 
				
			||||||
 | 
					      width = 'auto',
 | 
				
			||||||
 | 
					      variant = 'normal',
 | 
				
			||||||
 | 
					      type = 'text',
 | 
				
			||||||
 | 
					      autoFocus = false,
 | 
				
			||||||
 | 
					      autoSelect = false,
 | 
				
			||||||
 | 
					      autocomplete,
 | 
				
			||||||
 | 
					      label,
 | 
				
			||||||
 | 
					      placeholder,
 | 
				
			||||||
 | 
					      onBlur,
 | 
				
			||||||
 | 
					      onChange,
 | 
				
			||||||
 | 
					      icon,
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick,
 | 
				
			||||||
 | 
					      floatingLabel,
 | 
				
			||||||
 | 
					      defaultValue,
 | 
				
			||||||
 | 
					      value,
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					    }: FormInputProps,
 | 
				
			||||||
 | 
					    $ref: any,
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const [hasValue, setHasValue] = useState(defaultValue !== '');
 | 
				
			||||||
 | 
					    const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
 | 
				
			||||||
 | 
					    const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
 | 
				
			||||||
 | 
					    // The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
 | 
				
			||||||
 | 
					    // TODO(jordanknott): This is super ugly, find a better approach?
 | 
				
			||||||
 | 
					    const $innerRef = React.useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					    const combinedRef: any = useCombinedRefs($ref, $innerRef);
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      if (combinedRef && combinedRef.current) {
 | 
				
			||||||
 | 
					        if (autoFocus) {
 | 
				
			||||||
 | 
					          combinedRef.current.focus();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (autoSelect) {
 | 
				
			||||||
 | 
					          combinedRef.current.select();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <InputWrapper className={className} width={width}>
 | 
				
			||||||
 | 
					        <InputInput
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
 | 
				
			||||||
 | 
					            onChange(e);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          disabled={disabled}
 | 
				
			||||||
 | 
					          hasValue={hasValue}
 | 
				
			||||||
 | 
					          ref={combinedRef}
 | 
				
			||||||
 | 
					          id={id}
 | 
				
			||||||
 | 
					          type={type}
 | 
				
			||||||
 | 
					          name={name}
 | 
				
			||||||
 | 
					          onClick={onClick}
 | 
				
			||||||
 | 
					          autoComplete={autocomplete ? 'on' : 'off'}
 | 
				
			||||||
 | 
					          defaultValue={defaultValue}
 | 
				
			||||||
 | 
					          onBlur={onBlur}
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          hasIcon={typeof icon !== 'undefined'}
 | 
				
			||||||
 | 
					          width={width}
 | 
				
			||||||
 | 
					          placeholder={placeholder}
 | 
				
			||||||
 | 
					          focusBg={focusBg}
 | 
				
			||||||
 | 
					          borderColor={borderColor}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {label && <InputLabel width={width}>{label}</InputLabel>}
 | 
				
			||||||
 | 
					        <Icon>{icon && icon}</Icon>
 | 
				
			||||||
 | 
					      </InputWrapper>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FormInput;
 | 
				
			||||||
@@ -59,6 +59,7 @@ type Querier interface {
 | 
				
			|||||||
	DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
 | 
						DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
 | 
				
			||||||
	DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
 | 
						DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
 | 
				
			||||||
	DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
 | 
						DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
 | 
				
			||||||
 | 
						DoesUserExist(ctx context.Context, arg DoesUserExistParams) (bool, error)
 | 
				
			||||||
	GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error)
 | 
						GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error)
 | 
				
			||||||
	GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
 | 
						GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
 | 
				
			||||||
	GetAllOrganizations(ctx context.Context) ([]Organization, error)
 | 
						GetAllOrganizations(ctx context.Context) ([]Organization, error)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,6 +63,9 @@ SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system');
 | 
				
			|||||||
-- name: HasActiveUser :one
 | 
					-- name: HasActiveUser :one
 | 
				
			||||||
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: DoesUserExist :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE email = $1 OR username = $2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: CreateConfirmToken :one
 | 
					-- name: CreateConfirmToken :one
 | 
				
			||||||
INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;
 | 
					INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,6 +157,22 @@ func (q *Queries) DeleteUserAccountInvitedForEmail(ctx context.Context, email st
 | 
				
			|||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const doesUserExist = `-- name: DoesUserExist :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE email = $1 OR username = $2)
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DoesUserExistParams struct {
 | 
				
			||||||
 | 
						Email    string `json:"email"`
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) DoesUserExist(ctx context.Context, arg DoesUserExistParams) (bool, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, doesUserExist, arg.Email, arg.Username)
 | 
				
			||||||
 | 
						var exists bool
 | 
				
			||||||
 | 
						err := row.Scan(&exists)
 | 
				
			||||||
 | 
						return exists, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
 | 
					const getAllUserAccounts = `-- name: GetAllUserAccounts :many
 | 
				
			||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system'
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system'
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -872,23 +872,27 @@ func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*D
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
 | 
					func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
 | 
				
			||||||
	_, ok := GetUser(ctx)
 | 
						userID, ok := GetUserID(ctx)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		return &db.Team{}, nil
 | 
							return &db.Team{}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// if role == auth.RoleAdmin { // TODO: add permision check
 | 
						role, err := r.Repository.GetRoleForUserID(ctx, userID)
 | 
				
			||||||
	if true {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("while creating team")
 | 
				
			||||||
 | 
							return &db.Team{}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
 | 
				
			||||||
 | 
							return &db.Team{}, &gqlerror.Error{
 | 
				
			||||||
 | 
								Message: "Must be an organization admin",
 | 
				
			||||||
 | 
								Extensions: map[string]interface{}{
 | 
				
			||||||
 | 
									"code": "0-400",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	createdAt := time.Now().UTC()
 | 
						createdAt := time.Now().UTC()
 | 
				
			||||||
	team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
 | 
						team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
 | 
				
			||||||
	return &team, err
 | 
						return &team, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
	return &db.Team{}, &gqlerror.Error{
 | 
					 | 
				
			||||||
		Message: "You must be an organization admin to create new teams",
 | 
					 | 
				
			||||||
		Extensions: map[string]interface{}{
 | 
					 | 
				
			||||||
			"code": "1-400",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTeamMember) (*CreateTeamMemberPayload, error) {
 | 
					func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTeamMember) (*CreateTeamMemberPayload, error) {
 | 
				
			||||||
	addedDate := time.Now().UTC()
 | 
						addedDate := time.Now().UTC()
 | 
				
			||||||
@@ -954,12 +958,16 @@ func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTea
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
 | 
					func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
 | 
				
			||||||
	_, ok := GetUser(ctx)
 | 
						userID, ok := GetUserID(ctx)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		return &db.UserAccount{}, nil
 | 
							return &db.UserAccount{}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// if role != auth.RoleAdmin { TODO: add permsion check
 | 
						role, err := r.Repository.GetRoleForUserID(ctx, userID)
 | 
				
			||||||
	if true {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("while creating user account")
 | 
				
			||||||
 | 
							return &db.UserAccount{}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
 | 
				
			||||||
		return &db.UserAccount{}, &gqlerror.Error{
 | 
							return &db.UserAccount{}, &gqlerror.Error{
 | 
				
			||||||
			Message: "Must be an organization admin",
 | 
								Message: "Must be an organization admin",
 | 
				
			||||||
			Extensions: map[string]interface{}{
 | 
								Extensions: map[string]interface{}{
 | 
				
			||||||
@@ -972,6 +980,19 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return &db.UserAccount{}, err
 | 
							return &db.UserAccount{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userExists, err := r.Repository.DoesUserExist(ctx, db.DoesUserExistParams{Username: input.Username, Email: input.Email})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return &db.UserAccount{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if userExists {
 | 
				
			||||||
 | 
							return &db.UserAccount{}, &gqlerror.Error{
 | 
				
			||||||
 | 
								Message: "User with that username or email already exists",
 | 
				
			||||||
 | 
								Extensions: map[string]interface{}{
 | 
				
			||||||
 | 
									"code": "0-300",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	userAccount, err := r.Repository.CreateUserAccount(ctx, db.CreateUserAccountParams{
 | 
						userAccount, err := r.Repository.CreateUserAccount(ctx, db.CreateUserAccountParams{
 | 
				
			||||||
		FullName:     input.FullName,
 | 
							FullName:     input.FullName,
 | 
				
			||||||
		RoleCode:     input.RoleCode,
 | 
							RoleCode:     input.RoleCode,
 | 
				
			||||||
@@ -986,16 +1007,20 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
 | 
					func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
 | 
				
			||||||
	_, ok := GetUser(ctx)
 | 
						userID, ok := GetUserID(ctx)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		return &DeleteUserAccountPayload{Ok: false}, nil
 | 
							return &DeleteUserAccountPayload{Ok: false}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// if role != auth.RoleAdmin { TODO: add permision check
 | 
						role, err := r.Repository.GetRoleForUserID(ctx, userID)
 | 
				
			||||||
	if true {
 | 
						if err != nil {
 | 
				
			||||||
		return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
 | 
							log.WithError(err).Error("while deleting user account")
 | 
				
			||||||
			Message: "User not found",
 | 
							return &DeleteUserAccountPayload{}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
 | 
				
			||||||
 | 
							return &DeleteUserAccountPayload{}, &gqlerror.Error{
 | 
				
			||||||
 | 
								Message: "Must be an organization admin",
 | 
				
			||||||
			Extensions: map[string]interface{}{
 | 
								Extensions: map[string]interface{}{
 | 
				
			||||||
				"code": "0-401",
 | 
									"code": "0-400",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1004,8 +1029,6 @@ func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUs
 | 
				
			|||||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
							return &DeleteUserAccountPayload{Ok: false}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO(jordanknott) migrate admin ownership
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
 | 
						err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
							return &DeleteUserAccountPayload{Ok: false}, err
 | 
				
			||||||
@@ -1062,16 +1085,20 @@ func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateU
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
 | 
					func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
 | 
				
			||||||
	_, ok := GetUser(ctx)
 | 
						userID, ok := GetUserID(ctx)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		return &UpdateUserRolePayload{}, nil
 | 
							return &UpdateUserRolePayload{}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// if role != auth.RoleAdmin { TODO: add permision check
 | 
						role, err := r.Repository.GetRoleForUserID(ctx, userID)
 | 
				
			||||||
	if true {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("while updating user role")
 | 
				
			||||||
 | 
							return &UpdateUserRolePayload{}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
 | 
				
			||||||
		return &UpdateUserRolePayload{}, &gqlerror.Error{
 | 
							return &UpdateUserRolePayload{}, &gqlerror.Error{
 | 
				
			||||||
			Message: "User not found",
 | 
								Message: "Must be an organization admin",
 | 
				
			||||||
			Extensions: map[string]interface{}{
 | 
								Extensions: map[string]interface{}{
 | 
				
			||||||
				"code": "0-401",
 | 
									"code": "0-400",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1331,14 +1358,7 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var teams []db.Team
 | 
						var teams []db.Team
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	/* TODO: add permsion check
 | 
					 | 
				
			||||||
	if orgRole == "admin" {
 | 
					 | 
				
			||||||
		teams, err = r.Repository.GetAllTeams(ctx)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
	teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
						teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	teams, err = r.Repository.GetAllTeams(ctx)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	projects := make(map[string]db.Project)
 | 
						projects := make(map[string]db.Project)
 | 
				
			||||||
	for _, team := range teams {
 | 
						for _, team := range teams {
 | 
				
			||||||
@@ -1390,13 +1410,6 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
 | 
				
			|||||||
		return []db.Team{}, errors.New("internal error")
 | 
							return []db.Team{}, errors.New("internal error")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
		TODO: add permision check
 | 
					 | 
				
			||||||
		if orgRole == "admin" {
 | 
					 | 
				
			||||||
			return r.Repository.GetAllTeams(ctx)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	teams := make(map[string]db.Team)
 | 
						teams := make(map[string]db.Team)
 | 
				
			||||||
	adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
						adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user