Skip to content

Commit

Permalink
[portafly] Create Product page
Browse files Browse the repository at this point in the history
🚧 trying to manage server validation error from response body

rebase

rebase
  • Loading branch information
josemigallas committed Jul 14, 2020
1 parent d72b707 commit 01da96f
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
2 changes: 2 additions & 0 deletions portafly/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { LastLocationProvider } from 'react-router-last-location'
const OverviewPage = React.lazy(() => import('components/pages/Overview'))
const ApplicationsPage = React.lazy(() => import('components/pages/applications/ApplicationsIndexPage'))
const AccountsIndexPage = React.lazy(() => import('components/pages/accounts/AccountsIndexPage'))
const CreateProductPage = React.lazy(() => import('components/pages/product/CreateProductPage'))

const PagesSwitch = () => (
<SwitchWith404>
<LazyRoute path="/" exact render={() => <OverviewPage />} />
<LazyRoute path="/applications" exact render={() => <ApplicationsPage />} />
<LazyRoute path="/accounts" exact render={() => <AccountsIndexPage />} />
<LazyRoute path="/products/new" exact render={() => <CreateProductPage />} />
<Redirect path="/overview" to="/" exact />
</SwitchWith404>
)
Expand Down
182 changes: 182 additions & 0 deletions portafly/src/components/pages/product/CreateProductPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React, {
useState, FormEventHandler, FocusEventHandler, useEffect
} from 'react'

import {
PageSection,
TextContent,
Text,
Card,
CardBody,
Form,
FormGroup,
TextInput,
TextArea,
ActionGroup,
Button
} from '@patternfly/react-core'
import { useTranslation } from 'i18n/useTranslation'
import { useHistory, Redirect } from 'react-router'
import { createProduct, NewProduct } from 'dal/products'
import { useAsync } from 'react-async'
import { useAlertsContext } from 'components/util'
import { ValidationException } from 'utils'

type Validations = Record<string, {
validation: 'default' | 'success' | 'error',
errors?: string[]
}>

const CreateProductPage = () => {
// @ts-ignore missing strings file
const { t } = useTranslation('createProduct')
const { goBack } = useHistory()
const { addAlert } = useAlertsContext()

const [name, setName] = useState('')
const [systemName, setSystemName] = useState('')
const [description, setDescription] = useState('')
const [validations, setValidations] = useState<Validations>({
name: { validation: 'default' },
system_name: { validation: 'default' },
description: { validation: 'default' }
})

const {
isPending, error, run, data
} = useAsync({ deferFn: createProduct })

useEffect(() => {
if (error) {
if (Object.prototype.hasOwnProperty.call(error, 'validationErrors')) {
const { validationErrors } = (error as unknown as ValidationException)
const newValidations: Validations = {}
Object.keys(validationErrors).forEach((id) => {
newValidations[id] = {
validation: 'error',
errors: validationErrors[id]
}
})
setValidations({ ...validations, ...newValidations })
} else {
addAlert({ id: String(Date.now()), title: error.message, variant: 'danger' })
}
}
}, [error])

if (data) {
const { service } = data as NewProduct
return <Redirect to={`/products/${service.id}`} />
}

const isValid = validations.name.validation === 'success' && validations.system_name.validation !== 'error'

const validate = (inputName: string) => {
switch (inputName) {
case 'name':
return name.length > 5 ? 'success' : 'error'
case 'system_name':
// eslint-disable-next-line no-nested-ternary
return !systemName ? 'default' : (systemName.length > 5 ? 'success' : 'error')
case 'description':
return description.length > 0 ? 'success' : 'default'
default:
return 'default'
}
}

const onBlur: FocusEventHandler = (ev) => {
const { name: inputName } = ev.currentTarget as HTMLInputElement

const newValidations = { ...validations }

newValidations[inputName] = {
validation: validate(inputName)
}

setValidations(newValidations)
}

const onSubmit: FormEventHandler = (ev) => {
ev.preventDefault()
const formData = new FormData(ev.currentTarget as HTMLFormElement)
run(formData)
}

return (
<>
<PageSection variant="light">
<TextContent>
<Text component="h1">{t('New API Product')}</Text>
</TextContent>
</PageSection>

<PageSection>
<Card>
<CardBody>
<Form onSubmit={onSubmit}>
<FormGroup
label={t('Name')}
fieldId="name"
helperTextInvalid={validations.name.errors?.flat()}
validated={validations.name.validation}
isRequired
>
<TextInput
validated={validations.name.validation}
id="name"
type="text"
name="name"
value={name}
onChange={setName}
onBlur={onBlur}
isRequired
/>
</FormGroup>
<FormGroup
label={t('System name')}
fieldId="system_name"
helperText={t('Only ASCII letters, numbers...')}
helperTextInvalid={validations.system_name.errors?.flat()}
validated={validations.system_name.validation}
>
<TextInput
validated={validations.system_name.validation}
id="system_name"
type="text"
name="system_name"
value={systemName}
onChange={setSystemName}
onBlur={onBlur}
/>
</FormGroup>
<FormGroup
label={t('Description')}
fieldId="description"
helperTextInvalid={validations.description.errors?.flat()}
validated={validations.description.validation}
>
<TextArea
validated={validations.description.validation}
id="description"
name="description"
value={description}
onChange={setDescription}
onBlur={onBlur}
/>
</FormGroup>
<ActionGroup>
<Button type="submit" isDisabled={isPending || !isValid} variant="primary">{t('Create')}</Button>
<Button onClick={goBack} variant="link">{t('Cancel')}</Button>
</ActionGroup>
</Form>
</CardBody>
</Card>
</PageSection>
</>
)
}

// Default export needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default CreateProductPage
47 changes: 47 additions & 0 deletions portafly/src/dal/products/createProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { craftRequest, postData } from 'utils'
import { DeferFn } from 'react-async'

type CreateProductParams = {
name: string
description?: string
deployment_option?: string
backend_version?: string
system_name?: string
}

export interface NewProduct {
service: {
id: number,
name: string,
state: string,
system_name: string,
backend_version: string,
deployment_option: string,
support_email: string,
description: string,
intentions_required: boolean,
buyers_manage_apps: boolean,
buyers_manage_keys: boolean,
referrer_filters_required: boolean,
custom_keys_enabled: boolean,
buyer_key_regenerate_enabled: boolean,
mandatory_app_key: boolean,
buyer_can_select_plan: boolean,
buyer_plan_change_permission: string,
created_at: string,
updated_at: string,
links: Array<{ rel: string, href: string }>
}
}

export type CreateProductValidationErrors = {
name?: string[],
system_name?: string[]
}

const createProduct: DeferFn<NewProduct> = async ([data]) => {
const request = craftRequest('/admin/api/services.json')
return postData(request, data)
}

export { createProduct }
1 change: 1 addition & 0 deletions portafly/src/dal/products/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createProduct'
4 changes: 4 additions & 0 deletions portafly/src/types/product.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IProduct {
id: number
name: string
}

0 comments on commit 01da96f

Please sign in to comment.