feat: wip import csv [part 1] (#1033)

* feat: wip import csv

* feat: start implementing twenty UI

* feat: new radio button component

* feat: use new radio button component and fix scroll issue

* fix: max height modal

* feat: wip try to customize react-data-grid to match design

* feat: wip match columns

* feat: wip match column selection

* feat: match column

* feat: clean heading component & try to fix scroll in last step

* feat: validation step

* fix: small cleaning and remove unused component

* feat: clean folder architecture

* feat: remove translations

* feat: remove chackra theme

* feat: remove unused libraries

* feat: use option button to open spreadsheet & fix stories

* Fix lint and fix imports

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2023-08-16 00:12:47 +02:00
committed by GitHub
parent 1ca41021cf
commit 56cada6335
95 changed files with 7042 additions and 99 deletions

View File

@ -6,6 +6,7 @@ import { IconCheck, IconMinus } from '@/ui/icon';
export enum CheckboxVariant {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
}
export enum CheckboxShape {
@ -21,7 +22,8 @@ export enum CheckboxSize {
type OwnProps = {
checked: boolean;
indeterminate?: boolean;
onChange?: (value: boolean) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onCheckedChange?: (value: boolean) => void;
variant?: CheckboxVariant;
size?: CheckboxSize;
shape?: CheckboxShape;
@ -33,13 +35,15 @@ const StyledInputContainer = styled.div`
position: relative;
`;
const StyledInput = styled.input<{
type InputProps = {
checkboxSize: CheckboxSize;
variant: CheckboxVariant;
indeterminate?: boolean;
shape?: CheckboxShape;
isChecked: boolean;
}>`
isChecked?: boolean;
};
const StyledInput = styled.input<InputProps>`
cursor: pointer;
margin: 0;
opacity: 0;
@ -61,18 +65,25 @@ const StyledInput = styled.input<{
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
background: ${({ theme, indeterminate, isChecked }) =>
indeterminate || isChecked ? theme.color.blue : 'transparent'};
border-color: ${({ theme, indeterminate, isChecked, variant }) =>
indeterminate || isChecked
? theme.color.blue
: variant === CheckboxVariant.Primary
? theme.border.color.inverted
: theme.border.color.secondaryInverted};
border-color: ${({ theme, indeterminate, isChecked, variant }) => {
switch (true) {
case indeterminate || isChecked:
return theme.color.blue;
case variant === CheckboxVariant.Primary:
return theme.border.color.inverted;
case variant === CheckboxVariant.Tertiary:
return theme.border.color.medium;
default:
return theme.border.color.secondaryInverted;
}
}};
border-radius: ${({ theme, shape }) =>
shape === CheckboxShape.Rounded
? theme.border.radius.rounded
: theme.border.radius.sm};
border-style: solid;
border-width: 1px;
border-width: ${({ variant }) =>
variant === CheckboxVariant.Tertiary ? '2px' : '1px'};
content: '';
cursor: pointer;
display: inline-block;
@ -81,8 +92,11 @@ const StyledInput = styled.input<{
}
& + label > svg {
--padding: ${({ checkboxSize }) =>
checkboxSize === CheckboxSize.Large ? '2px' : '1px'};
--padding: ${({ checkboxSize, variant }) =>
checkboxSize === CheckboxSize.Large ||
variant === CheckboxVariant.Tertiary
? '2px'
: '1px'};
--size: ${({ checkboxSize }) =>
checkboxSize === CheckboxSize.Large ? '16px' : '12px'};
height: var(--size);
@ -97,6 +111,7 @@ const StyledInput = styled.input<{
export function Checkbox({
checked,
onChange,
onCheckedChange,
indeterminate,
variant = CheckboxVariant.Primary,
size = CheckboxSize.Small,
@ -108,9 +123,11 @@ export function Checkbox({
React.useEffect(() => {
setIsInternalChecked(checked);
}, [checked]);
function handleChange(value: boolean) {
onChange?.(value);
setIsInternalChecked(!isInternalChecked);
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
onChange?.(event);
onCheckedChange?.(event.target.checked);
setIsInternalChecked(event.target.checked);
}
return (
@ -126,7 +143,7 @@ export function Checkbox({
checkboxSize={size}
shape={shape}
isChecked={isInternalChecked}
onChange={(event) => handleChange(event.target.checked)}
onChange={handleChange}
/>
<label htmlFor="checkbox">
{indeterminate ? (

View File

@ -0,0 +1,157 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { rgba } from '@/ui/theme/constants/colors';
import { RadioGroup } from './RadioGroup';
export enum RadioSize {
Large = 'large',
Small = 'small',
}
export enum LabelPosition {
Left = 'left',
Right = 'right',
}
const Container = styled.div<{ labelPosition?: LabelPosition }>`
${({ labelPosition }) =>
labelPosition === LabelPosition.Left
? `
flex-direction: row-reverse;
`
: `
flex-direction: row;
`};
align-items: center;
display: flex;
`;
type RadioInputProps = {
radioSize?: RadioSize;
};
const RadioInput = styled(motion.input)<RadioInputProps>`
-webkit-appearance: none;
appearance: none;
background-color: transparent;
border: 1px solid ${({ theme }) => theme.font.color.secondary};
border-radius: 50%;
:hover {
background-color: ${({ theme, checked }) => {
if (!checked) {
return theme.background.tertiary;
}
}};
outline: 4px solid
${({ theme, checked }) => {
if (!checked) {
return theme.background.tertiary;
}
return rgba(theme.color.blue, 0.12);
}};
}
&:checked {
background-color: ${({ theme }) => theme.color.blue};
border: none;
&::after {
background-color: ${({ theme }) => theme.color.gray0};
border-radius: 50%;
content: '';
height: ${({ radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'};
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: ${({ radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'};
}
}
&:disabled {
cursor: not-allowed;
opacity: 0.12;
}
height: ${({ radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'};
position: relative;
width: ${({ radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'};
`;
type LabelProps = {
disabled?: boolean;
labelPosition?: LabelPosition;
};
const Label = styled.label<LabelProps>`
color: ${({ theme }) => theme.font.color.primary};
cursor: pointer;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme, labelPosition }) =>
labelPosition === LabelPosition.Right ? theme.spacing(2) : '0px'};
margin-right: ${({ theme, labelPosition }) =>
labelPosition === LabelPosition.Left ? theme.spacing(2) : '0px'};
opacity: ${({ disabled }) => (disabled ? 0.32 : 1)};
`;
export type RadioProps = {
style?: React.CSSProperties;
className?: string;
checked?: boolean;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onCheckedChange?: (checked: boolean) => void;
size?: RadioSize;
disabled?: boolean;
labelPosition?: LabelPosition;
};
export function Radio({
checked,
value,
onChange,
onCheckedChange,
size = RadioSize.Small,
labelPosition = LabelPosition.Right,
disabled = false,
...restProps
}: RadioProps) {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
onChange?.(event);
onCheckedChange?.(event.target.checked);
}
return (
<Container {...restProps} labelPosition={labelPosition}>
<RadioInput
type="radio"
id="input-radio"
name="input-radio"
data-testid="input-radio"
checked={checked}
value={value}
radioSize={size}
disabled={disabled}
onChange={handleChange}
initial={{ scale: 0.95 }}
animate={{ scale: checked ? 1.05 : 0.95 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
{value && (
<Label
htmlFor="input-radio"
labelPosition={labelPosition}
disabled={disabled}
>
{value}
</Label>
)}
</Container>
);
}
Radio.Group = RadioGroup;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { useTheme } from '@emotion/react';
import { RadioProps } from './Radio';
type RadioGroupProps = React.PropsWithChildren & {
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onValueChange?: (value: string) => void;
};
export function RadioGroup({
value,
onChange,
onValueChange,
children,
}: RadioGroupProps) {
const theme = useTheme();
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
onChange?.(event);
onValueChange?.(event.target.value);
}
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement<RadioProps>(child)) {
return React.cloneElement(child, {
style: { marginBottom: theme.spacing(2) },
checked: child.props.value === value,
onChange: handleChange,
});
}
return child;
})}
</>
);
}

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { LabelPosition, Radio, RadioSize } from '../Radio';
const meta: Meta<typeof Radio> = {
title: 'UI/Input/Radio',
component: Radio,
};
export default meta;
type Story = StoryObj<typeof Radio>;
export const Default: Story = {
args: {
value: 'Radio',
checked: false,
disabled: false,
size: RadioSize.Small,
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: {
value: 'Radio',
},
argTypes: {
value: { control: false },
size: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'checked',
values: [false, true],
props: (checked: boolean) => ({ checked }),
},
{
name: 'disabled',
values: [false, true],
props: (disabled: boolean) => ({ disabled }),
},
{
name: 'size',
values: Object.values(RadioSize),
props: (size: RadioSize) => ({ size }),
},
{
name: 'labelPosition',
values: Object.values(LabelPosition),
props: (labelPosition: string) => ({
labelPosition,
}),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,6 +1,8 @@
import {
ChangeEvent,
FocusEventHandler,
ForwardedRef,
forwardRef,
InputHTMLAttributes,
useRef,
useState,
@ -13,6 +15,7 @@ import { IconAlertCircle } from '@/ui/icon';
import { IconEye, IconEyeOff } from '@/ui/icon/index';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
@ -95,22 +98,26 @@ const StyledTrailingIcon = styled.div`
const INPUT_TYPE_PASSWORD = 'password';
export function TextInput({
label,
value,
onChange,
onFocus,
onBlur,
fullWidth,
error,
required,
type,
disableHotkeys = false,
...props
}: OwnProps): JSX.Element {
function TextInputComponent(
{
label,
value,
onChange,
onFocus,
onBlur,
fullWidth,
error,
required,
type,
disableHotkeys = false,
...props
}: OwnProps,
ref: ForwardedRef<HTMLInputElement>,
): JSX.Element {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
const {
goBackToPreviousHotkeyScope,
@ -151,7 +158,7 @@ export function TextInput({
<StyledInputContainer>
<StyledInput
autoComplete="off"
ref={inputRef}
ref={combinedRef}
tabIndex={props.tabIndex ?? 0}
onFocus={handleFocus}
onBlur={handleBlur}
@ -189,3 +196,5 @@ export function TextInput({
</StyledContainer>
);
}
export const TextInput = forwardRef(TextInputComponent);