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

fix(Expense form): Update form inputs to use new inputs & improve error state #10909

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions components/AccountingCategorySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { cn } from '../lib/utils';
import { ACCOUNTING_CATEGORY_HOST_FIELDS } from './expenses/lib/accounting-categories';
import { isSameAccount } from '@/lib/collective';

import { Button } from './ui/Button';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/Command';
import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover';

Expand Down Expand Up @@ -331,7 +332,7 @@ const AccountingCategorySelect = ({
allowNone = false,
showCode = false,
expenseValues = undefined,
buttonClassName = 'rounded-lg',
buttonClassName = '',
children = null,
selectFirstOptionIfSingle,
disabled,
Expand Down Expand Up @@ -378,16 +379,15 @@ const AccountingCategorySelect = ({
<Popover open={isOpen} onOpenChange={setOpen}>
<PopoverTrigger asChild onBlur={onBlur} disabled={disabled}>
{children || (
<button
<Button
id={id}
variant="outline"
className={cn(
'flex w-full max-w-[300px] items-center justify-between border px-3 py-2',
buttonClassName,
'w-full max-w-[300px] font-normal',
{
'border-red-500': error,
'border-gray-300': !error,
'bg-[hsl(0,0%,95%)] text-[hsl(0,0%,60%)]': disabled,
'ring-2 ring-destructive ring-offset-2': error,
},
buttonClassName,
)}
disabled={disabled}
>
Expand All @@ -399,8 +399,8 @@ const AccountingCategorySelect = ({
{getCategoryLabel(intl, selectedCategory, false, valuesByRole) ||
intl.formatMessage({ defaultMessage: 'Select category', id: 'RUJYth' })}
</span>
<ChevronDown size="1em" className={cn({ 'text-[hsl(0,0%,80%)]': disabled })} />
</button>
<ChevronDown size="1em" />
</Button>
)}
</PopoverTrigger>
<PopoverContent className="min-w-[280px] p-0" style={{ width: 'var(--radix-popover-trigger-width)' }}>
Expand Down
194 changes: 62 additions & 132 deletions components/StyledDropzone.tsx → components/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,39 @@
import React from 'react';
import { ExclamationCircle } from '@styled-icons/fa-solid/ExclamationCircle';
import { Download as DownloadIcon } from '@styled-icons/feather/Download';
import { isNil, omit } from 'lodash';
import { Upload } from 'lucide-react';
import { isNil, isString, omit } from 'lodash';
import { CircleAlert, Upload } from 'lucide-react';
import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { FormattedMessage, useIntl } from 'react-intl';
import styled, { css } from 'styled-components';
import { v4 as uuid } from 'uuid';

import type { OcrParsingOptionsInput, UploadedFileKind, UploadFileResult } from '../lib/graphql/types/v2/schema';
import { useGraphQLFileUploader } from '../lib/hooks/useGraphQLFileUploader';
import { useImageUploader } from '../lib/hooks/useImageUploader';
import { cn } from '@/lib/utils';

import { Button } from './ui/Button';
import { useToast } from './ui/useToast';
import type { ContainerProps } from './Container';
import Container from './Container';
import { Box } from './Grid';
import { getI18nLink } from './I18nFormatters';
import LocalFilePreview from './LocalFilePreview';
import StyledSpinner from './StyledSpinner';
import { P, Span } from './Text';
import UploadedFilePreview from './UploadedFilePreview';

export const DROPZONE_ACCEPT_IMAGES = { 'image/*': ['.jpeg', '.png'] };
export const DROPZONE_ACCEPT_CSV = { 'text/csv': ['.csv'] };
export const DROPZONE_ACCEPT_PDF = { 'application/pdf': ['.pdf'] };
export const DROPZONE_ACCEPT_ALL = { ...DROPZONE_ACCEPT_IMAGES, ...DROPZONE_ACCEPT_PDF };

const Dropzone = styled(Container)<{ onClick?: () => void; error?: any }>`
border: 1px dashed #c4c7cc;
border-radius: 10px;
text-align: center;
background: white;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;

${props =>
props.onClick &&
css`
cursor: pointer;

&:hover:not(:disabled) {
background: #f9f9f9;
border-color: ${props => props.theme.colors.primary[300]};
}

&:focus {
outline: 0;
border-color: ${props => props.theme.colors.primary[500]};
}
`}

${props =>
props.error &&
css`
border: 1px solid ${props.theme.colors.red[500]};
`}

img {
max-height: 100%;
max-width: 100%;
}
`;

const ReplaceContainer = styled.div`
box-sizing: border-box;
background: rgba(49, 50, 51, 0.5);
color: #ffffff;
cursor: pointer;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 24px;
padding: 8px;
margin-top: -24px;
font-size: 12px;
line-height: 1em;

&:hover {
background: rgba(49, 50, 51, 0.6);
}
`;

/**
* A dropzone to upload one or multiple files
*/
const StyledDropzone = ({
const Dropzone = ({
onReject = undefined,
onDrop = undefined,
children = null,
isLoading = false,
loadingProgress = undefined,
minHeight = 96,
size,
fontSize = '14px',
mockImageGenerator = () => `https://loremflickr.com/120/120?lock=${uuid()}`,
accept,
minSize,
Expand All @@ -122,8 +56,9 @@ const StyledDropzone = ({
limit = undefined,
kind = null,
showReplaceAction = true,
className = '',
...props
}: StyledDropzoneProps) => {
}: DropzoneProps) => {
const { toast } = useToast();
const intl = useIntl();
const imgUploaderParams = { isMulti, mockImageGenerator, onSuccess, onReject, kind, accept, minSize, maxSize };
Expand Down Expand Up @@ -181,69 +116,65 @@ const StyledDropzone = ({
minHeight = size || minHeight;
const innerMinHeight = minHeight - 2; // -2 To account for the borders
const dropProps = getRootProps();

const errorMsg = isString(error) ? error : undefined;
return (
<Dropzone
position="relative"
<div
className={cn(
'group relative flex h-full w-full cursor-pointer place-items-center overflow-hidden rounded-lg border-2 border-dashed border-muted-foreground/25 text-center transition hover:bg-muted/25',
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'[&>img]:max-h-full [&>img]:max-w-full',
isDragActive && 'border-muted-foreground/50',
props.disabled && 'pointer-events-none opacity-60',
error && 'ring-2 ring-destructive ring-offset-2',
className,
)}
{...props}
{...(value ? omit(dropProps, ['onClick']) : dropProps)}
minHeight={size || minHeight}
size={size}
error={error}
style={{ height: size, width: size, minHeight: size || minHeight }}
>
<input name={name} disabled={props.disabled} {...getInputProps()} />
{isLoading || isUploading || isUploadingWithGraphQL ? (
<Container
position="relative"
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
width="100%"
minHeight={innerMinHeight}
<div
className="relative flex h-full w-full items-center justify-center"
style={{ minHeight: innerMinHeight }}
data-loading="true"
>
<Container
position="absolute"
display="flex"
justifyContent="center"
alignItems="center"
size={innerMinHeight}
<div
className="absolute flex items-center justify-center"
style={{ height: innerMinHeight, width: innerMinHeight }}
>
{UploadingComponent ? <UploadingComponent /> : <StyledSpinner size="70%" />}
</Container>
{isUploading && <Container fontSize="9px">{uploadProgress}%</Container>}
{isLoading && !isNil(loadingProgress) && <Container>{loadingProgress}%</Container>}
</Container>
{UploadingComponent ? <UploadingComponent /> : <StyledSpinner size="50%" />}
</div>
{isUploading && <div className="text-xs">{uploadProgress}%</div>}
{isLoading && !isNil(loadingProgress) && <div>{loadingProgress}%</div>}
</div>
) : (
<Container position="relative" maxWidth="100%">
<div className="relative w-full max-w-full">
{isDragActive ? (
<Container color="primary.500" fontSize="12px">
<Box mb={2}>
<DownloadIcon size={20} />
</Box>
<FormattedMessage
id="StyledDropzone.DropMsg"
defaultMessage="Drop {count,plural, one {file} other {files}} here"
values={{ count: isMulti ? 2 : 1 }}
/>
</Container>
<div className="flex flex-col items-center gap-2 text-xs">
<Upload size={20} />
<p>
<FormattedMessage
id="StyledDropzone.DropMsg"
defaultMessage="Drop {count,plural, one {file} other {files}} here"
values={{ count: isMulti ? 2 : 1 }}
/>
</p>
</div>
) : (
<React.Fragment>
{!value ? (
<Container color={error ? 'red.500' : 'black.600'} px={2} fontSize={fontSize}>
{error ? (
<React.Fragment>
<ExclamationCircle color="#E03F6A" size={16} />
<br />
<Span fontWeight={600} ml={1}>
{error}
</Span>
<br />
</React.Fragment>
<div className={cn('px-2 text-sm', errorMsg ? 'text-destructive' : 'text-muted-foreground')}>
{errorMsg ? (
<div className="flex flex-col items-center gap-1">
<CircleAlert size={20} />
<p className="font-semibold">{errorMsg}</p>
</div>
) : isMulti ? (
<div className="flex flex-col items-center">
{showIcon && (
<div className="mb-1 text-neutral-500">
<div className="mb-1 text-muted-foreground">
<Upload size={24} />
</div>
)}
Expand All @@ -255,7 +186,7 @@ const StyledDropzone = ({
/>
</div>
{showInstructions && (
<P fontSize="12px" color="black.500" mt={1}>
<p className="mt-1 text-xs text-muted-foreground">
<FormattedMessage
defaultMessage="{count,plural, one {File} other {Files}} should be {acceptedFormats} and no larger than {maxSize}."
id="StyledDropzone.FileInstructions"
Expand All @@ -275,13 +206,13 @@ const StyledDropzone = ({
/>
</span>
)}
</P>
</p>
)}
</div>
) : (
<div className="flex flex-col items-center">
{showIcon && (
<div className="mb-1 text-neutral-500">
<div className="mb-1 text-muted-foreground">
<Upload size={24} />
</div>
)}
Expand All @@ -303,7 +234,7 @@ const StyledDropzone = ({
)}
</div>
{showInstructions && (
<P fontSize="12px" color="black.500" mt={1}>
<p className="mt-1 text-xs text-muted-foreground">
<FormattedMessage
defaultMessage="{count,plural, one {File} other {Files}} should be {acceptedFormats} and no larger than {maxSize}."
id="StyledDropzone.FileInstructions"
Expand All @@ -313,16 +244,17 @@ const StyledDropzone = ({
maxSize: `${Math.round(maxSize / 1024 / 1024)}MB`,
}}
/>
</P>
</p>
)}
</div>
)}
</Container>
</div>
) : typeof value === 'string' ? (
<React.Fragment>
<UploadedFilePreview size={previewSize || size} url={value} border="none" />
{showReplaceAction && (
<ReplaceContainer
<div
className="absolute -mt-6 box-border flex h-6 w-full cursor-pointer items-center justify-center bg-foreground/50 p-2 text-xs leading-none text-background hover:bg-foreground/60"
onClick={dropProps.onClick}
role="button"
tabIndex={0}
Expand All @@ -334,7 +266,7 @@ const StyledDropzone = ({
}}
>
<FormattedMessage id="Image.Replace" defaultMessage="Replace" />
</ReplaceContainer>
</div>
)}
</React.Fragment>
) : value instanceof File ? (
Expand All @@ -343,13 +275,13 @@ const StyledDropzone = ({
{children}
</React.Fragment>
)}
</Container>
</div>
)}
{value && showActions && (
<div className="absolute right-3 top-3">
<Button
variant="outline"
size="sm"
size="xs"
onClick={() => {
if (isMulti) {
(onSuccess as (files: File[], fileRejections: FileRejection[]) => void)([], []);
Expand All @@ -363,7 +295,7 @@ const StyledDropzone = ({
</Button>
</div>
)}
</Dropzone>
</div>
);
};

Expand All @@ -374,7 +306,7 @@ type UploadedFile = {
type: string;
};

type StyledDropzoneProps = Omit<ContainerProps, 'accept' | 'children' | 'ref' | 'onClick' | 'as'> & {
type DropzoneProps = React.HTMLAttributes<HTMLDivElement> & {
/** Called back with the rejected files */
onReject?: (msg: string) => void;
/** Called when the user drops files */
Expand All @@ -387,8 +319,6 @@ type StyledDropzoneProps = Omit<ContainerProps, 'accept' | 'children' | 'ref' |
isLoading?: boolean;
/** Use this to override the loading progress indicator */
loadingProgress?: number;
/** Font size used for the default messages */
fontSize?: number | string;
/** Min height of the container */
minHeight?: number;
/** To have square container */
Expand Down Expand Up @@ -451,4 +381,4 @@ type StyledDropzoneProps = Omit<ContainerProps, 'accept' | 'children' | 'ref' |
))
);

export default StyledDropzone;
export default Dropzone;
Loading
Loading