Add basic company subform UX demo

This commit is contained in:
Kacper Donat 2020-06-10 23:47:07 +02:00
parent a3710d4b9b
commit ee44cbf2f7
11 changed files with 194 additions and 88 deletions

View File

@ -13,7 +13,6 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"formik": "^2.1.4",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",

View File

@ -1,14 +1,21 @@
import React from 'react';
import { Container } from "@material-ui/core";
import { Container, ThemeProvider, Typography } from "@material-ui/core";
import { CompanyForm } from "./forms/company";
import { studentTheme } from "./ui/theme";
function App() {
return (
<div className="app">
<Container>
<CompanyForm />
</Container>
</div>
<ThemeProvider theme={ studentTheme }>
<div className="app">
<Container maxWidth={"md"}>
<Typography variant="h3">Zgłoszenie Praktyki</Typography>
<Typography variant="subtitle1" style={{ marginBottom: '100px' }}>UX Demo</Typography>
<Typography variant="h5">Miejsce odbywania praktyki</Typography>
<CompanyForm />
</Container>
</div>
</ThemeProvider>
);
}

3
src/data/common.ts Normal file
View File

@ -0,0 +1,3 @@
export interface Identifiable {
id?: string
}

View File

@ -1,3 +1,5 @@
import { Identifiable } from "./common";
export interface Address {
street: string;
building: string;
@ -6,14 +8,14 @@ export interface Address {
country: string;
}
export interface Company {
export interface Company extends Identifiable {
name: string;
url?: string;
nip: string;
offices: BranchOffice[];
}
export interface BranchOffice {
export interface BranchOffice extends Identifiable {
address: Address;
}
@ -24,4 +26,16 @@ export const emptyCompany: Company = {
nip: "",
}
export const emptyAddress: Address = {
street: "",
city: "",
country: "",
postalCode: "",
building: ""
}
export const emptyBranchOffice: BranchOffice = {
address: emptyAddress,
}
export const formatAddress = ({ postalCode, building, street, city, country }: Address): string => `${street} ${building}, ${postalCode} ${city}, ${country}`

View File

@ -1,69 +1,139 @@
import React, { HTMLProps, useState } from "react";
import { useFormik } from "formik";
import { Nullable } from "../helpers";
import { formatAddress, Company, emptyCompany, BranchOffice } from "../data/company";
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress } from "../data/company";
import { sampleCompanies } from "../provider/company";
import { Autocomplete } from "@material-ui/lab";
import { Box, Grid, TextField, Typography } from "@material-ui/core";
import { Grid, TextField, Typography } from "@material-ui/core";
import { formFieldProps } from "./helpers";
export type CompanyFormProps = {
export type CompanyFormProps = {}
export type BranchOfficeProps = {
company: Company,
disabled?: boolean
}
export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
<div className="company-item" {...props}>
<div className="company-item" { ...props }>
<div>{ company.name }</div>
<Typography variant="caption">NIP: <strong>{ company.nip }</strong></Typography>
</div>
)
export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTMLProps<any>) => (
<div className="office-item" {...props}>
<div>{ office.address.city }, { office.address.street }</div>
<div className="office-item" { ...props }>
<div>{ office.address.city }</div>
<Typography variant="caption">{ formatAddress(office.address) }</Typography>
</div>
)
export const CompanyForm: React.FunctionComponent<CompanyFormProps> = props => {
const formik = useFormik<Nullable<Company>>({ onSubmit: values => console.log(values), initialValues: emptyCompany });
export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = false }) => {
const [office, setOffice] = useState<BranchOffice>(emptyBranchOffice)
const formikProps = (prop: keyof Company) => ({ onChange: formik.handleChange, value: formik.values[prop], name: prop })
const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]);
const [company, setCompany] = useState<Company | null>(null);
const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address }))
const handleCityChange = (event: any, value: BranchOffice | string | null) => {
if (typeof value === "string") {
setOffice({
...emptyBranchOffice,
address: {
...emptyAddress,
city: value,
}
});
} else if (typeof value === "object" && value !== null) {
setOffice(value);
} else {
setOffice(emptyBranchOffice);
}
}
const handleCityInput = (event: any, value: string) => {
const base = office.id ? emptyBranchOffice : office;
setOffice({
...base,
address: {
...base.address,
city: value,
}
})
}
useEffect(() => void (office.id && setOffice(emptyBranchOffice)), [company])
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<Autocomplete options={ sampleCompanies }
getOptionLabel={ option => option.name }
renderOption={ company => <CompanyItem company={company} /> }
renderInput={ props => <TextField {...props} label={"Nazwa firmy"} fullWidth />}
onChange={ (ev, value: Company | null) => {
setCompany(value)
formik.setFieldValue("name", value?.name || "")
formik.setFieldValue("url", value?.url || "");
formik.setFieldValue("nip", value?.nip || "");
} }
/>
</Grid>
<Grid container item spacing={2} xs={12}>
<Grid item xs={4}>
<TextField label={"NIP"} fullWidth {...formikProps("nip")} disabled={ company != null}/>
</Grid>
<Grid item xs={8}>
<TextField label={"Url"} fullWidth {...formikProps("url")} disabled={ company != null }/>
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container>
<Grid item md={ 7 }>
<Autocomplete options={ company?.offices || [] }
disabled={ company == null}
getOptionLabel={ office => office.address.city }
renderOption={ office => <OfficeItem office={ office } /> }
renderInput={ props => <TextField {...props} label={"Oddział"} fullWidth />}
disabled={ disabled }
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
renderOption={ office => <OfficeItem office={ office }/> }
renderInput={ props => <TextField { ...props } label={ "Miasto" } fullWidth/> }
onChange={ handleCityChange }
onInputChange={ handleCityInput }
inputValue={ office.address.city }
value={ office.id ? office : null }
freeSolo
/>
</Grid>
<Grid item md={ 2 }>
<TextField label={ "Kod pocztowy" } fullWidth disabled={ !canEdit } { ...fieldProps("postalCode") }/>
</Grid>
<Grid item md={ 3 }>
<TextField label={ "Kraj" } fullWidth disabled={ !canEdit } { ...fieldProps("country") }/>
</Grid>
<Grid item md={ 10 }>
<TextField label={ "Ulica" } fullWidth disabled={ !canEdit } { ...fieldProps("street") }/>
</Grid>
<Grid item md={ 2 }>
<TextField label={ "Nr Budynku" } fullWidth disabled={ !canEdit } { ...fieldProps("building") }/>
</Grid>
</Grid>
)
}
export const CompanyForm: React.FunctionComponent<CompanyFormProps> = props => {
const [company, setCompany] = useState<Company>(emptyCompany);
const canEdit = useMemo(() => !company.id, [company.id]);
const fieldProps = formFieldProps(company, setCompany)
const handleCompanyChange = (event: any, value: Company | string | null) => {
if (typeof value === "string") {
setCompany({
...emptyCompany,
name: value,
});
} else if (typeof value === "object" && value !== null) {
setCompany(value);
} else {
setCompany(emptyCompany);
}
}
return (
<>
<Grid container style={ { marginBottom: 0, marginTop: 0 } }>
<Grid item>
<Autocomplete options={ sampleCompanies }
getOptionLabel={ option => option.name }
renderOption={ company => <CompanyItem company={ company }/> }
renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> }
onChange={ handleCompanyChange }
freeSolo
/>
</Grid>
<Grid item md={ 4 }>
<TextField label={ "NIP" } fullWidth { ...fieldProps("nip") } disabled={ !canEdit }/>
</Grid>
<Grid item md={ 8 }>
<TextField label={ "Url" } fullWidth { ...fieldProps("url") } disabled={ !canEdit }/>
</Grid>
</Grid>
<Typography variant="subtitle1">Oddział</Typography>
<BranchForm company={ company }/>
</>
)
}

20
src/forms/helpers.ts Normal file
View File

@ -0,0 +1,20 @@
import { DOMEvent } from "../helpers";
type UpdatingEvent = "onBlur" | "onChange" | "onInput";
type FormFieldHelperOptions<T> = {
event: UpdatingEvent
}
export function formFieldProps<T>(subject: T, update: (value: T) => void, options: Partial<FormFieldHelperOptions<T>> = {}) {
const {
event = "onChange"
} = options;
return (field: keyof T) => ({
value: subject[field],
[event]: (event: DOMEvent<HTMLInputElement>) => update({
...subject,
[field]: event.target.value,
} as T)
})
}

View File

@ -1 +1,5 @@
export type Nullable<T> = { [P in keyof T]: T[P] | null }
export interface DOMEvent<TTarget extends EventTarget> extends Event {
target: TTarget;
}

View File

@ -1,11 +1,17 @@
import { Company } from "../data/company";
import { makeIdSequence } from "./helpers";
const companySequence = makeIdSequence();
const officeSequence = makeIdSequence();
export const sampleCompanies: Company[] = [
{
id: companySequence(),
name: "Intel",
url: "https://www.intel.com/content/www/us/en/jobs/locations/poland.html",
nip: "9570752316",
offices: [{
id: officeSequence(),
address: {
city: "Gdańsk",
street: "ul. Słowackiego",
@ -16,10 +22,12 @@ export const sampleCompanies: Company[] = [
}]
},
{
id: companySequence(),
name: "IHS Markit",
url: "http://ihsgdansk.com/",
nip: "5842068320",
offices: [{
id: officeSequence(),
address: {
city: "Gdańsk",
street: "ul. Marynarki Polskiej",
@ -30,10 +38,12 @@ export const sampleCompanies: Company[] = [
}]
},
{
id: companySequence(),
name: "Asseco Poland",
url: "http://pl.asseco.com/",
nip: "5842068320",
offices: [{
id: officeSequence(),
address: {
city: "Gdynia",
street: "ul. Podolska",
@ -42,6 +52,7 @@ export const sampleCompanies: Company[] = [
country: "Poland"
}
}, {
id: officeSequence(),
address: {
city: "Łódź",
street: "al. Marszałka Józefa Piłsudskiego",
@ -50,6 +61,7 @@ export const sampleCompanies: Company[] = [
country: "Poland"
}
}, {
id: officeSequence(),
address: {
city: "Wrocław",
street: "Traugutta",

4
src/provider/helpers.ts Normal file
View File

@ -0,0 +1,4 @@
export const makeIdSequence = (start: number = 1) => {
let i = start;
return () => i++;
}

10
src/ui/theme.ts Normal file
View File

@ -0,0 +1,10 @@
import { createMuiTheme } from "@material-ui/core";
export const studentTheme = createMuiTheme({
props: {
MuiGrid: {
spacing: 4,
xs: 12,
}
}
})

View File

@ -3672,11 +3672,6 @@ deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
default-gateway@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@ -4751,20 +4746,6 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formik@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.1.4.tgz#8deef07ec845ea98f75e03da4aad7aab4ac46570"
integrity sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.14"
lodash-es "^4.17.14"
react-fast-compare "^2.0.1"
scheduler "^0.18.0"
tiny-warning "^1.0.2"
tslib "^1.10.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5121,7 +5102,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -6694,11 +6675,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.14:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -8832,11 +8808,6 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -9404,14 +9375,6 @@ saxes@^3.1.9:
dependencies:
xmlchars "^2.1.1"
scheduler@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"