Skip to content

Commit 2d8887c

Browse files
authored
Merge pull request #32 from qoretechnologies/feature/file-form-field
File upload field
2 parents b2a2663 + 2b6db96 commit 2d8887c

File tree

5 files changed

+243
-6
lines changed

5 files changed

+243
-6
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@qoretechnologies/reqraft",
3-
"version": "0.6.12",
3+
"version": "0.7.0",
44
"description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -111,10 +111,12 @@
111111
"classnames": "^2.2.6",
112112
"cronstrue": "^2.50.0",
113113
"epoch-timeago": "^1.1.9",
114+
"filesize": "^10.1.6",
114115
"js-yaml": "^4.1.0",
115116
"lodash": "^4.17.21",
116117
"polished": "^4.2.2",
117118
"react-color": "^2.19.3",
119+
"react-dropzone": "^14.3.5",
118120
"react-markdown": "^9.0.1",
119121
"react-use": "^17.4.0",
120122
"scheduler": "^0.23.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { StoryObj } from '@storybook/react';
2+
import { fn } from '@storybook/test';
3+
import { useState } from 'react';
4+
5+
import { StoryMeta } from '../../../../types';
6+
import { ReqraftFileFormField } from './File';
7+
8+
const meta = {
9+
component: ReqraftFileFormField,
10+
title: 'Components/Form/File',
11+
args: {
12+
onChange: fn(),
13+
},
14+
render(args) {
15+
const [value, setValue] = useState(args.value);
16+
return (
17+
<ReqraftFileFormField
18+
{...args}
19+
value={value}
20+
onChange={(value) => {
21+
args.onChange?.(value);
22+
setValue(value);
23+
}}
24+
/>
25+
);
26+
},
27+
} as StoryMeta<typeof ReqraftFileFormField>;
28+
29+
export default meta;
30+
type Story = StoryObj<typeof meta>;
31+
32+
export const Default: Story = {};
33+
export const WithSpecifiedExtensions: Story = {
34+
args: {
35+
options: {
36+
accept: {
37+
'image/png': ['.png'],
38+
'image/jpeg': ['.jpg', '.jpeg'],
39+
'application/pdf': ['.pdf'],
40+
},
41+
},
42+
},
43+
};
44+
45+
export const WithValue: Story = {
46+
args: {
47+
options: {
48+
accept: {
49+
'image/png': ['.png'],
50+
'image/jpeg': ['.jpg', '.jpeg'],
51+
'application/pdf': ['.pdf'],
52+
},
53+
},
54+
value: {
55+
name: 'MyFile.pdf',
56+
content: 'test',
57+
size: 28736,
58+
},
59+
},
60+
};
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
ReqoreButton,
3+
ReqoreControlGroup,
4+
ReqoreH4,
5+
ReqoreIcon,
6+
ReqoreP,
7+
ReqorePanel,
8+
} from '@qoretechnologies/reqore';
9+
import { IReqoreButtonProps } from '@qoretechnologies/reqore/dist/components/Button';
10+
import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
11+
import { filesize } from 'filesize';
12+
import { reduce, size } from 'lodash';
13+
import { useCallback, useEffect, useMemo } from 'react';
14+
import { Accept, DropzoneOptions, useDropzone } from 'react-dropzone';
15+
16+
export interface IReqraftFileFormFieldValue {
17+
name: string;
18+
content: string;
19+
size?: number;
20+
}
21+
export interface IReqraftFileFormFieldProps extends Omit<IReqorePanelProps, 'onChange'> {
22+
value: IReqraftFileFormFieldValue;
23+
onChange(value: IReqraftFileFormFieldValue): void;
24+
readonly?: boolean;
25+
options?: DropzoneOptions;
26+
valueButtonProps?: IReqoreButtonProps;
27+
}
28+
29+
export const ReqraftFileFormField = ({
30+
value,
31+
onChange,
32+
options = {},
33+
valueButtonProps = {},
34+
...rest
35+
}: IReqraftFileFormFieldProps) => {
36+
const contentStyle: React.CSSProperties = useMemo(
37+
(): React.CSSProperties => ({
38+
display: 'flex',
39+
flexDirection: 'column',
40+
justifyContent: 'center',
41+
alignItems: 'center',
42+
}),
43+
[]
44+
);
45+
46+
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
47+
disabled: rest.disabled || rest.readonly,
48+
maxFiles: 1,
49+
50+
...options,
51+
});
52+
53+
const extensions = useMemo(() => {
54+
if (!options.accept) {
55+
return [];
56+
}
57+
58+
return reduce<Accept, string[]>(
59+
options.accept,
60+
(acc, ext) => {
61+
return [...acc, ...ext];
62+
},
63+
[]
64+
);
65+
}, [options.accept]);
66+
67+
const renderExtensions = useCallback(
68+
(asString?: boolean) => {
69+
if (size(extensions) === 0) {
70+
return '';
71+
}
72+
73+
if (asString) {
74+
return extensions.join(', ');
75+
}
76+
77+
return (
78+
<ReqoreP intent='muted' size='small'>
79+
{extensions.join(', ')}
80+
</ReqoreP>
81+
);
82+
},
83+
[extensions]
84+
);
85+
86+
useEffect(() => {
87+
if (acceptedFiles.length === 0) {
88+
return;
89+
}
90+
91+
const reader = new FileReader();
92+
93+
reader.onload = () => {
94+
onChange({
95+
name: acceptedFiles[0].name,
96+
content: reader.result as string,
97+
size: acceptedFiles[0].size,
98+
});
99+
};
100+
101+
reader.readAsDataURL(acceptedFiles[0]);
102+
}, [acceptedFiles]);
103+
104+
if (value) {
105+
return (
106+
<>
107+
<input {...getInputProps()} />
108+
<ReqoreButton
109+
label={value.name}
110+
minimal
111+
intent='info'
112+
icon='FileLine'
113+
rightIcon='FileUploadLine'
114+
badge={filesize(value.size || 0)}
115+
description={`Click here to upload a different ${renderExtensions(true)} file`}
116+
{...valueButtonProps}
117+
{...getRootProps()}
118+
/>
119+
</>
120+
);
121+
}
122+
123+
return (
124+
<ReqorePanel contentStyle={contentStyle} {...rest} {...getRootProps()} size='huge'>
125+
<input {...getInputProps()} />
126+
<ReqoreControlGroup vertical horizontalAlign='center'>
127+
<ReqoreH4 size='small'>
128+
<ReqoreIcon icon='FileAddLine' size='small' /> Click or drop files here to upload
129+
</ReqoreH4>
130+
{renderExtensions()}
131+
</ReqoreControlGroup>
132+
</ReqorePanel>
133+
);
134+
};

src/components/form/fields/long-string/LongString.tsx

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReqoreTextarea } from '@qoretechnologies/reqore';
22
import { IReqoreTextareaProps } from '@qoretechnologies/reqore/dist/components/Textarea';
3+
import { useCallback } from 'react';
34
import { TFormFieldValueType } from '../../../../types/Form';
45

56
export interface ILongStringFormFieldProps extends Omit<IReqoreTextareaProps, 'onChange'> {
@@ -14,15 +15,24 @@ export const LongStringFormField = ({
1415
onClearClick,
1516
...rest
1617
}: ILongStringFormFieldProps) => {
18+
const handleClearClick = useCallback(() => {
19+
onClearClick?.();
20+
onChange?.('');
21+
}, [onClearClick, onChange]);
22+
23+
const handleChange = useCallback(
24+
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
25+
onChange?.(event.currentTarget.value, event);
26+
},
27+
[onChange]
28+
);
29+
1730
return (
1831
<ReqoreTextarea
1932
scaleWithContent
2033
fluid
21-
onClearClick={() => {
22-
onClearClick?.();
23-
onChange?.('');
24-
}}
25-
onChange={(event) => onChange(event.currentTarget.value, event)}
34+
onClearClick={handleClearClick}
35+
onChange={handleChange}
2636
rows={4}
2737
{...rest}
2838
/>

yarn.lock

+31
Original file line numberDiff line numberDiff line change
@@ -4782,6 +4782,11 @@ asynckit@^0.4.0:
47824782
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
47834783
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
47844784

4785+
attr-accept@^2.2.4:
4786+
version "2.2.5"
4787+
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e"
4788+
integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==
4789+
47854790
available-typed-arrays@^1.0.7:
47864791
version "1.0.7"
47874792
resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
@@ -6608,6 +6613,13 @@ file-entry-cache@^6.0.1:
66086613
dependencies:
66096614
flat-cache "^3.0.4"
66106615

6616+
file-selector@^2.1.0:
6617+
version "2.1.2"
6618+
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
6619+
integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==
6620+
dependencies:
6621+
tslib "^2.7.0"
6622+
66116623
file-system-cache@2.3.0:
66126624
version "2.3.0"
66136625
resolved "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz"
@@ -6621,6 +6633,11 @@ filesize@^10.0.12:
66216633
resolved "https://registry.npmjs.org/filesize/-/filesize-10.1.1.tgz"
66226634
integrity sha512-L0cdwZrKlwZQkMSFnCflJ6J2Y+5egO/p3vgRSDQGxQt++QbUZe5gMbRO6kg6gzwQDPvq2Fk9AmoxUNfZ5gdqaQ==
66236635

6636+
filesize@^10.1.6:
6637+
version "10.1.6"
6638+
resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361"
6639+
integrity sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==
6640+
66246641
fill-range@^7.0.1:
66256642
version "7.0.1"
66266643
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
@@ -10178,6 +10195,15 @@ react-docgen@^7.0.0:
1017810195
loose-envify "^1.1.0"
1017910196
scheduler "^0.23.2"
1018010197

10198+
react-dropzone@^14.3.5:
10199+
version "14.3.5"
10200+
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.5.tgz#1a8bd312c8a353ec78ef402842ccb3589c225add"
10201+
integrity sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==
10202+
dependencies:
10203+
attr-accept "^2.2.4"
10204+
file-selector "^2.1.0"
10205+
prop-types "^15.8.1"
10206+
1018110207
react-element-to-jsx-string@^15.0.0:
1018210208
version "15.0.0"
1018310209
resolved "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz"
@@ -11658,6 +11684,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
1165811684
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
1165911685
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
1166011686

11687+
tslib@^2.7.0:
11688+
version "2.8.1"
11689+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
11690+
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
11691+
1166111692
tunnel-agent@^0.6.0:
1166211693
version "0.6.0"
1166311694
resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"

0 commit comments

Comments
 (0)