initial version of json file upload component (#75)

This commit is contained in:
Erik Jan de Wit 2020-09-08 19:16:08 +02:00 committed by GitHub
parent c59a7198c9
commit 082682e6d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 690 additions and 88 deletions

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
...require("@snowpack/app-scripts-react/jest.config.js")(), ...require("@snowpack/app-scripts-react/jest.config.js")(),
"snapshotSerializers": ["enzyme-to-json/serializer"],
"moduleNameMapper": { "moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js", "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/src/__mocks__/styleMock.js" "\\.(css|less)$": "<rootDir>/src/__mocks__/styleMock.js"

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { Button, AlertVariant } from "@patternfly/react-core"; import { Button, AlertVariant } from "@patternfly/react-core";
import { mount } from "enzyme"; import { mount } from "enzyme";
import EnzymeToJson from "enzyme-to-json";
import { act } from "react-dom/test-utils"; import { act } from "react-dom/test-utils";
import { AlertPanel } from "./AlertPanel"; import { AlertPanel } from "./AlertPanel";
@ -20,9 +19,7 @@ const WithButton = () => {
}; };
it("renders global alerts", () => { it("renders global alerts", () => {
const empty = EnzymeToJson( const empty = mount(<AlertPanel alerts={[]} onCloseAlert={() => {}} />);
mount(<AlertPanel alerts={[]} onCloseAlert={() => {}} />)
);
expect(empty).toMatchSnapshot(); expect(empty).toMatchSnapshot();
const tree = mount(<WithButton />); const tree = mount(<WithButton />);
@ -32,10 +29,10 @@ it("renders global alerts", () => {
act(() => { act(() => {
button!.simulate("click"); button!.simulate("click");
}); });
expect(EnzymeToJson(tree)).toMatchSnapshot(); expect(tree).toMatchSnapshot();
act(() => { act(() => {
jest.runAllTimers(); jest.runAllTimers();
}); });
expect(EnzymeToJson(tree)).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });

View file

@ -0,0 +1,35 @@
import React from "react";
import { mount } from "enzyme";
import { JsonFileUpload } from "./JsonFileUpload";
describe("<JsonFileUpload />", () => {
it("render", () => {
const comp = mount(<JsonFileUpload id="test" onChange={jest.fn()} />);
expect(comp).toMatchSnapshot();
});
it("upload file", async () => {
const onChange = jest.fn((value) => value);
const comp = mount(<JsonFileUpload id="upload" onChange={onChange} />);
const fileInput = comp.find('[type="file"]');
expect(fileInput.length).toBe(1);
const json = '{"bla": "test"}';
const file = new File([json], "test.json");
const dummyFileReader = {
onload: jest.fn(),
readAsText: () => Promise.resolve(json),
};
(window as any).FileReader = jest.fn(() => dummyFileReader);
fileInput.simulate("change", {
target: {
files: [file],
},
});
expect(comp).toMatchSnapshot();
});
});

View file

@ -0,0 +1,118 @@
import React, { useState } from "react";
import {
FormGroup,
FileUpload,
Modal,
ModalVariant,
Button,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
type FileUpload = {
value: string | File;
filename: string;
isLoading: boolean;
modal: boolean;
};
export type JsonFileUploadProps = {
id: string;
onChange: (
value: string | File,
filename: string,
event:
| React.DragEvent<HTMLElement> // User dragged/dropped a file
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
| React.MouseEvent<HTMLButtonElement, MouseEvent> // User clicked Clear button
) => void;
};
export const JsonFileUpload = ({
id,
onChange,
...rest
}: JsonFileUploadProps) => {
const { t } = useTranslation();
const defaultUpload = {
value: "",
filename: "",
isLoading: false,
modal: false,
};
const [fileUpload, setFileUpload] = useState<FileUpload>(defaultUpload);
const removeDialog = () => setFileUpload({ ...fileUpload, modal: false });
const handleChange = (
value: string | File,
filename: string,
event:
| React.DragEvent<HTMLElement>
| React.ChangeEvent<HTMLTextAreaElement>
| React.MouseEvent<HTMLButtonElement, MouseEvent>
): void => {
if (event.nativeEvent instanceof MouseEvent) {
setFileUpload({ ...fileUpload, modal: true });
} else {
setFileUpload({
...fileUpload,
value,
filename,
});
onChange(value, filename, event);
}
};
return (
<>
{fileUpload.modal && (
<Modal
variant={ModalVariant.small}
title={t("Clear this file")}
isOpen
onClose={removeDialog}
actions={[
<Button
key="confirm"
variant="primary"
onClick={(event) => {
setFileUpload(defaultUpload);
onChange("", "", event);
}}
>
{t("Clear")}
</Button>,
<Button key="cancel" variant="link" onClick={removeDialog}>
{t("Cancel")}
</Button>,
]}
>
{t("confirmImportClear")}
</Modal>
)}
<FormGroup
label={t("Resource file")}
fieldId={id}
helperText="Upload a JSON file"
>
<FileUpload
id={id}
{...rest}
type="text"
value={fileUpload.value}
filename={fileUpload.filename}
onChange={handleChange}
allowEditingUploadedText
onReadStarted={() =>
setFileUpload({ ...fileUpload, isLoading: true })
}
onReadFinished={() =>
setFileUpload({ ...fileUpload, isLoading: false })
}
isLoading={fileUpload.isLoading}
dropzoneProps={{
accept: ".json",
}}
/>
</FormGroup>
</>
);
};

View file

@ -0,0 +1,504 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<JsonFileUpload /> render 1`] = `
<JsonFileUpload
id="test"
onChange={[MockFunction]}
>
<FormGroup
fieldId="test"
helperText="Upload a JSON file"
label="Resource file"
>
<div
className="pf-c-form__group"
>
<div
className="pf-c-form__group-label"
>
<label
className="pf-c-form__label"
htmlFor="test"
>
<span
className="pf-c-form__label-text"
>
Resource file
</span>
</label>
</div>
<div
className="pf-c-form__group-control"
>
<FileUpload
allowEditingUploadedText={true}
dropzoneProps={
Object {
"accept": ".json",
}
}
filename=""
id="test"
isLoading={false}
onChange={[Function]}
onReadFinished={[Function]}
onReadStarted={[Function]}
type="text"
value=""
>
<t
accept=".json"
disabled={false}
getDataTransferItems={[Function]}
maxSize={Infinity}
minSize={0}
multiple={false}
onDropAccepted={[Function]}
onDropRejected={[Function]}
preventDropOnDocument={true}
>
<FileUploadField
allowEditingUploadedText={true}
containerRef={[Function]}
filename=""
id="test"
isLoading={false}
onBlur={[Function]}
onBrowseButtonClick={[Function]}
onChange={[Function]}
onClearButtonClick={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
tabIndex={null}
type="text"
value=""
>
<div
className="pf-c-file-upload"
onBlur={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
tabIndex={null}
>
<div
className="pf-c-file-upload__file-select"
>
<InputGroup>
<div
className="pf-c-input-group"
>
<ForwardRef
aria-describedby="test-browse-button"
aria-label="Drag a file here or browse to upload"
id="test-filename"
isDisabled={false}
isReadOnly={true}
key=".0"
name="test-filename"
placeholder="Drag a file here or browse to upload"
value=""
>
<TextInputBase
aria-describedby="test-browse-button"
aria-label="Drag a file here or browse to upload"
className=""
id="test-filename"
innerRef={null}
isDisabled={false}
isReadOnly={true}
isRequired={false}
name="test-filename"
onChange={[Function]}
placeholder="Drag a file here or browse to upload"
type="text"
validated="default"
value=""
>
<input
aria-describedby="test-browse-button"
aria-invalid={false}
aria-label="Drag a file here or browse to upload"
className="pf-c-form-control"
disabled={false}
id="test-filename"
name="test-filename"
onChange={[Function]}
placeholder="Drag a file here or browse to upload"
readOnly={true}
required={false}
type="text"
value=""
/>
</TextInputBase>
</ForwardRef>
<Button
id="test-browse-button"
isDisabled={false}
key=".1"
onClick={[Function]}
variant="control"
>
<button
aria-disabled={false}
aria-label={null}
className="pf-c-button pf-m-control"
data-ouia-component-id={0}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
id="test-browse-button"
onClick={[Function]}
type="button"
>
Browse...
</button>
</Button>
<Button
isDisabled={true}
key=".2"
onClick={[Function]}
variant="control"
>
<button
aria-disabled={true}
aria-label={null}
className="pf-c-button pf-m-control pf-m-disabled"
data-ouia-component-id={1}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={true}
onClick={[Function]}
tabIndex={null}
type="button"
>
Clear
</button>
</Button>
</div>
</InputGroup>
</div>
<div
className="pf-c-file-upload__file-details"
>
<TextArea
aria-label="File upload"
className=""
disabled={false}
id="test"
isRequired={false}
name="test"
onChange={[Function]}
readOnly={false}
resizeOrientation="vertical"
validated="default"
value=""
>
<textarea
aria-invalid={false}
aria-label="File upload"
className="pf-c-form-control pf-m-resize-vertical"
disabled={false}
id="test"
name="test"
onChange={[Function]}
readOnly={false}
required={false}
value=""
/>
</TextArea>
</div>
<input
accept=".json"
autoComplete="off"
multiple={false}
onChange={[Function]}
onClick={[Function]}
style={
Object {
"display": "none",
}
}
tabIndex={-1}
type="file"
/>
</div>
</FileUploadField>
</t>
</FileUpload>
<div
aria-live="polite"
className="pf-c-form__helper-text"
id="test-helper"
>
Upload a JSON file
</div>
</div>
</div>
</FormGroup>
</JsonFileUpload>
`;
exports[`<JsonFileUpload /> upload file 1`] = `
<JsonFileUpload
id="upload"
onChange={[MockFunction]}
>
<FormGroup
fieldId="upload"
helperText="Upload a JSON file"
label="Resource file"
>
<div
className="pf-c-form__group"
>
<div
className="pf-c-form__group-label"
>
<label
className="pf-c-form__label"
htmlFor="upload"
>
<span
className="pf-c-form__label-text"
>
Resource file
</span>
</label>
</div>
<div
className="pf-c-form__group-control"
>
<FileUpload
allowEditingUploadedText={true}
dropzoneProps={
Object {
"accept": ".json",
}
}
filename=""
id="upload"
isLoading={false}
onChange={[Function]}
onReadFinished={[Function]}
onReadStarted={[Function]}
type="text"
value=""
>
<t
accept=".json"
disabled={false}
getDataTransferItems={[Function]}
maxSize={Infinity}
minSize={0}
multiple={false}
onDropAccepted={[Function]}
onDropRejected={[Function]}
preventDropOnDocument={true}
>
<FileUploadField
allowEditingUploadedText={true}
containerRef={[Function]}
filename=""
id="upload"
isDragActive={false}
isLoading={false}
onBlur={[Function]}
onBrowseButtonClick={[Function]}
onChange={[Function]}
onClearButtonClick={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
tabIndex={null}
type="text"
value=""
>
<div
className="pf-c-file-upload"
onBlur={[Function]}
onClick={[Function]}
onDragEnter={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
tabIndex={null}
>
<div
className="pf-c-file-upload__file-select"
>
<InputGroup>
<div
className="pf-c-input-group"
>
<ForwardRef
aria-describedby="upload-browse-button"
aria-label="Drag a file here or browse to upload"
id="upload-filename"
isDisabled={false}
isReadOnly={true}
key=".0"
name="upload-filename"
placeholder="Drag a file here or browse to upload"
value=""
>
<TextInputBase
aria-describedby="upload-browse-button"
aria-label="Drag a file here or browse to upload"
className=""
id="upload-filename"
innerRef={null}
isDisabled={false}
isReadOnly={true}
isRequired={false}
name="upload-filename"
onChange={[Function]}
placeholder="Drag a file here or browse to upload"
type="text"
validated="default"
value=""
>
<input
aria-describedby="upload-browse-button"
aria-invalid={false}
aria-label="Drag a file here or browse to upload"
className="pf-c-form-control"
disabled={false}
id="upload-filename"
name="upload-filename"
onChange={[Function]}
placeholder="Drag a file here or browse to upload"
readOnly={true}
required={false}
type="text"
value=""
/>
</TextInputBase>
</ForwardRef>
<Button
id="upload-browse-button"
isDisabled={false}
key=".1"
onClick={[Function]}
variant="control"
>
<button
aria-disabled={false}
aria-label={null}
className="pf-c-button pf-m-control"
data-ouia-component-id={4}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
id="upload-browse-button"
onClick={[Function]}
type="button"
>
Browse...
</button>
</Button>
<Button
isDisabled={true}
key=".2"
onClick={[Function]}
variant="control"
>
<button
aria-disabled={true}
aria-label={null}
className="pf-c-button pf-m-control pf-m-disabled"
data-ouia-component-id={5}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={true}
onClick={[Function]}
tabIndex={null}
type="button"
>
Clear
</button>
</Button>
</div>
</InputGroup>
</div>
<div
className="pf-c-file-upload__file-details"
>
<TextArea
aria-label="File upload"
className=""
disabled={false}
id="upload"
isRequired={false}
name="upload"
onChange={[Function]}
readOnly={false}
resizeOrientation="vertical"
validated="default"
value=""
>
<textarea
aria-invalid={false}
aria-label="File upload"
className="pf-c-form-control pf-m-resize-vertical"
disabled={false}
id="upload"
name="upload"
onChange={[Function]}
readOnly={false}
required={false}
value=""
/>
</TextArea>
</div>
<input
accept=".json"
autoComplete="off"
multiple={false}
onChange={[Function]}
onClick={[Function]}
style={
Object {
"display": "none",
}
}
tabIndex={-1}
type="file"
/>
</div>
</FileUploadField>
</t>
</FileUpload>
<div
aria-live="polite"
className="pf-c-form__helper-text"
id="upload-helper"
>
Upload a JSON file
</div>
</div>
</div>
</FormGroup>
</JsonFileUpload>
`;

View file

@ -6,41 +6,25 @@ import {
Divider, Divider,
Form, Form,
FormGroup, FormGroup,
FileUpload,
TextInput, TextInput,
ActionGroup, ActionGroup,
Button, Button,
AlertVariant, AlertVariant,
Modal,
ModalVariant,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ClientRepresentation } from "../../model/client-model"; import { ClientRepresentation } from "../../model/client-model";
import { ClientDescription } from "./ClientDescription"; import { ClientDescription } from "./ClientDescription";
import { HttpClientContext } from "../../http-service/HttpClientContext"; import { HttpClientContext } from "../../http-service/HttpClientContext";
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { AlertPanel } from "../../components/alert/AlertPanel"; import { AlertPanel } from "../../components/alert/AlertPanel";
type FileUpload = {
value: string | File;
filename: string;
isLoading: boolean;
modal: boolean;
};
export const ImportForm = () => { export const ImportForm = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const httpClient = useContext(HttpClientContext)!; const httpClient = useContext(HttpClientContext)!;
const [add, alerts, hide] = useAlerts(); const [add, alerts, hide] = useAlerts();
const defaultUpload = {
value: "",
filename: "",
isLoading: false,
modal: false,
};
const [fileUpload, setFileUpload] = useState<FileUpload>(defaultUpload);
const defaultClient = { const defaultClient = {
protocol: "", protocol: "",
clientId: "", clientId: "",
@ -49,21 +33,11 @@ export const ImportForm = () => {
}; };
const [client, setClient] = useState<ClientRepresentation>(defaultClient); const [client, setClient] = useState<ClientRepresentation>(defaultClient);
const handleFileChange = (value: string | File, filename: string) => { const handleFileChange = (value: string | File) => {
if (value === "" && client.protocol !== "") { setClient({
// clear clicked ...client,
setFileUpload({ ...fileUpload, modal: true }); ...(value ? JSON.parse(value as string) : defaultClient),
} else { });
setFileUpload({
...fileUpload,
value,
filename,
});
setClient({
...client,
...(value ? JSON.parse(value as string) : defaultClient),
});
}
}; };
const handleDescriptionChange = ( const handleDescriptionChange = (
value: string, value: string,
@ -72,7 +46,6 @@ export const ImportForm = () => {
const name = (event.target as HTMLInputElement).name; const name = (event.target as HTMLInputElement).name;
setClient({ ...client, [name]: value }); setClient({ ...client, [name]: value });
}; };
const removeDialog = () => setFileUpload({ ...fileUpload, modal: false });
const save = async () => { const save = async () => {
try { try {
@ -95,56 +68,8 @@ export const ImportForm = () => {
</PageSection> </PageSection>
<Divider /> <Divider />
<PageSection variant="light"> <PageSection variant="light">
{fileUpload.modal && (
<Modal
variant={ModalVariant.small}
title={t("Clear this file")}
isOpen
onClose={removeDialog}
actions={[
<Button
key="confirm"
variant="primary"
onClick={() => {
setClient(defaultClient);
setFileUpload(defaultUpload);
}}
>
{t("Clear")}
</Button>,
<Button key="cancel" variant="link" onClick={removeDialog}>
{t("Cancel")}
</Button>,
]}
>
{t("confirmImportClear")}
</Modal>
)}
<Form isHorizontal> <Form isHorizontal>
<FormGroup <JsonFileUpload id="realm-file" onChange={handleFileChange} />
label={t("Resource file")}
fieldId="realm-file"
helperText="Upload a JSON file"
>
<FileUpload
id="realm-file"
type="text"
value={fileUpload.value}
filename={fileUpload.filename}
onChange={handleFileChange}
allowEditingUploadedText
onReadStarted={() =>
setFileUpload({ ...fileUpload, isLoading: true })
}
onReadFinished={() =>
setFileUpload({ ...fileUpload, isLoading: false })
}
isLoading={fileUpload.isLoading}
dropzoneProps={{
accept: ".json",
}}
/>
</FormGroup>
<ClientDescription <ClientDescription
onChange={handleDescriptionChange} onChange={handleDescriptionChange}
client={client} client={client}

View file

@ -0,0 +1,22 @@
import React from "react";
import { Meta, Story } from "@storybook/react";
import {
JsonFileUpload,
JsonFileUploadProps,
} from "../components/json-file-upload/JsonFileUpload";
export default {
title: "Json file upload dailog",
component: JsonFileUpload,
parameters: { actions: { argTypesRegex: "^on.*" } },
} as Meta;
const Template: Story<JsonFileUploadProps> = (args) => (
<JsonFileUpload {...args} />
);
export const Dialog = Template.bind({});
Dialog.args = {
id: "jsonFile",
};